什么是pickle
pickle是Python中一个用来序列化和反序列化对象的模块。它能将任意Python对象转换为二进制流并还原
当然在Python 中并不止pickle一个模块能够进行这一操作,更原始的序列化模块如marshal,同样能够完成序列化的任务,不过两者的侧重点并不相同,marshal存在主要是为了支持 Python 的.pyc文件。现在开发时一般首选pickle。
简单使用
Plain Text |
我们直接从示例来看
Plain Text |
opcode
Pickle反序列化过程相当于一个完整的虚拟机(Pickle VM,简称PVM)在Python解释器中执行字节码序列 。在解析字节流时,每遇到一个操作码(opcode),就执行相应操作并更新栈或memo,直到遇到终止符(.)为止,最终栈顶的对象即为反序列化结果。
由此我们能知道可以通过编写opcode来编写反序列化的字节码(opcode比直接序列化更加灵活)
常用opcode
在Python的pickle.py中,我们能够找到所有的opcode及其解释,常用的opcode如下,这里我们以V0版本为例
指令 |
描述 |
具体写法 |
栈上的变化 |
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标记以及被组合的数据出栈,列表被更新 |
opcode解析
看上去很迷惑 难以理解 这里还是直接从代码中来看
Plain Text |

这里就能看出来了 opcode能执行任意python代码
这些opcode代码是什么意思呢 看也看不懂
我们来一一理顺
Plain Text |
这里就梳理清楚了opcode的使用
这里引用两张非常形象的图片
参考:https://goodapple.top/archives/1069
1PVM解析 str 的过程

1PVM解析 __reduce__() 的过程

pickletools
pickletools是一个将opcode转化成方便我们阅读的形式的模块
简单使用
Plain Text |
pickle反序列化
直接序列化
在执行pickle.loads()时 会调用__reduce__函数 所以当服务器能够解析我们传入的pickle数据时 我们可以命令执行 由此产生漏洞
引用自Pickle反序列化 - 枫のBlog:
__reduce__()其实是object类中的一个魔术方法,我们可以通过重写类的 object.__reduce__() 函数,使之在被实例化时按照重写的方式进行。
Python要求该方法返回一个字符串或者元组。如果返回元组(callable, ([para1,para2...])[,...]) ,那么每当该类的对象被反序列化时,该callable就会被调用,参数为para1、para2...
Plain Text |
这样就可以实现命令执行了
通过opcode利用
在上文我们提到了,opcode比直接序列化更加灵活。
我们可以通过在类中重写__reduce__方法,从而在反序列化时执行任意命令,但是通过这种方法一次只能执行一个命令,如果想一次执行多个命令,就只能通过手写opcode的方式了。
示例;
Plain Text |
通过opcode我们能够执行python代码能够命令执行、实例化对象、以及覆盖变量 这里希望你能自行实现这里不再过多讲解
以上就是pickle漏洞的基本用法
ctf例题
[HZNUCTF 2023 preliminary]pickle
Bash |
因为/calc 无回显 所以我们可以尝试将读取的文件写入/111 中 然后使用/readfile 读取
不能反弹 shell 服务器 无法出网
因为有对 os 的禁用 这里使用拼写绕过
Bash |
Bash |
得知 flag 存在环境变量中 执行 env | tee a
[MTCTF 2022]easypickle
Bash |
先用 flask -unsigned 工具来爆破密码
这里根据 os.urandom(2).hex()来生成一个字典
爆破得到
SECRET_KEY=0662
这样就可以伪造 admin 身份登录进去了
然后就是 pickle 反序列化
因为对大部分包进行了过滤 所以这里直接使用 opcode 来写
通过 opcode 反弹 shell
Bash |
但是题中又对R i o b 有过滤
这里可以通过 Unicode 来绕过
V指令可以识别Unicode编码
Bash |
https://leekosss.github.io/2023/09/05/%5BMTCTF2022%5Deasypickle/#%E7%BB%95%E8%BF%87R-i-o-b%E8%BF%87%E6%BB%A4
经过 bs64 编码后 再伪造 session 传入便成功反弹 shell 得到 flag
参考博客:
Python Pickle 反序列化漏洞(原理+示例) - FreeBuf网络安全行业门户
Pickle反序列化 - 枫のBlog

