Java反序列化系列漏洞00篇-URLDNS链详解
在了解反序列化链之前,应先了解Java的序列化、反序列化和Java的反射机制
什么是 URLDNS
URLDNS
是 ysoserial提供的一个反序列化利用链,目的是无回显探测目标系统是否存在 Java 反序列化漏洞,是反序列化链中最简单的一条链,可以作为学习反序列化链的开始。
特点
- 目标无回显,可控的参数只是个url,因此只能发起DNS请求探测是否存在反序列化漏洞。
- 不限制 jdk 版本,使用 Java 内置类且不依赖第三方库。
总体流程理解
这里先提前说一下大体利用流程,方便后续理解。
漏洞点:URLDNS的本质就是Java的反序列化漏洞,因此只要是能够反序列化用户传入的数据的功能点(接口、RMI、JNDI)或者可以上传.ser
文件和带有序列化对象的文件的功能点;服务端解析后就可能触发反序列化漏洞。
使用ysoserial的URELDNS链生成poyload:
1 | #直接使用 |
生成后就可以在功能点测试,看看是否能收到DNS请求
也可以使用如下POC测试
POC
1 | import java.io.FileInputStream; |
ysoserial URLDNS payload代码解读
看这条链之前,先解读一下ysoserial URLDNS 给出的payload源码
源码链接
1 | package ysoserial.payloads; |
代码总共分两部分,一步步解读
第一部分
1 | public Object getObject(final String url) throws Exception { |
- 构造了一个
HashMap
,以URL
作为 key。 HashMap
在反序列化的时候会执行put()
,put()
会调用key.hashCode()
。- 通过反射构造的
URL
对象的hashCode()
被重置为-1
,以便它会重新计算hashCode()
;重新计算hashCode()
时,会调用handler.hashCode(URL)
,触发 DNS 查询InetAddress.getByName()
),详情后续会讲。
hashmap介绍
HashMap
是 Java 中非常常用的一个数据结构,本质上是基于哈希表(Hash Table)实现的键值对(key-value)集合。
HashMap
是一个 存储“键值对”映射关系 的集合类。
你可以通过一个 键(Key) 快速查找对应的 值(Value)。
就像你有一个“字典”:
1 | Key(单词): "apple" |
用 HashMap
存起来,就是:
1 | HashMap<String, String> map = new HashMap<>(); |
第二部分
1 |
|
SilentURLStreamHandler为第一部分重写了的URLStreamHandler,重写了openConnection,getHostAddress的函数;
因为hashmap的put方法也会调用它们进行一次DNS解析请求,重写来覆盖掉原函数,避免在构造poyload的时候发起DNS请求,影响最后的判断。
URLDNS链
核心调用流程
下方为URLDNS 反序列化链 核心调用流程:
1 | HashMap.readObject() |
正常分析利用链时,应该从尾部找,一步一步找到头部,这条链比较好理解并且已经分析过yso的Payload,因此我直接正向分析这条链了。
接下来用上方提到的poc详细看该反序列化链的调用流程
HashMap.readObject()
先ctrl+alt +b跳转到hashmap的实现代码
我们利用payload或者Poc生成序列化文件并把它传入存在反序列化漏洞的服务器上时,服务器就会对它进行序列化,当该类被反序列化时就会调用readObject()方法
readObject()介绍(有基础不需要看)
readObject()方法可以理解为PHP中的魔术方法,在PHP中的__wakeup()
方法在 unserialize()
执行时会自动调用;在Java中,readObject()
方法在反序列化(ObjectInputStream.readObject()
)时会自动执行
跟进hashmap()下的readObject方法
HashMap.hash(Object key)
往下滑,可以看到putVal()调用了hash(),并把键值key传入了,key也就是前方传入的dnslog的url
putVal()就是put()的底层实现,put()就是一个外部接口
key.hashCode()
继续跟进hash(),可以看到又调用了key.hashcode()
方法
当点击转到实现方法时,发现key是一个Object基类。
Object基类是所有类的父类,父类引用(Object key)调用方法时,根据实际对象类型调用子类实现。
因此需要跟进URL.hashcode()
重写和多态介绍(有基础不需要看)
HashMap 的 key 是 Object 类型引用,但真正调用 hashCode()、equals() 方法时,是根据 实际对象类型 走子类的实现。
1 | Object key = new URL("http://example.com"); |
URL.hashcode()
继续跟进URL.hashcode()
可以直接打个URL,alt+左键跳转到URL类,再找一下hashcode()
可以看到如果hashcode是非1的值,if的条件就会满足,直接return的值。该条件说明已经计算过hash值了,会直接返回缓存值。
hashCode == -1
时,代表还没有计算过哈希值,会调用handler.hashcode()进行第一次计算。
因此上方的payload中才会通过反射把hashcode的值设置成-1,否则就无法进行链子的下一个调用。
hashcode()介绍(有基础不需要看)
每个对象都有自己的 hashCode()
,这是一个返回 整数值 的方法。
这个返回值是对象的 哈希值,用来加速数据查找、比较等操作
作用
hashCode()
返回一个整数,用作哈希表中的“索引”。
比如 HashMap
,插入和查找 key 的时候:
- 先调用
key.hashCode()
算出哈希值。 - 然后根据这个哈希值决定放到哪一个“桶”里(
table
的索引)。 - 再在这个“桶”里通过
equals()
比较找到真正的 key。
原理
哈希表底层是一个数组,每个 key 根据哈希值决定放在哪个下标位置(index)。hashCode()
越分散,哈希表性能越好。
URLStreamHandler.hashCode()
跟进handler的hashCode(),发现handler
是URLStreamHandler
的实例,因此需要跟进URLStreamHandler的hashCode()
。
同时我们看到hashcode的默认值为-1,但是HashMap()的put()
时已经调用过一次hashCode()
函数,hashCode
的值会有缓存,因此需要上方的payload中才会通过反射把hashcode的值设置成-1,否则就无法进行链子的下一个调用。
并且payload也重写了URLStreamHandler的getHostAddress,openConnection方法,防止在第一次put是触发DNS请求,影响探测效果。
跟进URLStreamHandler的hashCode()
第一部分代码相当于取出协议部分
第二部分的InetAddress addr = getHostAddress(u);取出主机部分。尝试通过 getHostAddress(u)
获取 InetAddress
(相当于解析 URL
的 IP 地址)。
发现它又调用了getHostAddress(),继续跟进。
URLStreamHandler.getHostAddress
该方法又调用了InetAddress.getByName(),并传入主机名
InetAddress.getByName
该方法对传入的主机名(host
)解析出对应的 IP 地址(InetAddress
对象),从而发送了一次DNS请求。
总结
Hashmap是反序列化的入口点,getByName是最后的利用点,本质就是想办法把头和尾串联起来;传入的key和把hashcode的值通过反射改为-1就是这条链的关键点。把他们串起来之后加上dnslog进行序列化处理,传入可能存在的漏洞点就可以进行测试了。