反序列化漏洞
不安全反序列化:序列化机制原理、Java/PHP/Python 利用链(gadget chain)、真实漏洞模式与防御体系。
1. 序列化与反序列化是什么
序列化(Serialization):把内存中的对象转成可传输/可存储的字节序列(JSON、二进制流等)。 反序列化(Deserialization):把字节序列还原为内存对象。
类比:序列化像「家具拆解打包」,反序列化像「照说明书重新组装」。问题在于——如果说明书 本身被调包,组装出来的可能不是衣柜,而是一台「拆掉墙的机器」:因为许多语言的反序列化 不只是恢复数据,还会执行对象重建过程中的钩子代码。
数据与行为的边界,是理解本漏洞的钥匙:JSON 反序列化只产生「数据」,而 Java/PHP/Python 的 原生反序列化会在恢复过程中自动调用方法,攻击者正是借这些方法间接力执行任意代码。
对应 OWASP Top 10 2021 的 A08(软件与数据完整性失败)、CWE-502。
2. 魔术方法与回调:漏洞的发动机
反序列化漏洞的必要条件是「反序列化过程中存在会被自动触发的代码」:
| 语言 | 自动触发点(示例) | 触发时机 |
|---|---|---|
| PHP | __wakeup __destruct | 反序列化完成时 / 对象销毁时 |
| Java | readObject 可被重写 | ObjectInputStream.readObject() 时 |
| Python | __reduce__ __setstate__ | 反序列化重建对象时 |
| .NET | OnDeserialization | 反序列化回调 |
以 Python 为例,__reduce__ 的返回值会被 pickle 当作「重建指令」直接执行:
import pickle, base64
class Exploit:
def __reduce__(self):
# 反序列化时会执行:os.system("whoami > /tmp/pwned")
import os
return (os.system, ("whoami > /tmp/pwned",))
payload = base64.b64encode(pickle.dumps(Exploit()))
print(payload.decode()) # 这串 base64 即恶意载荷,交给存在漏洞的接口即触发
3. 三大语言的攻击模式
3.1 Java:readObject + gadget chain
Java 原生序列化是历史最悠久、危害最大的反序列化体系(WebLogic、JBoss、Jenkins、 Commons-Collections 均有著名案例)。其危害模型是 gadget chain(利用链):
应用/依赖库中不存在一行「执行命令」的反序列化代码,
但存在若干「反序列化时会自动调用」的方法,它们恰好形成一条链:
恶意对象.readObject()
→ HashMap.hash() # JDK 集合类的钩子
→ InvokerTransformer.transform() # Commons-Collections 的通用方法
→ Runtime.getRuntime().exec("calc") # 链尾达成任意命令执行
要点:链上的每一环单独看都「无害且合法」,组合起来才成为武器。这就是为什么 升级一个无关业务功能的依赖库(如 commons-collections 3.x→4.x)就能修复 RCE—— 链条断在中间任意一环,整条链即失效。
3.2 PHP:POP 链与 phar
PHP 的利用链称为 POP(Property-Oriented Programming),原理同 gadget chain, 但武器是类属性:反序列化恢复属性后,魔术方法读取了被污染的属性值并造成危险调用。
<?php
// 漏洞应用代码(示例):__destruct 中把属性当模板执行
class Template {
public $code;
function __destruct() { eval($this->code); } // 危险汇聚点
}
// 攻击者提交 O:8:"Template":1:{s:4:"code";s:10:"phpinfo();";}
unserialize($_COOKIE['state']); // 反序列化即触发 __destruct → 任意代码执行
PHP 特有的入口是 phar 反序列化:file_exists()、is_dir() 等文件函数遇到
phar:// 流包装器时会自动解析 phar 元数据并触发反序列化——这意味着
文件操作函数也可能成为反序列化入口(PHP 8.0 起已限制,但存量系统庞大)。
3.3 Python:pickle 一击必中
pickle 默认执行 __reduce__ 指令,无需构造链即可直接 RCE,因此规则非常明确:
pickle 只反序列化可信数据。典型事故:用 pickle 存 session(Flask 早期生态、
Celery 老配置、Redis 队列任务体),一旦攻击者能写入该存储(如未授权 Redis),
即可通过伪造任务直接控制 worker。
3.4 真实漏洞模式盘点
| 模式 | 典型案例 | 后果 |
|---|---|---|
| Java 中间件 | WebLogic T3/IIOP、JBoss JMX、Jenkins CLI | RCE |
| Java 依赖链 | Commons-Collections 3.x、Commons-Beanutils | RCE |
| PHP 入口 | unserialize() 用户输入、phar 流 | RCE/注入 |
| Python 存储 | pickle session、Celery/Redis 队列 | RCE |
| .NET | ViewState(machineKey 泄露)、BinaryFormatter | RCE |
4. 利用链的构造方法
gadget chain 的构造不是「猜」,而是系统化搜索:
1. 确定入口 :目标 classpath 中有哪些库与版本(报错信息、/jolokia、依赖清单)
2. 列出钩子 :找实现了 readObject/__wakeup/OnDeserialization 且行为复杂的类
3. 追踪调用边 :钩子方法调用了哪些其他方法?(IDE「调用层次」或静态分析)
4. 找到汇聚点 :链上是否存在 Runtime.exec / ProcessBuilder / eval / 反射 invoke
5. 组装与验证 :ysoserial(Java)、phpggc(PHP)生成载荷,本地搭同版本环境验证
# Java:用 ysoserial 生成 CommonsCollections5 链的测试载荷(仅限授权测试)
java -jar ysoserial.jar CommonsCollections5 'ping -c1 oob.dnslog.cn' > payload.bin
# PHP:phpggc 生成 Laravel/FuelPHP 等框架链
php phpggc Laravel/RCE5 'system' 'id' --base64
防御者同样应使用这些工具:把它们接入 DAST/渗透测试,验证「这条链在我的系统上断了吗」。
5. 防御体系
5.1 第一优先:不反序列化不可信数据
跨服务/跨语言数据交换 → JSON / Protocol Buffers / Avro(只恢复数据,不执行代码)
会话存储 → 服务端 Session 或签名 JSON(JWT 仅做无状态身份,见 032)
任务队列 → 消息体用 JSON;确需二进制用带 schema 的格式
5.2 必须使用原生序列化时的加固
Java:过滤反序列化类(JEP 290 自 JDK 9 起内置,JDK 6u141/7u131/8u121 回溯提供):
ObjectInputStream ois = new ObjectInputStream(inputStream);
// 只允许业务白名单类通过反序列化
ois.setObjectInputFilter(info -> {
if (info.serialClass() != null &&
info.serialClass().getName().startsWith("com.example.model.")) {
return ObjectInputFilter.Status.ALLOWED;
}
return ObjectInputFilter.Status.REJECTED;
});
PHP:allowed_classes 白名单:
// 只允许这两个类被还原,其余一律变成 __PHP_Incomplete_Class
$obj = unserialize($data, ["allowed_classes" => ["App\\Config", "App\\Cart"]]);
Python:RestrictedUnpickler:
import pickle, io
class SafeUnpickler(pickle.Unpickler):
def find_class(self, module, name):
# 默认全拒;仅放行确需的内置类型
raise pickle.UnpicklingError(f"global '{module}.{name}' is forbidden")
.NET:淘汰 BinaryFormatter——微软官方已宣布其不安全并在 .NET 9 中默认抛异常, 迁移到 System.Text.Json / MessagePack(安全模式)。
5.3 分层防御汇总
根治层 :不安全格式不进信任边界(换 JSON/带 schema 格式)
加固层 :反序列化类白名单(JEP 290 / allowed_classes / RestrictedUnpickler)
运行层 :最小权限运行 + 容器沙箱,链命中也难落地
依赖层 :SCA 移除带 gadget 的旧依赖(Commons-Collections 3.x、旧版 XStream)
6. 检测与审计
代码审计关键词:
Java :ObjectInputStream.readObject、XMLDecoder、XStream.fromXML
PHP :unserialize((重点看参数是否可被用户影响)、phar://
Python :pickle.loads、yaml.load((必须 Loader=SafeLoader)
.NET :BinaryFormatter.Deserialize、LosFormatter(ViewState)
黑盒探针:
Java 序列化流的魔数 ac ed 00 05(十六进制)、base64 后为 rO0AB
PHP 序列化串的特征 O:8:" 或 a:1:{
Python pickle 的特征字节 \x80\x04(协议 4)
7. 常见陷阱
| 陷阱 | 事实 |
|---|---|
| 「内网服务没有风险」 | 一次 SSRF/LLMNR 投毒即可把反序列化入口变公网可达 |
| 「升级补丁就完事」 | 只要不可信数据仍能进入原生反序列化,新链还会出现 |
| 「JSON 就绝对安全」 | JSON 本身安全,但若用 JSON 承载 pickle base64 则无济于事 |
| 「加密了序列化数据就安全」 | 加密不等于完整性;密钥泄露或无 MAC 时仍可伪造 |
| 「内部流量不用验证」 | A08 的核心就是信任链,内部边界同样是信任边界 |
小结
- 初学者要点:反序列化漏洞 = 「不可信数据在还原对象时被执行了代码」。记忆锚点是
Python
__reduce__与 PHP__wakeup:反序列化会自动跑代码。换 JSON 是第一防御。 - 进阶注意:Java 的危害源于 gadget chain——链上任何一环被修复/移除即断链,因此依赖
治理(SCA)与类白名单(JEP 290)要同时做;PHP 注意 phar 扩展的文件函数入口;
Python 的 pickle 没有部分安全模式,
yaml.load同理必须SafeLoader; .NET 直接淘汰 BinaryFormatter。渗透验证优先用 ysoserial/phpggc + 带外探针, 修复后用同一工具回归,确认链条确实断掉。