pickle反序列化
简介
pickle反序列化和php反序列化差不多,本质上都是为了长期存储内存中的数据。pickle是python进行序列化/反序列化的包,早期使用的是marshal,现在开发多使用pickle。
与json相比,pickle以二进制形式存储,人工难以阅读
opcode的操作实际上是一系列的入栈出栈操作
pickle执行的过程相当于一个完整的虚拟机,pickle virtual machine(PVM)
可序列化的对象
None、True和False- 整数、浮点数、复数
- str、byte、bytearray
- 只包含可封存对象的集合,包括 tuple、list、set 和 dict
- 定义在模块最外层的函数(使用 def 定义,lambda 函数则不可以)
- 定义在模块最外层的内置函数
- 定义在模块最外层的类
__dict__属性值或__getstate__()函数的返回值可以被序列化的类(详见官方文档的Pickling Class Instances)
Object.__reduce__ 函数
python要求 object.__reduce__() 返回一个 (callable, ([para1,para2...])[,...]) 的元组,每当该类的对象被unpickle时,该callable就会被调用以生成对象(该callable其实是构造函数)。
opcode
版本
目前pickle有6个版本,每个版本会有一些区别,下面是一段对比
1 | |
1 | |
如果不指定版本的话,默认使用的是pickle4
以pickle3为例
1 | |
| 指令 | 名字 | 作用(人话翻译) |
|---|---|---|
c |
GLOBAL | 加载类 / 函数(你刚才问的) |
( |
MARK | 标记开始构建元组 |
) |
TUPLE | 结束元组,生成元组对象 |
} |
DICT | 生成字典对象 |
. |
STOP | 序列化结束 |
i |
INT | 整数 |
s |
STRING | 字符串(老版本) |
\x8c |
BINUNICODE | Unicode 字符串(Python3 常用) |
\x94 |
NEWOBJ | 创建新对象 |
h |
BINGET | 读取缓存里的值 |
\x93 |
STACK_GROW | 栈操作(内部用) |
u |
SETITEM | 往字典里加键值对 |
特性绕过\x00
由于pickle是向前兼容的,所以pickle0可以在所有版本中执行,可以看到pickle0里面没有\x00,可以利用这个特性来绕过\x00的关键词检测
漏洞利用
方法一:利用pickle库生成序列化数据
前面我们提到了__reduce__ 方法,他是用来自定义对象如何被序列化/反序列化的
在pickle.loads时会自动调用这个函数,和php中魔术方法的__wakeup__ 一样
示例代码:
1 | |
当执行pickle.dumps(pickle_data) 时,就会触发命令执行
注意事项:在使用这种方法生成序列化数据时有一个非常需要注意的地方,如果题目环境是linux,你生成数据的脚本必须也在linux中跑,否则可能会反序列化失败
方法二:手写opcode
直接生成的方法虽然方便快捷,但是一次只能执行一个函数,那么如果我需要执行多个函数怎么办呢?
这就需要用到opcode了,前面版本对比的时候可以看到,pickle0的数据是最容易读的,并且能在所有版本下执行,所以我们手写opcode都是写的版本为0的
一些操作符
| 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标记以及被组合的数据出栈,列表被更新 | 无 |
由这些opcode我们可以得到一些需要注意的地方:
- 编写opcode时要想象栈中的数据,以正确使用每种opcode。
- 在理解时注意与python本身的操作对照(比如python列表的
append对应a、extend对应e;字典的update对应u)。 c操作符会尝试import库,所以在pickle.loads时不需要漏洞代码中先引入系统库。- pickle不支持列表索引、字典索引、点号取对象属性作为左值,需要索引时只能先获取相应的函数(如
getattr、dict.get)才能进行。但是因为存在s、u、b操作符,作为右值是可以的。即“查值不行,赋值可以”。pickle能够索引查值的操作只有c、i。而如何查值也是CTF的一个重要考点。 s、u、b操作符可以构造并赋值原来没有的属性、键值对。
简单来说,就是在opcode中,不能通过lists[0]的方式获取列表中的第一个元素的值,但是可以通过lists[0] = 1 的方式给列表中的第一个元素赋值
拼接opcode
想把两段opcode拼起来也很简单,只需要把结尾用来结束的. 删掉,再拼起来就行了
覆盖变量
情景如下
1 | |
c表示加载 类/函数/变量
__main__ 表示从当前模块找
secret 接c指令,找到secret
( 压入一个MARK标记,表示要开始创建一个新字典
随后分别压入字符串name和1
d 操作找到上一个MARK标记,把这之间的的数据组合成字典,也就是得到了{“name”:”1”}
b 操作把刚刚得到的字典对secret对象进行属性赋值,name被设置为1
. 表示结束
执行函数
与执行函数相关的操作符有三种 R、i、o
1 | |
cos
system 表示引入os.system
(S’whoami’
t 表示建立一个元组 ,里面有一个元素’whoami’
R 表示把os.system当作函数,元组当作参数,实现函数执行
1 | |
1 | |
实例化一个对象
1 | |
相当于是执行了__init__(‘XiaoMing,’20’)
相关工具
pker
基于opcode绕过字节码过滤
这就不言而喻了
注意事项
虽然前面已经写过了,但是这个点确实比较搞人,所以再说一下
在windows上生成的序列化数据,可能没法在linux上用