环境准备
package org.URLDns;
import java.io.FileInputStream;
import java.io.ObjectInputStream;
public class URLDNS {
public static void main(String[] args) throws Exception {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("C:\Users\86183\Desktop\out.bin"));
ois.readObject();
}
}
利用链分析
通过上面一篇文章,搭建后,然后我们对最简单的URLDNS链进行分析
触发反序列化的⽅法是readObject
,那么,我们可以直奔 HashMap
类的 readObject
⽅法:
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);
}
}
}
HashMap
:键唯一,键值对存取无序, 由哈希表保证键唯一
在最后一行中,我们可以看到将HashMap
的键名计算了hash,以此保证键唯一:
putVal(hash(key), key, value, false, false);
以至于我们为什么关注这个点?
因为ysoserial提到了hashCode计算操作时触发了DNS请求
我们可以看到这边调用链,以及代码,这边readObject调用了hash()
我们跟进去看
发现hash()方法又调用了hashcode()方法,由于key是java.net.URL
类型
我们继续跟进hashCode()方法,因为是该类型,所以进入的hashcode()方法不一样(可能是跟我女朋友打电话,搞得我没法专心,断点下错了还是啥)
hashcode就是等于-1,所以进入下面一行,handler 是 URLStreamHandler 对象
之后我们继续跟进hashcode()方法
此处调用了 getHostAddress,我们再一次的f7跟进
这里的InetAddress.getByName,在给定主机名的情况下确定主机的IP地址,意思就是发起一次请求
如果还有不懂的,可以看看这篇文章 Java学习之网路编程
所以,⾄此,整个 URLDNS 的Gadget其实清晰⼜简单:
- HashMap->readObject()
- HashMap->hash()
- URL->hashCode()
- URLStreamHandler->hashCode()
- URLStreamHandler->getHostAddress()
- InetAddress->getByName()
总的来说:
在序列化 HashMap 类的对象时, 为了减小序列化后的大小, 并没有将整个哈希表保存进去, 而是仅仅保存了所有内部存储的 key 和 value. 所以在反序列化时, 需要重新计算所有 key 的 hash, 然后与 value 一起放入哈希表中. 而恰好, URL 这个对象计算 hash 的过程中用了 getHostAddress 查询了 URL 的主机地址, 自然需要发出 DNS 请求
要构造这个Gadget,只需要初始化⼀个 java.net.URL 对象,作为 key 放在 java.util.HashMap
中;然后,设置这个 URL 对象的 hashCode 为初始值 -1 ,这样反序列化时将会重新计算
其 hashCode ,才能触发到后⾯的DNS请求,否则不会调⽤ URL->hashCode()
参考文章
p牛的java漫谈
百度上的一些文章