Pickle 反序列化

在 Python 里,pickle 是一个用于 序列化(serialize) 和 反序列化(deserialize) Python 对象的模块。
- 序列化(pickling):把内存中的 Python 对象转成字节串,方便存盘、传输、缓存。
- 反序列化(unpickling):把字节串还原回等价的 Python 对象。
它不仅能保存基础类型(int/str/list/dict),还能处理自定义类实例、函数引用(受限制)、复杂容器等。pickle 是 Python 专用格式,不是跨语言的通用格式。
基本使用
基础用法
Pickle 库提供了下面几种 API 用于序列化和反序列化:
函数 | 功能 | 说明 |
---|---|---|
pickle.dump(obj, file, protocol=...) |
序列化对象到文件 | 文件需以 "wb" 打开 |
pickle.dumps(obj, protocol=...) |
序列化对象为 bytes | 内存用 |
pickle.load(file) |
从文件反序列化 | 文件需以 "rb" 打开 |
pickle.loads(bytes) |
从 bytes 反序列化 |
示例代码:
1 | import pickle |
另外像 pickle.dumps
这种序列化 API 中有一个 protocol
参数可以指定序列化的协议版本。这是因为 Pickle 有多个协议(protocol)版本,数字越大功能越强,性能越好,但可读性和兼容性差。
- 0:文本格式,最兼容
- 1:早期二进制协议
- 2:支持新式类、效率提升
- 3:Py3 默认,支持
bytes
- 4:支持大对象、
set
、fronzenset
等 - 5:引入
PickleBuffer
,支持 out-of-band buffer(零拷贝)
我们可以观察到不同版本协议下序列化的结果是不同的:
1 | import pickle |
提示
在手动构造 Payload 的时候我们通常选择协议 0,因为这个版本的协议是文本协议,可读性好且兼容性好。
pickletools
模块可以反汇编 pickle 字节流,看它包含哪些 opcode(方便安全审计)。
1 | import pickle, pickletools |
自定义序列化
默认的 Pickle 会尝试用“类+构造参数+属性字典”把对象还原,但:
- 有些类不容易自动还原(例如不可变类型、
__slots__
、需要构造器参数的对象); - 你可能想压缩/脱敏/裁剪状态,或做版本迁移;
- 你想明确控制“反序列化时调用哪个函数,用哪些参数,然后如何恢复属性”。
因此 Pickle 提供了类的魔术方法可以支持自定义的序列化/反序列化。
reduce / reduce_ex
__reduce__
/ __reduce_ex__(protocol)
这两个方法告诉 pickle:“如何重建我”。它们需要返回一个 tuple,基本形态:
1 | (callable, args_tuple[, state[, listitems[, dictitems]]]) |
最常见的是前两位或前三位:
callable
:反序列化时要被调用的可调用对象,通常是类的构造函数,或者你定义的一个工厂函数。args_tuple
:传给callable
的参数元组,这些参数将用于重新构造对象。state
(可选):对象的额外状态(通常是一个属性字典),用来恢复对象的内部状态。
如果实现了 __reduce__
魔术方法,则 Pickle 反序列化等价于:
1 | obj = callable(*args_tuple) |
callable(*args_tuple)
:在反序列化时,pickle
会调用callable(*args_tuple)
来创建一个新的对象。这个callable
通常是类本身,而args_tuple
是传给该类构造函数的参数。所以,反序列化实际上是通过类和构造函数参数来重建对象。恢复
state
:如果__reduce__
返回了一个state
(通常是一个字典),那么在创建好对象后,pickle
会尝试恢复对象的状态:如果对象实现了
__setstate__(state)
方法,pickle
会调用该方法,这是常见的恢复对象状态的方式。1
obj.__setstate__(state)
如果没有
__setstate__
方法,pickle
会通过obj.__dict__.update(state)
来手动更新对象的属性字典,从而恢复状态。
这里的
state
主要是对象的内部属性,例如self.__dict__
,通常在序列化时提取,或者是自定义的状态数据。
提示
_reduce_ex__(protocol)
比__reduce__
优先。如果类实现了__reduce_ex__
,那么 pickle 会优先调用它;如果没有实现__reduce_ex__
,则会回退到调用__reduce__
。Pickle 序列化时将
__reduce__
返回的元组(通常是(callable, args_tuple)
)写入数据流,反序列化时根据这个元组调用callable(*args_tuple)
来重建对象。这样,pickle 在反序列化时更具灵活性,但也带来了潜在的安全风险,因为它不仅还原数据,还可能执行某些逻辑。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20import pickle
import os
# 自定义类,定义 __reduce__ 方法
class CommandExecutor:
def __init__(self, command):
self.command = command
def __reduce__(self):
# 返回值是 (callable, args_tuple),会在反序列化时调用
return (os.system, (self.command,)) # os.system 执行命令
# 创建 CommandExecutor 对象
obj = CommandExecutor("echo 'Hello, World!'")
# 序列化对象
serialized_obj = pickle.dumps(obj)
# 反序列化对象并执行命令
pickle.loads(serialized_obj)
getstate / setstate
__getstate__
和 __setstate__
是另外两个方法,它们用于导出和恢复对象的内部状态。
__getstate__
:序列化时调用,返回对象的状态(通常是属性字典)。__setstate__
:反序列化时调用,用来恢复对象的状态。
例如,我们可以在 __getstate__
中对对象进行一些处理,过滤掉不必要的数据,或对数据进行压缩。
1 | class MyClass: |
提示
__reduce__
是与对象的序列化过程相关,它返回一个“配方”(如(callable, args_tuple)
),这个配方会在 序列化时 写进字节流,并且 反序列化时 被用来恢复对象。__getstate__
和__setstate__
只与对象的状态有关,它们不涉及对象的构建过程,而是直接导出和恢复对象的状态(例如属性、字段等)。
限制对象加载
在反序列化时,pickle
遇到某些指令(如 GLOBAL
或 STACK_GLOBAL
)时,需要按模块名 + 对象名去加载一个全局对象,比如:
GLOBAL
(协议 0,c
)STACK_GLOBAL
(协议 4,\x93
)REDUCE
(间接触发,R
)
这一步是通过 Unpickler.find_class(module, name)
方法完成的。默认实现会直接导入对应模块并取出对象——这也是攻击者可利用的执行点。
1 | def find_class(self, module, name): |
为了降低风险,可以继承 pickle.Unpickler
并重写 find_class
,在里面做白名单检查:
- 只允许加载你指定的安全对象(如
builtins.range
等); - 其余一律抛出
UnpicklingError
阻止执行。
注意
:基础类型(int
、list
、dict
等)在反序列化时并不会走 find_class
,它们由专用的 pickle 指令处理,所以不会被白名单逻辑拦截。这种限制方法只影响“按名加载”的对象(即类、函数等可全局引用的对象)。
1 | import builtins, io, pickle |
提示
pickle.Unpickler
需要一个“类文件对象”来读取 pickle 流。
而 io.BytesIO
会把 bytes 类型(二进制)的 pickle 数据包装成一个内存文件对象,就像用 open()
打开的文件一样可以被读取,只是数据在内存中,不在磁盘里。
load()
是 Unpickler
的方法,会开始从传入的文件对象读取 pickle 数据,逐条解释指令,创建对象,最后返回反序列化结果。
Pickle 字节码
反序列化原理
pickle 的序列化结果是指令流(opcode + 参数),反序列化就是一条条解释执行这些指令。其中会涉及到如下概念:
- 数据栈(stack):用于临时存放构造中的对象、参数等。
- 标记(MARK):用来圈定一段栈内容,之后用来打包成 list/tuple/dict 或函数参数。
- 备忘录(memo):一个索引 → 对象 的字典,用于保存已创建对象,解决重复引用和循环引用。
- 构造路径:简单对象直接压栈,复杂对象通过
GLOBAL
/REDUCE
/BUILD
等组合指令构造。
pickle 反序列化的大致流程如下:
- 初始化 Unpickler
- 反序列化入口是
pickle.Unpickler(file_like).load()
。 - 这个对象持有:
- 文件流(pickle 数据源,可以是
BytesIO
内存文件) - 数据栈
- memo
- find_class 方法(可重写来做白名单)
- 文件流(pickle 数据源,可以是
- 反序列化入口是
- 逐字节读取指令
- 从 pickle 数据流中读取一个字节(opcode)。
- 根据 opcode 类型,可能需要继续读取参数(文本或二进制)。
- 执行指令
- 如果是常量(
NONE
、INT
、STRING
等)→ 直接构造对象压栈。 - 如果是容器(
LIST
、DICT
、TUPLE
等)→ 从最近MARK
后的栈元素收集打包。 - 如果是
GLOBAL
→ 调用find_class(module, name)
取出全局对象(通常是类/函数)。 - 如果是
REDUCE
→ 从栈取(callable, args_tuple)
,执行callable(*args)
,结果压栈。 - 如果是
BUILD
→ 恢复对象状态(__setstate__
或__dict__.update
)。 - 如果是
PUT
/GET
→ 在 memo 中存取对象引用。
- 如果是常量(
- 遇到 STOP
- 返回栈顶对象作为反序列化结果。
字节码指令
下面只介绍pickle
协议 0(protocol 0)的指令集。它是最早的纯 ASCII 文本协议:所有参数用可读文本表示并以换行结尾,解释器像“栈机器”一样按指令对栈做操作,并用一个备忘录(memo)表来处理共享/循环引用。
栈与流程控制指令
指令名 | 字符 | 作用 | 参数形式 | 栈操作示例 |
---|---|---|---|---|
MARK | ( |
压入标记对象,用于收集一段元素 | 无 | [...] → [..., ⟂] |
STOP | . |
结束 pickle,返回栈顶对象 | 无 | [obj] → return obj |
POP | 0 |
弹出一个栈顶元素 | 无 | [..., x] → [...] |
POP_MARK | 1 |
回退到最近的 MARK (把该 MARK 也弹掉) |
无 | [..., ⟂, a, b] → [...] |
DUP | 2 |
复制栈顶元素 | 无 | [..., x] → [..., x, x] |
常量与标量(文本编码)
指令名 | 字符 | 作用 | 参数形式 | 栈操作示例 |
---|---|---|---|---|
NONE | N |
压入 None |
无 | [...] → [..., None] |
INT | I |
压入十进制整数或布尔 | I42\n / I01\n / I00\n |
[...] → [..., 42] |
LONG | L |
压入大整数(文本十进制) | L1234567890\n |
[...] → [..., big_int] |
FLOAT | F |
压入浮点数(文本十进制) | F3.14\n |
[...] → [..., 3.14] |
STRING | S |
压入字节串/文本的旧式表示,参数是 Python repr 风格(带引号、转义,换行结尾) |
S'abc'\n |
[...] → [..., b'abc'] |
UNICODE | V |
压入 Unicode 文本,参数是 raw-unicode-escape 编码 | Vabc\n |
[...] → [..., "abc"] |
容器构造指令
列表构造指令:
指令名 | 字符 | 作用 | 参数形式 | 栈操作示例 |
---|---|---|---|---|
EMPTY_LIST | ] |
压入空列表 | 无 | [...] → [..., []] |
LIST | l |
把从最近 MARK 之后的元素收集成 list |
无 | [..., ⟂, a, b] → [..., [a, b]] |
APPEND | a |
把栈顶 x 追加到其下方的 list |
无 | [..., L, x] → [..., L+[x]] |
APPENDS | e |
把 MARK 之后的连续元素批量追加到其下方的 list |
无 | [..., L, ⟂, a, b] → [..., L+[a, b]] |
元组构造指令:
指令名 | 字符 | 作用 | 参数形式 | 栈操作示例 |
---|---|---|---|---|
EMPTY_TUPLE | ) |
压入空元组 | 无 | [...] → [..., ()] |
TUPLE | t |
把 MARK 之后的元素收集成 tuple |
无 | [..., ⟂, a, b] → [..., (a, b)] |
字典构造指令:
指令名 | 字符 | 作用 | 参数形式 | 栈操作示例 |
---|---|---|---|---|
EMPTY_DICT | } |
压入空字典 | 无 | [...] → [..., {}] |
DICT | d |
把 MARK 之后按 k1, v1, k2, v2, … 的顺序收集成 dict |
无 | [..., ⟂, k1, v1, k2, v2] → [..., {k1:v1, k2:v2}] |
SETITEM | s |
把顶上两项作为 key, value 设置到其下方的 dict |
无 | `[…, D, k, v] → […, D |
SETITEMS | u |
把 MARK 之后的若干对 (k, v) 批量设置进其下方的 dict |
无 | `[…, D, ⟂, k1, v1, k2, v2] → […, D |
备忘录(memo)指令
指令名 | 字符 | 作用 | 参数形式 | 栈操作示例 |
---|---|---|---|---|
PUT | p |
将栈顶对象存入备忘录(文本索引) | p0\n |
memo[0] = top |
GET | g |
从备忘录取对象压栈(文本索引) | g0\n |
push(memo[0]) |
对象构造与执行
指令名 | 字符 | 作用 | 参数形式 | 栈操作示例 |
---|---|---|---|---|
GLOBAL | c |
按模块+名称加载全局对象 | "module\nname\n" |
[...] → [..., find_class(module, name)] |
REDUCE | R |
调用可调用对象构造新对象 | 无 | [..., callable, args_tuple] → [..., callable(*args_tuple)] |
INST | i |
老式类实例化(MARK 收集参数) | "module\nname\n" |
[..., ⟂, arg1, arg2, ...] → [..., find_class(module, name)(arg1, arg2, ...)] |
OBJ | o |
老式实例构造(MARK 收集参数) | 无 | [..., ⟂, callable, arg1, arg2, ...] → [..., callable(arg1, arg2, ...)] |
BUILD | b |
恢复对象状态 | 无 | [..., obj, state] → [..., obj] (对象状态已更新) |
持久化钩子
指令名 | 字符 | 作用 | 参数形式 | 栈操作示例 |
---|---|---|---|---|
PERSID | P |
通过 persistent_load 加载对象 | pid\n |
把文本持久化 ID交给 Unpickler.persistent_load(pid) 来解析成对象并压栈 |
常见情景
覆盖全局变量
以这段代码为例:
1 | # secret.py |
执行过程为:
c__main__\nsecret\n
→GLOBAL
: 调用find_class("__main__", "secret")
,把__main__
模块中名为secret
的全局对象 压栈。
⚠️ 这要求在main.py
里,确实存在一个名为secret
的全局对象(这里因为import secret
,__main__
的全局命名空间里就有个名字secret
指向那个模块对象)。(
→MARK
:打标记,准备“收集一段元素”。S'name'\n S'1'\n d
→ 依次把'name'
、'1'
压栈,然后d
(DICT
)把MARK
后的元素收集成字典:{'name': '1'}
。b
→BUILD
:对栈下方那个对象(也就是第一步拿到的secret
模块对象)执行状态恢复:- 如果对象定义了
__setstate__
就调用它; - 否则执行
obj.__dict__.update(state)
。
因为模块对象有__dict__
,于是相当于:secret.__dict__.update({'name': '1'})
,从而**覆盖了模块变量secret.name
**。
- 如果对象定义了
.
→STOP
:结束。
对应的栈变化为:
1 | [] |
结果:secret.name
从原值被改为 '1'
。
函数调用
REDUCE (R)
路径
1 | b'''cos |
执行过程为:
c os\n system\n
→GLOBAL
:压入os.system
这个可调用对象。(
S'whoami'\n
t
→MARK
+参数+TUPLE
:把'whoami'
打成参数元组('whoami',)
。R
→REDUCE
:执行os.system('whoami')
,把返回值(命令退出码)压栈。.
→ 结束、返回退出码。
对应的栈变化为:
1 | [] |
INST (i)
路径
1 | b'''(S'whoami' |
执行过程为:
(
S'whoami'\n
→ 压入MARK
和'whoami'
作为参数。i os\n system\n
→INST
:老式“构造实例”指令,会把栈上从MARK
开始的元素作为参数,调用 指定模块名与对象名 指向的可调用对象。
这实际上会调用os.system('whoami')
,虽然这个指令名看起来像“实例化类”,但底层就是调用可调用对象(当年主要用于旧式类)。.
→ 返回。
对应的栈变化为:
1 | [] |
OBJ (o)
路径
1 | b'''(cos |
执行过程为:
(
压入MARK
c os\n system\n
→ 把os.system
压栈S'whoami'\n
→ 压入参数o
→OBJ
:使用MARK
后的内容作为参数调用栈中的“类/可调用对象”,得到结果压栈(历史上用于“构造类实例”,本质还是“调用”)。.
→ 返回
对应的栈变化为:
1 | [] |
实例化对象
1 | class Student: |
执行过程为:
GLOBAL "__main__"\n "Student"\n
→ 压入Student
构造器;(
+S'XiaoMing'
+S'20'
+t
→ 参数元组('XiaoMing', '20')
;R
→ 调用Student('XiaoMing', '20')
;.
→ 返回实例。
对应的栈变化为:
1 | [] |
pker 编写字节码
pker
是一个安全研究工具,可将受限子集的 Python 源码直接转换为 Pickle 协议 0(文本)指令串。
它主要用于构造 Pickle 反序列化的 payload,在安全测试、CTF 中快速生成协议 0 的可执行 pickle 数据,而无需手写复杂的 opcode。
工具代码
1 | # -*- coding: utf-8 -*- |
使用方式
1 | python3 pker.py [选项] [文件...] |
输入优先级
-c/--code
→ 直接传入一段源码字符串- 文件参数(可多个,依次拼接)
- 无参数时:
- 如果 stdin 是管道/重定向 → 读 stdin
- 否则进入交互模式(两次空行结束输入)
输入模式
模式 | 示例 | 说明 |
---|---|---|
源码字符串 | pker.py -c "x=1;return x" |
直接在命令行传代码 |
文件 | pker.py payload.py |
读取指定源码文件 |
多文件 | pker.py part1.py part2.py |
合并多个文件内容生成 |
stdin | `cat payload.py | pker.py` |
强制 stdin | pker.py - |
不管 TTY 状态,直接读 stdin |
交互模式 | pker.py |
手动输入,连续两次空行结束 |
输出格式
选项 | 说明 | 默认 |
---|---|---|
--bytes |
Python bytes 字面量(b"..." ) |
✅ |
--raw |
原始协议 0 文本(便于直接查看 opcode) | |
--hex |
十六进制字符串(便于嵌入其他语言) |
输出可保存到文件:
1 | pker.py payload.py -o out.txt |
支持的语法
pker
并不是一个完整的 Python 编译器,它只识别并处理一个受限的语法子集,并将其转换为对应的 Pickle 协议 0 文本指令。
支持的值类型:
- 基本类型:
int
、float
、str
、None
、bool
- 容器类型:
list
、tuple
、dict
- 变量引用(必须先赋值过)
赋值(变量会存入 pickle 的 memo 表)
形式 | 含义 | 生成效果(协议 0) |
---|---|---|
x = value |
将 value 的结果保存到变量 x (memo 表) |
<构造 value 的指令> pN\n0 (PUT N,保存到 memo 槽位) |
x[i] = value |
给容器 x 的下标 i 赋值 |
g<idx>\n <构造 i> <构造 value> s (GET idx,SETITEM) |
x.attr = value |
给对象 x 的属性 attr 赋值 |
g<idx>\n (}(<构造 attr> <构造 value> d t b) (通过 BUILD 更新属性) |
示例:
1 | a = [0] # 列表存到 memo |
注意
protocol 0 里也没有“读属性/读下标”的专用 opcode,因此通常需要 GLOBAL('builtins','getattr')(x, '__getitem__')
来读取。但这在很多题里 builtins
会被禁用,所以通用性不强。
因此这里 pker
不支持下标读取的语法:
1 | getattr = GLOBAL('builtins','getattr') |
函数调用
普通函数调用语法如下,其中 func
必须是已赋值过的变量。
1 | func(a, b) |
上述代码会被转换为 g<func_idx>\n (<构造 a> <构造 b> t R)
(GET 函数 → 压参数 → TUPLE → REDUCE 调用)
另外 pker 还支持嵌套调用:
1 | f()() |
pker 会先生成外层调用结果,再将结果作为可调用对象继续生成调用指令。
return(顶层可用)
形式 | 生成效果 |
---|---|
return |
. (STOP,返回 None) |
return expr |
<构造 expr> . (返回 expr 结果) |
内置宏说明
内置宏是 pker
特有的保留字,用于快速生成特定的 pickle 指令,这些指令直接映射到协议 0 的“全局对象调用”相关 opcode。
GLOBAL(module, name)
引用模块中的对象(协议 0:c{module}\n{name}\n
)
1 | system = GLOBAL('os', 'system') |
INST(module, name, *args)
实例化对象(协议 0:(<构造 args...> i{module}\n{name}\n
)
1 | obj = INST('collections', 'Counter', ['a', 'b', 'a']) |
OBJ(callable, *args)
调用可调用对象(协议 0:(<构造 callable> <构造 args...> o
)
1 | result = OBJ(GLOBAL('math', 'pow'), 2, 3) |
反序列化绕过
绕过 Restricted Unpickler
绕过黑名单
寻找未被限制的利用链
Code-Breaking picklecode 给出了一个黑名单的场景,源码如下:
1 | import pickle |
这道题将 eval
,exec
等关键词进行了过滤,这意味着无法直接通过 builtins.eval
(即 GLOBAL(builtins, 'eval')
)来获取 eval
函数。
一种方法是通过 builtins.getattr(builtins, 'eval')
进行绕过。然而我们需要先 getattr
的第一个参数 builtins
模块,因为 GLOBAL
只能获取模块中的某个对象,也就是说我们需要从 builtin.globals()
的结果中通过 dict.get
函数获取。
1 | getattr = GLOBAL('builtins','getattr') |
另一种比较简便的方式是直接通过 builtins.__getattribute__('eval')
获取。
1 | return GLOBAL('builtins','__getattribute__')('eval')('__import__("os").system("ls")') |
绕过白名单
模块劫持
BalsnCTF 2019 Pyshv1 中使用了白名单进行限制。
1 | ## securePickle.py |
securePickle.py
中声明了一个 whitelist
, server.py
中将 sys
模块添加到白名单中。也就是说这里只允许加载 sys
模块。
然而 sys
模块中有一个 modules
字典用来维护已加载模块名称与模块对象的映射,例如当我们调用 GLOBAL('sys', 'modules')
实际上会先尝试 modules
的 sys
键对应的值作为模块对象。
换而言之我们可以先 GLOBAL('sys', 'modules')
获取到 sys.modules
并设置 modules['sys'] = modules
,此时 sys.modules
被看做是 sys
模块:
- 我们可以通过
GLOBAL('sys', 'get')
获取sys.modules
中的任意模块。尝试发现os
会随着sys
一并加载,因此我们可以获取os
模块。 - 将获取的
os
模块覆写到sys.modules['sys']
,此时我们可以将os
模块看做是sys
模块使用。
1 | modules = GLOBAL('sys', 'modules') |
属性创建与函数劫持
BalsnCTF 2019 Pyshv2 中白名单只有一个 structs 模块。与 BalsnCTF 2019 Pyshv1 不同的是,BalsnCTF 2019 Pyshv1 中使用了 pickle.Unpickler.find_class(self, module, name)
去寻找类,而这道题中使用的是 __import__
和 getattr
去获取。
1 | ## securePickle.py |
structs
模块是题目给出的 structs.py
,其内容为空。对于 python 中导入的模块,都以通过获取其 __builtins__
属性来获取 builtins
模块对应的字典,获取到这个字典之后,再获取 eval
函数就可以执行任意 python 代码了。目标是构造如下的代码:
1 | structs.__builtins__['eval'] |
由于需要从字典中取值,因此也需要用到 dict.get
函数,由于这里获取到的 __builtins__
只是一个字典,因此获取 dict.get
函数需要通过 __builtins__['dict'].get
去获取,再次需要通过字典去索引,这就会陷入死循环。
跳出这个死循环的关键是如何不通过 __builtins__['dict'].get
获取 get
函数。因为 __builtins__
本身就是字典,因此这里我们采用现有的 getattr
函数执行 getattr(__builtins__, 'get')
获取。
注意到白名单校验之后会通过 __import__
函数去导入模块,然后使用 getattr
获取属性。
1 | module = __import__(module) |
假如我们要通过 getattr(module, name)
获取到 dict.get
函数,就需要 module
的值为 __buitlins__
字典,name
为 get
,则上一步 module = __import__(module)
需要获取到 __buitlins__
字典。但 module
的值仅能为 structs
,因此正常情况下无法获取。
但 pickle 在反序列化时是可以进行全局覆盖的,如果将 __import__
覆盖成 __getattribute__
, 则在执行 module = __import__(module)
时,相当于执行:
1 | module = structs.__getattribute__('structs') |
此时会获取 structs
模块的 structs
属性。因此这里需要将 structs.structs
赋值为 __buitlins__
字典。但由于 structs.structs
从未声明过,因此不可以直接使用 GLOBAL('structs', 'structs')
这样的形式进行获取,而是需要通过直接给 structs.__dict__['structs']
赋值来实现。
1 | __dict__ = GLOBAL('structs', '__dict__') |
接着是覆盖 __import__
为 __getattribute__
, 然后获取 dict.get
函数。
1 | gtat = GLOBAL('structs', '__getattribute__') |
获取到 dict.get
函数之后,就可以从 __buitlins__
字典获取 eval
函数了。最终 exp 为:
1 | __dict__ = GLOBAL('structs', '__dict__') |
注意
这里已经将 __import__
函数进行了覆盖,因此不能再使用 eval('__import__("os").system("ls")')
来执行系统命令,但是可以使用 open
函数来读取文件内容。
有些题目会在自定义 find_class
里直接**调用原版的 pickle.Unpickler.find_class
**,而不是自己去 __import__
:
1 | def find_class(self, module, name): |
这种调用等于让 C 实现的反序列化代码(内部逻辑)直接去解析全局名、导入模块,不会经过我们改过的 __import__
(或者即便经过,也会在更早阶段做安全检查)。
方法创建
利用代码对象篡改函数
利用 load_build RCE
利用 load_newobj RCE
绕过操作码过滤
绕过字符串过滤
绕过 R 操作码过滤
- Title: Pickle 反序列化
- Author: sky123
- Created at : 2025-08-11 23:58:04
- Updated at : 2025-08-11 23:57:48
- Link: https://skyi23.github.io/2025/08/11/Pickle 反序列化/
- License: This work is licensed under CC BY-NC-SA 4.0.