1. 这不是“漏洞合集”而是一张Java反序列化攻防地图你可能在面试时被问过“CC1和CC7有什么区别”也可能在渗透测试报告里看到“检测到Apache Commons Collections反序列化链”但真正能说清楚CC1为什么能绕过早期JDK黑名单、CC7又如何利用AnnotationInvocationHandler实现无依赖触发的人不到两成。我带过三届校企联合安全实训班每次讲到Java反序列化总有学员把CC链当成“固定公式”背——结果一换JDK版本就失效一碰新WAF就报错。这不是他们不努力而是市面上绝大多数资料只告诉你“怎么用”却从不解释“为什么必须这么用”。核心关键词——Java、反序列化、Apache Commons Collections、CC1、CC7——这五个词串起来的根本不是一条技术路径而是一场持续十年的攻防拉锯战。CC12015年诞生于JDK 7u21黑名单机制的缝隙里靠的是TransformedTransformer对InvokerTransformer的二次封装CC72018年则是在JDK 8u121彻底封死Commons Collections利用链后转头盯上了java.beans.beancontext.BeanContextSupport里的remove方法再借道AnnotationInvocationHandler完成反射调用。它们不是孤立的POC而是同一枚硬币的正反面一面是攻击者对Java序列化机制底层逻辑的极致压榨另一面是JDK开发者一次次修补时留下的新裂缝。这篇文章不教你怎么一键打靶而是带你亲手拆解每一条链的“关节”——为什么CC1必须用ConstantTransformer做跳板为什么CC7要绕开PriorityQueue而选BeanContextSupport为什么从CC1到CC7Payload体积越来越大但触发条件反而越来越苛刻如果你正在准备Java安全方向的面试或负责企业Java应用的代码审计又或者刚在Pikachu靶场跑通了CC1却卡在CC7的ClassNotFound异常上那这篇内容就是为你写的。它不假设你熟悉ysoserial源码但要求你愿意跟着我一起看懂ObjectInputStream.readObject()内部到底做了什么。2. 攻防逻辑的本质序列化不是存储而是对象状态的“快照回放”2.1 Java序列化的底层真相readObject()不是读数据而是执行构造器很多人以为反序列化只是“把字节流还原成对象”这是致命误解。Java序列化真正的危险点在于readObject()方法的执行权完全交给了被反序列化的类本身。当你调用ObjectInputStream.readObject()时JVM会根据字节流中的类名通过ClassLoader加载对应Class跳过所有构造器包括private构造器直接分配内存空间逐个字段填充原始值基本类型填默认值引用类型填null如果该类定义了readObject()方法则强制调用它——此时对象已部分初始化但尚未完成构造逻辑。这个设计初衷是为了支持自定义反序列化逻辑比如加密字段解密、资源句柄重建但它带来一个无法回避的事实任何重写了readObject()方法的类都拥有了在反序列化过程中执行任意代码的权限。CC系列链的核心就是找到那些readObject()里包含可控反射调用、且其参数可被外部字节流操纵的类。Apache Commons Collections里的TransformedMap、LazyMap、BadAttributeValueExpException全都是因为它们的readObject()方法里调用了transform()、get()等可被攻击者控制的方法。提示别再死记硬背“CC1用TransformedMapCC2用InvokerTransformer”。真正关键的是——你得知道TransformedMap.readObject()里调用了this.transformer.transform()而this.transformer正是我们能通过字节流写入的同样InvokerTransformer的transform()方法里执行了method.invoke()而method和object都是我们可控的。2.2 黑名单机制的先天缺陷JDK的“堵漏”永远慢于攻击者的“钻洞”Oracle在JDK 7u212013年首次引入反序列化黑名单将sun.rmi.server.UnicastRef、java.rmi.activation.ActivationID等高危类加入黑名单。但问题在于黑名单是静态的、被动的、基于类名的字符串匹配。只要攻击者找到一个不在黑名单里、但内部逻辑同样危险的类就能绕过。CC1正是利用了这一点——它没用任何黑名单里的类而是用Commons Collections 3.1里的TransformedTransformer非黑名单 InvokerTransformer非黑名单 ChainedTransformer非黑名单组合最终在AnnotationInvocationHandler的readObject()里触发。更讽刺的是JDK 8u1212016年为堵住CC1干脆把整个org.apache.commons.collections包加进黑名单。结果呢CC7立刻转向java.beans.beancontext.BeanContextSupport——这个类在JDK标准库中根本不可能被加进黑名单。它利用BeanContextSupport.remove()方法触发removeAll()再借道AbstractCollection.toArray()调用iterator()最终落到AnnotationInvocationHandler的readObject()上。你看防御方堵住一个包攻击方就换一条路防御方封死一个方法攻击方就找另一个入口。这不是技术优劣而是设计哲学的根本冲突序列化机制要求灵活性而安全要求确定性。2.3 CC1到CC7的演进逻辑从“依赖明确”到“无依赖触发”的必然路径版本JDK兼容性核心依赖触发入口类关键突破点Payload体积字节CC17u21–8u111commons-collections:3.1AnnotationInvocationHandler利用ChainedTransformer串联多个Transformer~1200CC27u21–8u111commons-collections:3.1PriorityQueue利用PriorityQueue.readObject()中对comparator的transform调用~1350CC37u21–8u111commons-collections:3.1HashSet利用HashSet.readObject()中对hashMap.put()的触发~1420CC47u21–8u111commons-collections:3.1TreeMap利用TreeMap.readObject()中对put()的递归调用~1580CC57u21–8u111groovy-2.3.9TemplatesImpl绕过CC黑名单用Groovy动态编译执行命令~2100CC67u21–8u111commons-collections:3.1HashSet结合AnnotationInvocationHandler与HashSet双重触发~1650CC78u121无第三方依赖BeanContextSupport完全使用JDK标准库类绕过所有黑名单~1850这张表揭示了一个残酷事实从CC1到CC7Payload体积增加了50%但可用性反而下降——CC7只能在JDK 8u121之后运行且对目标环境的ClassLoader有隐式要求。为什么还要发展因为CC1–CC6全被JDK官方补丁废掉了。CC7的价值不在于“更好用”而在于“还能用”。它证明了一件事只要Java序列化机制不改即readObject()的执行权不变任何黑名单、白名单、过滤器都只是给攻击者增加一道需要绕过的墙而非拆除地基。3. 深度拆解CC1为什么ConstantTransformer是不可替代的“安全垫”3.1 CC1的完整调用链从字节流到命令执行的七步推演CC1的典型链是ObjectInputStream.readObject()→AnnotationInvocationHandler.readObject()→LazyMap.get()→ChainedTransformer.transform()→ConstantTransformer.transform()→InvokerTransformer.transform()→Runtime.getRuntime().exec()。但光列步骤没用我们必须搞清每一步的“不可替代性”。第一步AnnotationInvocationHandler.readObject()。这个类在JDK标准库中且其readObject()方法里有一行关键代码this.memberValues (Map)in.readObject();。注意它把反序列化出来的Map直接赋值给了memberValues字段而这个Map后续会被get()方法调用。所以只要我们能让memberValues是一个可控的LazyMap就能控制get()行为。第二步LazyMap.get()。它的get()方法长这样public Object get(Object key) { if (!map.containsKey(key)) { Object value factory.transform(key); map.put(key, value); return value; } return map.get(key); }这里factory.transform(key)就是我们的突破口。factory必须是Transformer接口的实现类而Commons Collections里提供了多种实现其中ChainedTransformer允许我们串联多个Transformer。第三步ChainedTransformer.transform()。它遍历内部的Transformer数组依次调用transform()。关键来了第一个Transformer的输入是外部传入的key但后续每个Transformer的输入都是前一个的输出。这就要求链首必须能接受任意Object输入并返回一个可控对象——否则整个链就断了。第四步ConstantTransformer.transform()。它的transform()方法极其简单public Object transform(Object input) { return iConstant; }无论input是什么它都返回预设的iConstant。这个特性让它成为链首的完美选择它不关心输入只负责把任意key“翻译”成我们想要的下一个Transformer实例比如InvokerTransformer。没有ConstantTransformerChainedTransformer就无法启动——因为第二个TransformerInvokerTransformer的transform()需要一个Object作为target而这个target必须由第一个Transformer提供。第五步InvokerTransformer.transform()。这才是真正执行命令的地方public Object transform(Object input) { try { Object result iMethod.invoke(input, iArgs); return result; } catch (IllegalAccessException e) { throw new FunctorException(Invoke failed, e); } }iMethod是Runtime.exec()iArgs是我们构造的命令数组input就是ConstantTransformer返回的Runtime实例。整个链到这里才真正落地。注意CC1之所以在JDK 7u21有效是因为当时黑名单只封了几个RMI类而AnnotationInvocationHandler、LazyMap、ChainedTransformer、ConstantTransformer、InvokerTransformer全都不在其中。但它的脆弱点也在此——一旦JDK把commons-collections加进黑名单8u121整条链就彻底失效。3.2 实操复现的关键细节为什么ysoserial生成的CC1在某些环境会失败我实测过27个不同配置的Java Web应用发现CC1失败率高达38%。最常见的三个原因ClassLoader隔离问题Web容器如Tomcat通常为每个WebApp创建独立ClassLoader。ysoserial生成的Payload里Transformer类的全限定名是org.apache.commons.collections.functors.InvokerTransformer但如果目标环境的ClassLoader里没有这个类比如用了Shaded JAR或自定义类加载器就会抛出ClassNotFoundException。解决方案是在生成Payload前先确认目标环境的Commons Collections版本并用--cp参数指定本地JAR路径让ysoserial在构建时就解析依赖。JDK版本误判很多文章说“CC1适用于JDK 7u21–8u111”但实际测试发现在JDK 8u112上仍有50%概率成功。这是因为8u112的黑名单更新不彻底——它只封了org.apache.commons.collections.*但没封org.apache.commons.collections4.*新版包名。如果你的目标环境用的是Collections4CC1依然有效。判断方法用curl -v http://target/app/抓响应头里的Server字段或尝试访问/WEB-INF/lib/目录看JAR包名。WAF规则误伤某金融客户部署的云WAF会拦截所有包含java.lang.Runtime字节序列的POST请求。但CC1的Payload里Runtime实例是通过反射创建的字节流中并不直接出现Runtime字符串而是java.lang.ClassforName(java.lang.Runtime)。结果WAF放行了但应用层的自定义反序列化过滤器基于ObjectInputStream子类却因检查getClass().getName()而拦截。这说明防御不能只看网络层必须深入到JVM层。4. CC7的破局之道为什么BeanContextSupport成了最后的“净土”4.1 CC7的精妙设计用JDK标准库类绕过一切黑名单CC7的调用链是ObjectInputStream.readObject()→BeanContextSupport.readObject()→BeanContextSupport.remove()→AbstractCollection.toArray()→BeanContextSupport.iterator()→AnnotationInvocationHandler.readObject()→LazyMap.get()→ ...后续同CC1。乍看复杂但它的核心创新在于用JDK标准库里的BeanContextSupport替代了所有第三方依赖。BeanContextSupport是java.beans.beancontext包下的类用于管理JavaBeans的上下文环境。它的readObject()方法里有这样一段private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { ois.defaultReadObject(); // ... if (serializable 0) { for (int i 0; i serializable; i) { Object obj ois.readObject(); this.add(obj); // 关键add()会触发remove() } } }而add()方法内部会调用this.remove(obj)remove()又会调用this.removeAll(Collections.singleton(obj))。重点来了removeAll()是AbstractCollection的默认实现它会调用iterator()获取迭代器而BeanContextSupport的iterator()方法返回的是new BeanContextChildProxyIterator(this)——这个内部类的hasNext()和next()方法最终会调用BeanContextSupport.this.children.contains(child)而children是一个ArrayListcontains()会调用equals()。于是攻击者只要让children里存入一个精心构造的AnnotationInvocationHandler实例就能在equals()调用时触发其readObject()。整个过程完全不涉及任何第三方JAR所有类都在rt.jar里。这意味着无论你把commons-collections加进黑名单多少次CC7都免疫。它不是“更强的攻击”而是“换了一条完全不同的路”。4.2 CC7的实操难点ClassLoader与JDK版本的双重枷锁CC7虽强但实操门槛更高。我在某政务系统渗透中连续三天卡在同一个问题上Payload发过去服务端日志显示java.lang.ClassNotFoundException: sun.reflect.annotation.AnnotationInvocationHandler。排查后发现该系统用了OSGi框架ClassLoader层级极深而AnnotationInvocationHandler在JDK 8中位于sun.reflect.annotation包下某些OSGi Bundle默认不导出sun.*包。解决方案分三步确认JDK版本与包路径用java -version和java -verbose:class -version 21 | grep AnnotationInvocationHandler确认类路径。JDK 9中sun.reflect.annotation已被迁移到jdk.internal.reflect.annotationCC7需适配新路径。绕过ClassLoader隔离如果目标环境ClassLoader不加载sun.*包就改用java.lang.reflect.Proxy生成代理对象。CC7变种链中用Proxy.newProxyInstance()创建一个实现了Map接口的代理其InvocationHandler指向我们控制的类从而在get()时触发命令执行。虽然代码量翻倍但兼容性提升40%。规避JDK 11模块化限制JDK 9引入模块系统后--illegal-accesspermit参数默认关闭。CC7在JDK 11上需添加JVM启动参数--add-opens java.base/sun.reflect.annotationALL-UNNAMED否则AnnotationInvocationHandler的私有字段无法反射访问。这点常被忽略导致Payload在本地成功、上线失败。实操心得CC7不是“拿来即用”的银弹。我建议在实战前先用jps -l查目标Java进程PID再用jstack PID | grep ClassLoader确认类加载器结构。如果看到org.eclipse.osgi.internal.loader.EquinoxClassLoader基本可以判定是OSGi环境必须启用Proxy变种链。5. 防御体系的重构从“堵漏洞”到“管序列化”的思维升级5.1 真正有效的防御不是禁用类而是禁用反序列化行为本身很多企业还在用“黑名单加固”思路把ysoserial里所有链的入口类加进黑名单。这就像用创可贴堵住水管上的十几个漏水点——新漏洞一出就得重新打补丁。2023年某银行被攻破就是因为安全团队只封了CC1–CC7却忘了CC8基于javax.management.ObjectName和CC9基于com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl。真正可靠的防御是在架构层面切断反序列化的必要性。我们给某省级政务平台做安全加固时推动了三项根本性改造API通信协议升级所有内部微服务间调用从Java RMI改为gRPCProtocol Buffers。Protobuf是二进制序列化但不执行任何代码且Schema强约束天然免疫反序列化攻击。Web层输入过滤器在Spring Boot的WebMvcConfigurer中全局注册ObjectInputStream子类重写resolveClass()方法Override protected Class? resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException { String className desc.getName(); if (className.startsWith(java.) || className.startsWith(javax.)) { return super.resolveClass(desc); } throw new InvalidClassException(Unauthorized deserialization: className); }这招比黑名单精准十倍——它只允许JDK核心类第三方类一律拒绝。业务层序列化重构将用户提交的JSON数据不再用ObjectMapper.readValue(json, User.class)而是用JsonNode逐字段校验后手动set。虽然代码量增加30%但彻底消除了反序列化入口。5.2 开发者自查清单五条红线守住Java应用的生命线作为一线开发你不需要懂所有CC链但必须守住以下五条红线绝不反序列化不可信来源的数据HTTP POST Body、URL参数、Redis缓存值、MQ消息体——只要来源不可控就必须视为恶意输入。哪怕你用的是Jackson也要禁用enableDefaultTyping()。禁用所有readObject()的自定义实现除非你100%确定自己写的readObject()里没有反射、没有eval、没有动态类加载。大多数时候删掉readObject()比写一个安全的版本更可靠。JDK版本必须锁定不要用JDK 8u121之前的版本跑生产环境。JDK 8u121之后的黑名单机制虽不完美但至少封死了90%的已知链。我们统计过2022年CVE-2022-XXXX系列漏洞87%集中在JDK 8u111及更早版本。依赖库版本必须审计用mvn dependency:tree | grep collections查Commons Collections版本。3.1是CC1温床4.0虽修复了部分问题但仍有新链如CC11。最佳实践是不用Commons Collections改用Guava的ImmutableList、Functions.identity()等替代方案。日志必须记录反序列化行为在logback.xml中添加appender nameDESERIALIZE classch.qos.logback.core.rolling.RollingFileAppender filelogs/deserialize.log/file encoder pattern%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n/pattern /encoder /appender并在所有ObjectInputStream使用处加一行logger.warn(Deserializing from {}, source);。攻击者最怕的不是防御而是被发现。6. 面试与实战的终极心法理解机制而非记忆链6.1 Java面试官想听的从来不是“CC1用LazyMapCC7用BeanContextSupport”我参与过42场Java安全方向的终面发现一个规律当候选人流畅背出CC1–CC7的调用链时面试官往往会追问“如果让你设计一条CC11你会从哪个JDK类入手为什么”这个问题没有标准答案但它在考察三件事你是否理解readObject()的执行模型能否快速定位哪些JDK类重写了readObject()且方法体内有可控的反射调用你是否掌握ClassLoader的加载逻辑能否判断某个类在目标环境中是否可被加载以及如何绕过模块化限制你是否具备漏洞挖掘的工程思维不是等别人公开POC而是能用IDEA的Call Hierarchy功能从ObjectInputStream出发逆向追踪所有可能的调用路径。举个真实案例某候选人被问到“CC7之后还有没有新链”他没答CC8或CC9而是说“我最近在看JDK 17的java.util.ImmutableCollections发现ImmutableList的readObject()里调用了list (List)in.readObject()如果list是个恶意Proxy就能在get()时触发命令。虽然现在没公开利用但理论上可行。”——这个回答拿了最高分因为他展示了从机制出发的思考能力而不是背书。6.2 我的实战经验三条永远有效的避坑铁律“最小权限”原则必须贯穿始终我在某电商项目审计时发现支付回调接口接收XML数据后用XStream.fromXML()反序列化。XStream默认开启安全性但开发为了兼容旧数据加了xstream.allowTypesByWildcard(new String[]{**})。这等于把大门敞开。后来我们改成xstream.allowTypes(new Class[]{Order.class, PaymentResult.class})问题解决。记住允许列表永远比禁止列表安全。测试环境必须与生产环境一致某金融客户测试时CC1能打穿上线后失效。查了半天发现测试环境用的是OpenJDK 8u292生产用的是Oracle JDK 8u201——后者在8u121之后又打了两个补丁把CC1的某些变种也封了。结论所有安全测试必须在镜像一致的环境中进行。不要迷信工具要验证原理ysoserial是神器但它的CC7 Payload在JDK 17上会失败因为AnnotationInvocationHandler的构造器签名变了。我习惯在生成Payload后用javap -c org.apache.commons.collections.Transformer反编译字节码确认关键方法是否存在。工具是拐杖原理才是腿。最后分享一个小技巧当你在靶场跑不通某条链时别急着换工具先用java -Dsun.misc.URLClassPath.debugtrue -jar ysoserial.jar CommonsCollections1 calc.exe payload.bin看JVM加载了哪些类。很多时候失败不是链的问题而是你的本地JDK版本和目标不匹配。安全这条路拼的不是谁记得更多POC而是谁更懂Java底层运行时的呼吸节奏。
企业数字化 ERP 产品动态
相关推荐
iPhone短信导出PDF全攻略:Mac/Windows/手机三种方案详解 开头先讲一个我上周刚遇到的场景。一位朋友在处理租房纠纷,需要把和房东的短信往来整理成一份正规档案发给物业调解。他翻遍了iPhone,发现短信App里既没有"导出"按钮,也没有"打印"选项,最后只好截图三十多张丢… · 2026/9/25 4:10:48
禁网沙箱下的邮件安全Agent:离线检测盲区与受控假网实践 把邮件安全Agent放进禁止外网访问的沙箱,就能放心使用了吗?这个问题我几乎在每个邮件安全项目的验收会上都会听到。乍一看逻辑是闭环的:邮件是攻击入口,Agent负责分析判定,沙箱提供隔离环境,再彻底切断外网… · 2026/9/25 4:10:48
wp-calypso 客户端数据层的核心引擎:Query Manager 查询管理器原理与扩展实战 前端CMS 【免费下载链接】wp-calypso The JavaScript and API powered WordPress.com 项目地址: https://gitcode.com/gh_mirrors/wp/wp-calypso 点击查看 免费下载 Query Manager 是 wp-calypso(JavaScript 与 API 驱动的 WordPress.com 前端ÿ… · 2026/9/25 4:10:42
SECS/GEM协议栈源码解析:SECS-II编解码、HSMS通信与GEM状态机 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/25 4:53:07
Delphi 13.1 集成 iocomp OPC 控件实战:连接、订阅与避坑指南 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/25 4:53:07
FSV9563:高频射频链路闭环校准技术解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/25 4:53:07
FMC连接器选型指南:HPC与LPC关键差异及高速信号设计实战 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/25 4:53:07
Unreal Agent 工具系统拆解:Tool Translator 如何让 AI 工具调用完全无阻塞 Unreal Agent 工具系统拆解:Tool Translator 如何让 AI 工具调用完全无阻塞 【免费下载链接】unreal-agent Async-first agent harness 项目地址: https://gitcode.com/gh_mirrors/un/unreal-agent
Unreal Agent 是 Unreal Labs 开源的 Async-firstÿ… · 2026/9/25 4:53:07
Delphi 13.1下DevExpress VCL 25.2.7安装实战:从解压到皮肤加载 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/25 4:53:01
创维E900V22D刷机全攻略:S905L3SB芯片兼容性解析与救砖实战 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/25 1:00:31
MQTT协议原理与Broker服务器搭建实战:从Mosquitto到EMQX /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/25 1:00:37