Java安全漫谈-URLDNS篇
环境准备
这是反序列化系列的第一篇文章,所以先简单介绍下环境搭建以及如何调试
ysoserial
ysoserial是一款用于生成反序列化数据的工具。攻击者可以选择利用链和输入自定义命令,然后通过该工具生成对应的反序列化利用数据,然后将生成的数据发送给存在漏洞的目标,从而执行命令。
- 安装多个不同版本 JDK,因为有些利用链有版本限制,这里是
8u291
和8u66
- 下载 ysoserial 源码到本地,该工具包含了所需要的依赖库,只需要 Maven 导入即可;
- 如果依赖有问题,可以手工点击菜单里的
Files - Project Structure
配置Libraries
- 如果依赖有问题,可以手工点击菜单里的
- 将
src/test/java/
目录下的文件全部删除,后续在这个目录下编写的测试类;最后将 ysoserial-all.jar 下载到本地,便于生成序列化数据进行测试。
Gadget
Gadget 就是利用链,也叫 Gadget Chains,它连接的是从触发位置开始到执行命令的位置结束。
ysoserial 中存在着很多条 Gadget,而使用 Gadget 时都会调用其中的getObject()
方法,该方法会返回一个对象,最后将该对象进行序列化;如果目标存在反序列化漏洞,并满足这个 Gadget 对应的条件。那么在将这个 Gadget 所对应生成的序列化对象发送给目标时,目标会对其进行反序列化,最终导致序列化对象中的恶意命令被执行。
运行 ysoserial-all.jar 工具简单生成了一条 URLDNS 的 POC:
1 | $ java -jar ysoserial-all.jar URLDNS "http://xxx.dnslog.cn" > dnslog.ser |
参数配置
在pom.xml
文件中找到入口文件,也就是GeneratePayload.java
1 | <manifest> |
在GeneratePayload.java
的main
函数处添加断点,然后右上角修改配置,添加所需参数,最后点击调试
利用链分析
利用链:
1 | Gadget Chain: |
获取类对象
这部分会先获取所调用的 Gadget 的类对象,这里是 URLDNS 的类对象,后续其他 Gadget 也是同理
首先 ysoserial 接收到参数并分别赋值,接着跟进Utils.getPayloadClass("URLDNS")
方法
1 | final String payloadType = args[0]; // URLDNS |
在getPayloadClass()
方法中,通过反射获取到传入的URLDNS
的 Class 对象并返回
- 这里一开始的
Class.forName("URLDNS")
并没有找到对应的类对象,所以clazz == null
- 进入
if
语句后,通过拼接完整类名ysoserial.payloads.URLDNS
后才获取到类对象
构造恶意序列化对象
这条 URLDNS 链的重点就是如何构造一个恶意的 HashMap 序列化对象并返回
URLDNS
的 Class 对象返回后将赋值给payloadClass
,然后执行newInstance()
方法创建实例,并调用该实例的getObject()
方法,并将 DNSLog 平台地址作为参数传入
跟进payload.getObject()
看看:
- 创建
HashMap
对象和URL
对象 - 调用
HashMap
对象的put
方法,键为 URL 对象,值为传入的参数,即 DNSLog 平台地址 - 最后通过反射设置
URL
对象的hashCode
设为-1
为什么要通过反射将 URL 对象的
hashCode
的值为-1
?原因已经在URLDNS
的注释中:
During the put above, the URL’s hashCode is calculated and cached. This resets that so the next time hashCode is called a DNS lookup will be triggered.
设置这个 URL 对象的hashCode
为初始值-1
,这样反序列化时将会重新计算其hashCode
,才能触发到后⾯的 DNS 请求,否则不会调⽤URL#hashCode()
最后将HashMap
对象进行序列化并返回
1 | final Object object = payload.getObject(command); |
反序列化触发
触发反序列化的⽅法是
readObject()
,因为 Java 开发者经常会在某些类中重写该方法,导致可以构造利⽤链。
在 URLDNS 利用链中,ysoserial 先获取到了 URLDNS 的类对象,然后调用了该类对象newInstance()
方法创建实例,然后调用该实例的getObject()
方法获取到一个序列化后的HashMap
对象。
因此在将这段序列化数据发送给目标,目标对其进行反序列化时,会触发HashMap
对象的readObject()
方法。所以可以直接看HashMap
类的readObject()
方法:
1 | private void readObject(java.io.ObjectInputStream s) |
在HashMap
类中的第41
行putVal(hash(key), key, value, false, false);
处打下断点
在没有分析过的情况下,为何关注 hash 函数?
因为 ysoserial 的注释中很明确地说明了“During the put above, the URL’s hashCode is calculated and cached. This resets that so the next time hashCode is called a DNS lookup will be triggered.”,是指 hashCode 的计算操作触发了DNS请求。
创建一个简单的TestDNS.java
文件并调试,程序将从ois.readObject()
进入到HashMap
类的断点
1 | package deserialization; |
接着调用了HashMap#putVal()
方法,该方法中调用了另一个方法hash(key)
,跟进发现HashMap#hash()
方法调用了key.hashCode()
,即计算key
的 Hash 值;
跟进跳转到URL#hashCode()
方法,这里当hashCode!=-1
时会直接返回。而如果hashCode==-1
,就会执行下面的handler.hashCode()
;
1 | public synchronized int hashCode() { |
再次跟进,跳转到URLStreamHandler#hashCode()
,该方法中调用了URLStreamHandler#getHostAddress()
方法;
依次跟进到URL#getHostAddress()
方法,这里调用了InetAddress.getByName(host)
,该方法会根据主机名获取 IP 地址,即进行一次 DNS 查询,触发 DNSLog 记录
完整调用栈如下:
1 | HashMap#readObject() |
总的来说,这条链的触发原因是HashMap
重写了readObject()
方法,该方法中会触发putVal(hash(key), key, value, false, false);
;跟进hash(key)
发现存在key.hashCode()
,即计算key
的哈希值;
因为前面把URL
对象当成key
存进了HashMap
,所以这里会触发URL
对象的hashCode()
方法;在URL#hashCode()
中,当hashCode==-1
时会触发URLStreamHandler#hashCode()
,进行到后⾯的 getHostAddress()
方法查询主机地址时,会触发 DNS 请求。
利用链构造
根据前面的调用栈:
- 首先需要一个
HashMap
和URL
实例 - 设置
URL
实例的hashCode
值为-1
- 将
hashMap
对象进行序列化 - 反序列化触发漏洞
1 | package deserialization; |