前言
文章首发:https://www.sec-in.com/article/299
2015年Gabriel Lawrence(@gebl)和Chris Frohoff(@frohoff)在AppSecCali大会上提出的利用Apache Commons Collections来构造命令执行利用链,随后发布ysoserial工具,从此打开Java安全的蓝海。
利用链(gadget chains),俗称gadget。通俗来说就是一种利用方法,它是从触发位置开始到执行命令的位置结束,也可以说是漏洞验证方法(POC)。
使用方法,GitHub下载jar包或者git源码自己编译。
java -jar ysoseial.jar URLDNS “http://baidu.com"
再将生成号POC发送目标,如果目标存在反序列化漏洞,并且满足利用链条件,则命令将会被执行。
ps: 本文实验代码都上传JavaLearnVulnerability项目,为了让更多人知道,麻烦动动小手star一下。
URLDNS
URLDNS
是其中的一个gadget的名字,此gadget不能执行命令,通常用来验证目标是否存在反序列化漏洞。URLDNS gadget十分适合用来验证目标是否存在反序列化漏洞。
- 此gadget完全使用Java内置的类构造,无需第三方库支持。
- 如果目标没有回显,通过DNS解析请求是否存在来判断存在反序列化漏洞。
漏洞分析
打开https://github.com/frohoff/ysoserial/blob/master/src/main/java/ysoserial/payloads/URLDNS.java
查看源码,但笔者这里使用自己改写的源码来分析此gadget链。
1 | package samny.serializable; |
漏洞复现
漏洞分析之前,我们先看看漏洞执行效果。
分析一个在线dns解析记录网站DNSlog Platform,网站无需任何登录注册,国内访问速度也快,DNS记录获取速度没得说,子域名可以无限更换。
漏洞复现步骤
- 修改你能过记录dns解析的网址。
- 直接运行main方法,刷新记录。
断点分析
触发反序列化漏洞的方法是readObject
,所以笔者在43行代码处和hashmap
的readObject
各设置一个断点。
PS:
- 以免dns记录混淆,建议每次分析都换一个域名。
- 此处会有一个问题就是我们到底怎么在JDK包中找到HashMap这个类的readobject函数呢?因为JDK的类超级多,难道我们必须要一个个翻找?
- 其实搜索是可以搜索导入包的内容的,
Ctrl+Shift+F
在Scope - All Places
搜索class hashmap
即可。
断点设置好了,开始分析,笔者这里直接从HashMap.java的readObject
开始分析。
hashmap中readObject会调用putVal方法是往HashMap中放入键值对的方法,进而会计算hashcode值。
接着会判断key是否为空,hashCode是否不等于-1,不等于-1会直接返回,等于-1会重新计算。这时候我们看笔者写源码36行,修改方法hashCode的值为-1,这时你是否明白此时的用意。
计算hash
的时候会跳转到java.net.URLStreamHandler.java#hashCode
方法来计算hash。注意看图片中被框住的一行代码,hashCode在计算hash时,会调用getHostAddress()
方法,进而调用getByName(host)
方法。
执行方法,我们发现会有一个等待时间大概2秒钟之后(其实就是DNS解析所需要的时间),可以获取DNS解析记录。
小结
整个的URLDNS
的gadget其实清晰又简单。
- HashMap->readObject()
- HashMap->hash()
- URL->hashCode()
- URLStreamHandler->hashCode()
- URLStreamHandler->getHostAddress()
- InetAddress->getByName()
其实作者在博客中写道,可以用四行代码就可以实现此gadget chains,反观ysoserial中源码,去掉注释也多还有几行代码,多出的代码时干嘛的呢?
巧妙避免重复
多出几行代码,我们来分析一下。ysoserial的作者重写了URLStreamHandler其中两个方法。但是我们还没搞清楚其中的作用。
1 | URLStreamHandler handler = new URLStreamHandler() { |
老规矩,断点撸码。不过此时断点应该设置在哪?笔者按照源作者的代码,去掉序列化和反序列化的过程。剩下代码也就上面的和下面图片给出的5行代码,分析不难发现断点应该设置在hm.put()
。其中31和32行代码肯定是不会去设置断点的,至于36行在之前就说明其作用。
调试代码不能发现,put方法也和hashmap中的readObject方法的方法是差不多的,继续跟进。
继续跟进还是来到java.net.URLStreamHandler#hashCode
方法,但此时方法会跳转到笔者复写的方法中,返回应该null
,进而就不会去解析dns。
为什么hashmap中putval方法就不会跳转到我们复现的类方法里面返回一个null呢?(个人见解,源码无法修改所以无法调试)
答:反序列化时应该时将二进制代码直接读取,进去调用hashmap中readObject方法,此时反序列化完全是使用jdk源码调用,不会再去看我们用户复写方法。
笔者这里有点事实可以整明我的观点。
众所周知Java是代码是一行一行的去编译解释的,我们复现的类URLStreamHandler,实现的类对象hander在url进实例化的时候处理了,也就是33行代码。但是进行反序列化操作的时候,并没有将此复现方法进行序列化,所以反序列化的时候不会处理URL,计算hash值的时候,不可能跳转到我们复写的方法返回一个null,只能是跳转到原本的方法中。
总结
当URL最初被放在HashMap中时,通过可以调用put,HashMap.hashMap.hash方法被调用。这个方法反过来又会调用该URL的hashCode,但是hashCode是有一个缓存值的,并不会触发DNS解析。但是我们可以在读取数据流的时候,在URL添加到HashMap中重置缓存值(使其hashCode=-1),来确保DNS解析。可以使用Java的Reflection中setFieldValue方法来达到重置hashCode值为-1。
Ysoserial用一个类复写完美避免重复DNSLOG,感概其作者的神奇逻辑思维能力。有兴趣的朋友完全可以去注释掉getHostAddress
方法亦或者是删除掉整个handler代码,然后就会出现DNSLOG。
参考
P神Java安全漫谈 - 08.反序列化篇(2)
https://blog.paranoidsoftware.com/triggering-a-dns-lookup-using-java-deserialization/
https://lalajun.github.io/2020/03/05/JAVA%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96-ysoserial-URLDNS/