本文实验基于 PortSwigger Web Security Academy 官方教学靶场,仅针对指定 Lab 进行授权测试,目的是理解 Java 不安全反序列化与 Gadget Chain 的利用机制,不涉及真实网站或第三方系统。
前面的反序列化实验主要通过修改对象属性、改变数据类型,或者注入具有危险 Magic method 的对象来影响应用行为。这些利用路径比较直接:攻击者控制某个对象字段,应用又恰好把这个字段传入了权限判断、文件操作或其他危险功能。
但在真实 Java 应用中,反序列化入口附近通常没有一个可以直接执行危险操作的方法。攻击者需要利用应用及其依赖库中已经存在的多段代码,把反序列化期间自动触发的方法逐步连接到危险 Sink,这就是 Gadget Chain。
一、Gadget Chain 的本质
Gadget 是目标应用、Java 标准库或第三方依赖中已经存在的一段代码。它能够在特定对象状态下被触发,并产生攻击链所需的中间效果。
单个 Gadget 不一定具有直接危害。例如,它可能只会:
- 比较两个对象;
- 转换一个对象;
- 调用另一个对象的方法;
- 创建一个类的实例;
- 读取某个对象属性。
但是,如果前一个 Gadget 的输出能够成为后一个 Gadget 的输入,多个 Gadget 就可以形成连续的调用关系:
不可信序列化数据
→ 反序列化自动触发入口 Gadget
→ 中间 Gadget 继续传递或转换数据
→ 数据到达危险 Sink
→ 产生文件操作、网络请求或命令执行
需要明确的是,Gadget Chain 不是攻击者向服务器提交的一串新方法。链条中的方法代码原本就存在于目标服务器。攻击者控制的是对象类型、字段值以及对象之间的引用关系,使服务器在反序列化这些对象时沿着预期之外的路径执行现有代码。
这类利用方式也常被称为 Property-Oriented Programming。它关注的不是向应用注入新代码,而是通过控制对象属性和对象图,让应用已有方法按照攻击者需要的顺序执行。
二、Java 反序列化恢复的不只是字段值
Java 序列化数据通常保存以下内容:
对象属于什么类
对象包含哪些字段
字段的值是什么
对象之间如何互相引用
反序列化时,服务器会根据序列化流中的类描述,从自己的运行环境中查找对应的类,然后创建对象并恢复字段。
因此:
攻击者提供对象结构和对象状态
服务器提供真正执行的方法代码
Java 原生序列化流通常以以下字节开头:
AC ED 00 05
经过 Base64 编码后,常表现为:
rO0AB…
所以在 HTTP Cookie、请求参数或请求体中发现 rO0AB,可以初步判断底层数据可能是 Java 原生序列化对象。
但它只能证明数据格式,不代表漏洞已经成立。还需要继续确认:
客户端能否控制这些数据
→ 数据是否真的进入 ObjectInputStream.readObject()
→ 服务器能否加载对象图中的类
→ 是否存在反序列化过滤器
→ 是否有可达的 Gadget Chain
→ 链条能否最终进入危险 Sink
三、Gadget Chain 中的关键角色
一条完整的反序列化利用链通常包含四个部分。
1. 反序列化入口
入口是处理不可信数据的反序列化操作,例如:
ObjectInputStream input =
new ObjectInputStream(requestInputStream);
Object object = input.readObject();
不安全反序列化的根因,是应用允许客户端可控的数据进入这种能够恢复复杂对象图的反序列化机制。
2. Kick-off Gadget
攻击者只能发送数据,不能直接要求服务器依次调用某些方法。因此,调用链必须从一个会在反序列化期间自动执行的方法开始。
这个入口称为 Kick-off Gadget。
它可能来自:
- 类自定义的 readObject();
- readResolve();
- 对象校验回调;
- 集合恢复、排序或哈希重建过程;
- 其他由对象生命周期自动触发的行为。
Kick-off Gadget 的作用是把程序从“恢复字段”带入“执行现有方法”的阶段。
3. 中间 Gadget
中间 Gadget 负责把调用继续传递下去。它可能:
- 调用对象字段所指向的方法;
- 将输入转换为另一个对象;
- 触发 Comparator、Transformer 或回调;
- 实例化一个攻击者指定的类;
- 将攻击者控制的数据送入下一个组件。
攻击者需要控制的不一定是每个方法的参数,也可能是对象字段、接口实现、Comparator、回调对象或者嵌套对象关系。
4. Sink
Sink 是最终执行危险操作的位置,例如:
Runtime.exec()
文件写入或删除
动态类加载
网络连接
反射调用
脚本或表达式执行
漏洞、Gadget 和 Sink 不能混为一谈:
不可信数据被反序列化:漏洞根因
Gadget:可以被复用的现有代码组件
Gadget Chain:多个组件形成的可达调用链
Sink:最终执行危险操作的位置
四、Classpath 和 ObjectInputFilter 为什么重要
理解 Java Gadget Chain,必须理解两个成立条件:服务器是否拥有相关类,以及服务器是否允许恢复这些类。
Classpath:服务器是否拥有这些代码
Classpath 是当前 Java 程序能够加载到的类代码集合,包括:
- Java 标准库;
- 应用自己编写的类;
- 第三方 JAR;
- Spring Boot 的 BOOT-INF/lib;
- Web 应用的 WEB-INF/lib;
- 应用服务器提供的共享依赖。
假设恶意序列化数据中声明存在:
org.apache.commons.collections4
.comparators.TransformingComparator
服务器不会从 Cookie 中接收这个类的完整方法代码,而是通过自己的 ClassLoader 在 Classpath 中查找。
如果对应 JAR 存在:
找到类
→ 创建对象
→ 恢复字段
→ 相关方法可以被调用
如果类不存在:
找不到类
→ ClassNotFoundException
→ 对象图无法完整恢复
→ Gadget Chain 中断
因此,某条 ysoserial Gadget Chain 能否工作,首先取决于目标环境是否具有兼容的依赖和类版本。
ObjectInputFilter:服务器是否允许恢复这些对象
即使服务器的 Classpath 中存在相关类,也不代表反序列化一定允许使用它们。
ObjectInputFilter 可以检查:
- 正在恢复的类;
- 数组长度;
- 对象图深度;
- 对象引用数量;
- 序列化流大小。
安全的配置应只允许预期业务类型,例如:
允许 UserSession
允许 String
允许少量必要的业务类
拒绝其他类型
当恶意对象图中出现:
PriorityQueue
TransformingComparator
TemplatesImpl
过滤器可以在链条启动前拒绝反序列化。
这里还有一个容易忽略的问题:应用在 readObject() 之后进行类型转换,并不能代替反序列化过滤。
例如:
UserSession session =
(UserSession) input.readObject();
即使恶意对象最终不是 UserSession,对象图也可能已经在类型转换前被部分恢复,相关 readObject() 或其他自动方法也可能已经执行。最终出现 ClassCastException 或“Invalid user”,并不代表此前没有产生危险副作用。
五、CommonsCollections4 Gadget Chain 的执行机制
PortSwigger Lab 明确说明目标使用 Java 序列化 Session,并加载了 Apache Commons Collections。官方实验使用 ysoserial 中的 CommonsCollections4 预构建链。
CommonsCollections4 是 ysoserial 中一条具体 Gadget Chain 的名称,它依赖的主要第三方组件是:
org.apache.commons:commons-collections4:4.0
该链可以抽象为:
PriorityQueue.readObject()
→ TransformingComparator.compare()
→ ChainedTransformer.transform()
→ ConstantTransformer
→ InstantiateTransformer
→ TrAXFilter
→ TemplatesImpl
→ Runtime.exec()
PriorityQueue:启动调用链
PriorityQueue 是 Java 的优先队列。它在反序列化后需要恢复内部堆结构,以确保队列元素仍然保持正确顺序。
恢复过程中需要比较元素:
PriorityQueue.readObject()
→ 重建堆结构
→ 比较队列元素
→ 调用 comparator.compare()
ysoserial 将 PriorityQueue 的 Comparator 设置为经过特殊构造的 TransformingComparator。
因此,正常的集合恢复行为成为了自动进入 Gadget Chain 的入口:
PriorityQueue.readObject()
→ TransformingComparator.compare()
这里的 PriorityQueue.readObject() 就是 Kick-off Gadget。
TransformingComparator:进入 Transformer
TransformingComparator 的正常功能,是先通过 Transformer 转换待比较对象,再比较转换后的结果。
其调用关系可以理解为:
Object transformed =
transformer.transform(value);
ysoserial 将其中的 Transformer 设置为:
ChainedTransformer
于是调用继续进入:
TransformingComparator.compare()
→ ChainedTransformer.transform()
ChainedTransformer:连接中间 Gadget
ChainedTransformer 的正常功能,是把多个转换操作依次连接:
输入
→ Transformer 1
→ Transformer 2
→ Transformer 3
→ 输出
在这条利用链中,它通过 ConstantTransformer 和 InstantiateTransformer 等组件,最终完成:
获得 TrAXFilter 类
→ 使用恶意 TemplatesImpl 作为构造参数
→ 实例化 TrAXFilter
调用过程可以简化为:
ChainedTransformer
→ ConstantTransformer
→ InstantiateTransformer
→ new TrAXFilter(恶意 TemplatesImpl)
TemplatesImpl:到达命令执行 Sink
ysoserial 会动态生成一段 Java 类字节码,并将命令执行逻辑放入该类的初始化过程:
Runtime.getRuntime().exec(command);
随后,这些字节码被写入 TemplatesImpl 对象的内部字段。
当 TrAXFilter 使用该 TemplatesImpl 创建 Transformer 时,TemplatesImpl 会加载其中的类字节码。类初始化后,嵌入的命令执行逻辑被触发。
整个链条的含义是:
反序列化 PriorityQueue
→ 队列恢复需要比较元素
→ Comparator 需要转换元素
→ Transformer 链实例化 TrAXFilter
→ TrAXFilter 使用 TemplatesImpl
→ TemplatesImpl 加载恶意字节码
→ Runtime.exec() 执行命令
这些类原本分别用于队列排序、对象转换、对象实例化和 XSLT 模板处理。危险来自攻击者能够控制它们之间的组合方式,使原本分散的正常功能形成通往命令执行 Sink 的调用链。
六、ysoserial 的作用与 Lab 验证
手工寻找 Gadget Chain 通常需要大量源码分析。需要逐步确定:
哪个方法会自动执行
→ 它会调用哪些方法
→ 攻击者能控制哪些对象属性
→ 返回值能否继续传递
→ 最终能否到达危险 Sink
在没有源码的情况下,手工构造复杂 Gadget Chain 通常非常困难。
但很多 Java 应用使用相同的第三方依赖。研究人员已经在部分常见库中发现了可复用的 Gadget Chain,并将其整理进 ysoserial 等工具。
ysoserial 接收:
Gadget Chain 名称
+
希望最终执行的命令
然后生成:
经过特殊组织的 Java 序列化对象图
它不是在本地执行目标命令,也不是单纯把命令字符串直接放入 Cookie。它会创建 PriorityQueue、Comparator、Transformer 和 TemplatesImpl 等对象,设置对象字段和引用关系,再将整个对象图输出为二进制 Java 序列化流。
本次实验使用:
CommonsCollections4
+
rm /home/carlos/morale.txt
其目标不是研究文件删除漏洞,而是用文件删除结果验证服务器已经完成远程命令执行。
实验流程可以概括为:
识别 Java 序列化 Session
→ 根据题目确认存在 Apache Commons Collections
→ 使用 ysoserial 构造 CommonsCollections4 对象图
→ 将命令嵌入 TemplatesImpl 字节码
→ 输出二进制序列化数据
→ Base64 编码
→ URL 编码
→ 替换 Session Cookie
→ 服务器反序列化
→ Gadget Chain 自动触发
→ 执行删除文件命令
在 Windows 环境中,先生成二进制文件:
cmd.exe /d /c "java –add-opens=java.xml/com.sun.org.apache.xalan.internal.xsltc.trax=ALL-UNNAMED –add-opens=java.xml/com.sun.org.apache.xalan.internal.xsltc.runtime=ALL-UNNAMED –add-opens=java.base/java.net=ALL-UNNAMED –add-opens=java.base/java.util=ALL-UNNAMED -jar ysoserial-all.jar CommonsCollections4 ""rm /home/carlos/morale.txt"" > payload.bin"
然后按原始字节进行 Base64 编码:
$payloadBase64 = [Convert]::ToBase64String(
[IO.File]::ReadAllBytes("$PWD\\payload.bin")
)
Java 16 及以上版本需要使用 –add-opens,是因为现代 Java 模块系统限制 ysoserial 访问 TemplatesImpl、TrAXFilter 等内部类。
这些参数只用于本地生成对象:
本地 Java 允许 ysoserial 访问内部类
→ ysoserial 成功生成 payload.bin
它们不会写入 Cookie,也不会发送给目标服务器。
生成的 payload.bin 是二进制对象流。Base64 和 URL 编码只是为了安全地通过 Cookie 传输:
Gadget Chain 对象图
→ Java 序列化二进制
→ Base64
→ URL 编码
→ Session Cookie
服务器则按相反顺序处理:
URL 解码
→ Base64 解码
→ ObjectInputStream.readObject()
→ 恢复恶意对象图
→ Gadget Chain 执行
因此,真正执行 Linux 命令的是远程 Lab 服务器,不是生成 payload 的 Windows 电脑。
七、真实环境中的识别思路
PortSwigger Lab 直接给出了 Apache Commons Collections 这一关键条件,目的是训练预构建 Gadget Chain 的使用。在真实授权测试中,通常需要自行识别反序列化入口和候选依赖。
识别 Java 序列化数据
黑盒测试中可以检查:
- Base64 数据是否以 rO0 或 rO0AB 开头;
- 解码后是否以 AC ED 00 05 开头;
- 是否出现 application/x-java-serialized-object;
- 修改数据后是否出现序列化相关异常。
需要注意:
JSESSIONID=随机字符串
通常只是服务端 Session 标识,不能据此判断其中存在 Java 序列化对象。
确认服务器是否真的反序列化
识别序列化格式后,还要证明数据确实进入服务器的反序列化入口。
有源码时,应跟踪客户端数据是否进入:
ObjectInputStream.readObject()
还要检查 readUnshared()、框架封装方法和消息中间件中的对象恢复操作。
在明确授权的黑盒测试中,可以使用低影响的 OAST 检测链,例如 ysoserial 的 URLDNS。它不以命令执行为目的,而是在对象反序列化时触发 DNS 查询:
服务器反序列化对象
→ URL 对象触发 DNS 解析
→ Burp Collaborator 收到唯一域名的查询
收到对应 DNS 交互,可以证明反序列化过程确实发生。
但没有收到请求不能证明不存在漏洞,因为服务器可能禁止外部 DNS、出站网络受限,或者对象被过滤器拦截。
识别依赖库和版本
有源码或部署权限时,优先检查:
pom.xml
build.gradle
Maven/Gradle 依赖树
SBOM
WEB-INF/lib
BOOT-INF/lib
容器镜像和部署包中的 JAR
如果发现:
commons-collections4-4.0.jar
可以把 CommonsCollections4 视为候选链。
黑盒环境中可以参考:
- 错误页面中的包名;
- Java 堆栈;
- ClassNotFoundException 暴露的类名;
- 公开源码和构建文件;
- 泄露的 JAR、依赖清单或版本信息;
- 授权环境中的可控验证结果。
但仅凭 rO0AB 无法确定服务器安装了哪个第三方库。
判断链条是否成立
最终要验证三个必要条件:
客户端可控数据进入 readObject()
+
目标 Classpath 存在兼容的 Gadget 类
+
过滤器和运行环境没有阻断调用链
=
Gadget Chain 具备可利用条件
如果只发现序列化数据,只能证明存在值得继续调查的输入。
如果只发现 Apache Commons Collections,只能证明存在候选 Gadget。
只有当完整调用链能够从反序列化入口到达 Sink,才能证明相应影响成立。
八、防御与总结
最根本的防御方式,是避免对不可信数据使用 Java 原生反序列化。客户端 Session 不应直接携带可以恢复为任意对象图的数据,可以改用:
- 服务端保存 Session 状态;
- 结构简单的数据格式;
- 明确字段和类型的 DTO;
- 严格验证的 JSON 等格式。
如果必须使用 Java 序列化,则应:
- 使用 ObjectInputFilter 建立严格的类型允许列表;
- 限制对象图深度、对象数量和数据大小;
- 对客户端数据进行完整性保护;
- 检查所有反序列化入口;
- 移除不必要的依赖库;
- 更新存在已知 Gadget Chain 的依赖;
- 以低权限账户运行应用;
- 限制应用的文件、网络和进程执行权限;
- 避免向外部用户暴露详细反序列化异常和堆栈。
需要注意,删除某个 Gadget 或升级某个依赖只能阻断特定利用链,不能修复“不可信数据被反序列化”这一根本问题。
对 Java Gadget Chain 的分析最终可以归纳为:
Classpath 决定代码是否存在
ObjectInputFilter 决定对象能否进入
Kick-off Gadget 决定调用链如何启动
中间 Gadget 决定数据如何传递
Sink 决定最终产生什么影响
本次 Lab 对应的完整模型是:
Source:客户端可控的 Session Cookie
反序列化入口:
ObjectInputStream.readObject()
Kick-off Gadget:
PriorityQueue.readObject()
中间 Gadget:
TransformingComparator
ChainedTransformer
ConstantTransformer
InstantiateTransformer
TrAXFilter
危险载体:
TemplatesImpl 中的类字节码
Sink:
Runtime.exec()
Final Effect:
执行 rm /home/carlos/morale.txt
因此,这个实验真正需要记住的不是 ysoserial 的命令,而是:Java 反序列化允许攻击者控制对象图;只要服务器拥有兼容的现有代码,并且没有严格限制可恢复的对象类型,攻击者就可能利用自动触发方法作为入口,将多个正常组件连接成通往危险 Sink 的 Gadget Chain。





