Java 反序列化基础
字数 3351 2025-10-26 18:21:34

Java反序列化漏洞基础教学文档

第一章:序列化与反序列化基础

1.1 核心概念

  • 序列化: 将内存中的Java对象转换为字节流的过程。这个过程可以将对象的状态(数据)保存到文件、数据库,或通过网络进行传输。
  • 反序列化: 将字节流还原为内存中Java对象的过程。它是序列化的逆过程。

1.2 技术存在的意义

序列化与反序列化主要用于数据传输和持久化,常见的应用场景包括:

  • 将对象状态保存到文件或数据库中。
  • 通过网络套接字传输对象。
  • 在RMI(远程方法调用)中传输对象。

1.3 常见的序列化协议

除了Java原生的序列化机制,还存在其他协议:

  • XML
  • SOAP: 基于XML的结构化消息传递协议。
  • JSON
  • Protobuf(Google Protocol Buffers)

1.4 Java中的序列化实现

  1. 实现Serializable接口: 任何需要被序列化的类都必须实现java.io.Serializable接口。这是一个标记接口,不包含任何方法。

    import java.io.Serializable;
    public class Person implements Serializable {
        private String name;
        private int age;
        // ... 构造方法、getter/setter、toString ...
    }
    
  2. 序列化过程: 使用ObjectOutputStream将对象写入字节流。

    // 写法一:传统IO
    ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
    oos.writeObject(person);
    
    // 写法二:NIO(现代推荐)
    ObjectOutputStream oos = new ObjectOutputStream(Files.newOutputStream(Paths.get("ser.bin")));
    oos.writeObject(person);
    
  3. 反序列化过程: 使用ObjectInputStream从字节流中读取对象。

    ObjectInputStream ois = new ObjectInputStream(Files.newInputStream(Paths.get("ser.bin")));
    Person person = (Person) ois.readObject();
    

第二章:反序列化漏洞的根源

2.1 核心安全问题

根本原因: Java反序列化机制在还原对象时,会自动调用特定方法(如readObject)。如果攻击者能够控制反序列化的数据源,并精心构造一个恶意的字节流,就可以诱使服务端在执行反序列化过程中执行任意代码。

2.2 漏洞触发点(攻击面)

文章指出了四种典型的漏洞触发模式:

  1. 入口类的readObject方法直接调用危险方法

    • 描述: 如果自定义类重写了readObject方法,并在其中直接调用了危险代码(如命令执行),那么反序列化该类的对象时就会触发漏洞。
    • 示例
      public class Person implements Serializable {
          private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
              ois.defaultReadObject(); // 调用默认反序列化
              Runtime.getRuntime().exec("calc"); // 危险操作!
          }
      }
      
  2. 入口类参数中包含可控类,该类有危险方法,readObject时调用

    • 描述: 入口类(如HashMap)的readObject方法在反序列化过程中,会对其中的键值对进行某些操作(如计算哈希值hashCode())。如果键是攻击者可控的类(如URL),并且该类的hashCodeequals等方法存在危险行为,漏洞就会被触发。
    • 示例URLDNS链就是利用这一点。反序列化一个HashMap<URL, Integer>时,HashMap.readObject()会调用URL.hashCode(),而URL.hashCode()会触发一次DNS查询,从而验证反序列化是否发生。
  3. 入口类参数中包含可控类,该类又调用其他有危险方法的类

    • 描述: 这是上一种情况的延伸,构成一条调用链(Gadget Chain)。入口类调用可控类A的方法,类A的方法又调用类B的危险方法。
  4. 构造函数/静态代码块等类加载时隐式执行

    • 描述: 即使不直接调用readObject,在类被加载时,其静态代码块或构造函数也会被执行。如果这些代码块中包含危险逻辑,同样可以构成攻击。
    • 示例: 重写toString方法,在其中执行命令,当反序列化后的对象被打印(隐式调用toString)时触发。

2.3 挖掘漏洞的三个关键条件

要成功利用一个反序列化漏洞,需要同时满足三个条件:

  1. 入口类(Source)

    • 可序列化。
    • 重写了readObject方法。
    • 该方法中调用了常见的函数(如put, equals, compare等)。
    • 方法的参数类型宽泛(如Object, Map等),允许传入任意对象。
    • 最好是JDK自带的类,因为攻击载荷的通用性更强。
  2. 执行类(Sink)

    • 一个可以执行危险操作的点,如:
      • RCERuntime.exec(), ProcessBuilder.start()
      • SSRFURL.openConnection()
      • 文件写入FileOutputStream.write()
  3. 调用链(Gadget Chain)

    • 一条从入口类readObject方法到执行类的危险方法的完整方法调用链。这条链由多个类的不同方法连接而成。

第三章:前置知识 - Java反射

3.1 反射的概念

反射机制允许程序在运行时(而非编译时)探查、获取并操作类(Class)、方法(Method)、字段(Field)等信息。它赋予了Java动态语言的能力。

  • 正射: 在编译时就知道要操作的类和具体方法。
    Student student = new Student(); // 编译时类型已知
    student.sayHello();
    
  • 反射: 在运行时才动态地加载类、创建对象、调用方法。
    Class clazz = Class.forName("com.example.Student"); // 运行时根据字符串加载类
    Object instance = clazz.newInstance();
    Method method = clazz.getMethod("sayHello");
    method.invoke(instance);
    

3.2 反射的核心API

反射中几个极为重要的方法:

  • Class.forName(String className): 根据类名获取该类的Class对象。
  • clazz.newInstance(): 通过Class对象创建类的实例(调用无参构造)。
  • clazz.getMethod(String name, Class... parameterTypes): 获取方法对象。
  • method.invoke(Object obj, Object... args): 调用指定对象的方法。

第四章:经典漏洞链分析 - URLDNS

4.1 链分析

URLDNS是ysoserial工具中的一个Gadget Chain,它不直接执行命令,而是用于检测目标是否存在反序列化漏洞,因为它会产生一个DNS请求,非常直观。

调用链
HashMap.readObject() -> HashMap.put() -> HashMap.hash() -> URL.hashCode() -> URLStreamHandler.hashCode() -> URLStreamHandler.getHostAddress() -> InetAddress.getByName() (触发DNS查询)

4.2 复现关键点

  1. 构造Payload: 创建一个HashMap,并放入一个URL对象作为key。
  2. 避免提前触发DNS: 在放入HashMap之前,需要利用反射将URL对象的hashCode字段设置为非-1的初始值,以防止在put操作时就触发DNS查询。真正的漏洞触发应在反序列化时的readObject中。
  3. 序列化与反序列化: 将构造好的HashMap序列化成字节流,然后让目标程序反序列化该流。

4.3 学习意义

URLDNS链是学习Java反序列化的最佳入门案例,因为它:

  • 只依赖JDK内置类,通用性极高。
  • 不涉及复杂的第三方库。
  • 清晰地展示了从readObject到最终触发网络请求的完整链条。
  • 无害,仅用于探测。

第五章:从探测到利用 - 命令执行(RCE)

URLDNS链仅用于探测。要实现真正的命令执行(RCE),需要寻找更复杂的Gadget Chain,其最终会调用Runtime.exec()或类似方法。

核心思路: 寻找一条从某个可序列化入口类(如AnnotationInvocationHandler, BadAttributeValueExpException等)的readObject方法出发,最终能够动态调用到Runtime.getRuntime().exec("命令")的调用链。这通常需要结合反射动态代理等机制来绕过各种限制。

由于这些链通常涉及多个第三方库(如Commons-Collections),构造起来比URLDNS复杂得多,但基本原理相通:控制反序列化数据,引导程序执行预设的恶意代码路径


总结

本教学文档系统性地梳理了Java反序列化漏洞的基础知识。理解这些概念是进一步学习复杂Gadget Chain(如CommonsCollections、Fastjson等)的基石。核心要点在于:反序列化本身不是漏洞,但将不可信的数据交给反序列化机制处理,且程序中存在一条从入口点到危险操作的完整调用链,就构成了严重的安全漏洞。

Java反序列化漏洞基础教学文档 第一章:序列化与反序列化基础 1.1 核心概念 序列化 : 将内存中的Java对象转换为字节流的过程。这个过程可以将对象的状态(数据)保存到文件、数据库,或通过网络进行传输。 反序列化 : 将字节流还原为内存中Java对象的过程。它是序列化的逆过程。 1.2 技术存在的意义 序列化与反序列化主要用于数据传输和持久化,常见的应用场景包括: 将对象状态保存到文件或数据库中。 通过网络套接字传输对象。 在RMI(远程方法调用)中传输对象。 1.3 常见的序列化协议 除了Java原生的序列化机制,还存在其他协议: XML SOAP : 基于XML的结构化消息传递协议。 JSON Protobuf (Google Protocol Buffers) 1.4 Java中的序列化实现 实现 Serializable 接口 : 任何需要被序列化的类都必须实现 java.io.Serializable 接口。这是一个标记接口,不包含任何方法。 序列化过程 : 使用 ObjectOutputStream 将对象写入字节流。 反序列化过程 : 使用 ObjectInputStream 从字节流中读取对象。 第二章:反序列化漏洞的根源 2.1 核心安全问题 根本原因 : Java反序列化机制在还原对象时,会自动调用特定方法(如 readObject )。如果攻击者能够控制反序列化的数据源,并精心构造一个恶意的字节流,就可以诱使服务端在执行反序列化过程中执行任意代码。 2.2 漏洞触发点(攻击面) 文章指出了四种典型的漏洞触发模式: 入口类的 readObject 方法直接调用危险方法 描述 : 如果自定义类重写了 readObject 方法,并在其中直接调用了危险代码(如命令执行),那么反序列化该类的对象时就会触发漏洞。 示例 : 入口类参数中包含可控类,该类有危险方法, readObject 时调用 描述 : 入口类(如 HashMap )的 readObject 方法在反序列化过程中,会对其中的键值对进行某些操作(如计算哈希值 hashCode() )。如果键是攻击者可控的类(如 URL ),并且该类的 hashCode 或 equals 等方法存在危险行为,漏洞就会被触发。 示例 : URLDNS 链就是利用这一点。反序列化一个 HashMap<URL, Integer> 时, HashMap.readObject() 会调用 URL.hashCode() ,而 URL.hashCode() 会触发一次DNS查询,从而验证反序列化是否发生。 入口类参数中包含可控类,该类又调用其他有危险方法的类 描述 : 这是上一种情况的延伸,构成一条调用链(Gadget Chain)。入口类调用可控类A的方法,类A的方法又调用类B的危险方法。 构造函数/静态代码块等类加载时隐式执行 描述 : 即使不直接调用 readObject ,在类被加载时,其静态代码块或构造函数也会被执行。如果这些代码块中包含危险逻辑,同样可以构成攻击。 示例 : 重写 toString 方法,在其中执行命令,当反序列化后的对象被打印(隐式调用 toString )时触发。 2.3 挖掘漏洞的三个关键条件 要成功利用一个反序列化漏洞,需要同时满足三个条件: 入口类(Source) : 可序列化。 重写了 readObject 方法。 该方法中调用了常见的函数(如 put , equals , compare 等)。 方法的参数类型宽泛(如 Object , Map 等),允许传入任意对象。 最好是JDK自带的类 ,因为攻击载荷的通用性更强。 执行类(Sink) : 一个可以执行危险操作的点,如: RCE : Runtime.exec() , ProcessBuilder.start() SSRF : URL.openConnection() 文件写入 : FileOutputStream.write() 调用链(Gadget Chain) : 一条从 入口类 的 readObject 方法到 执行类 的危险方法的完整方法调用链。这条链由多个类的不同方法连接而成。 第三章:前置知识 - Java反射 3.1 反射的概念 反射机制允许程序在运行时(而非编译时)探查、获取并操作类(Class)、方法(Method)、字段(Field)等信息。它赋予了Java动态语言的能力。 正射 : 在编译时就知道要操作的类和具体方法。 反射 : 在运行时才动态地加载类、创建对象、调用方法。 3.2 反射的核心API 反射中几个极为重要的方法: Class.forName(String className) : 根据类名获取该类的Class对象。 clazz.newInstance() : 通过Class对象创建类的实例(调用无参构造)。 clazz.getMethod(String name, Class... parameterTypes) : 获取方法对象。 method.invoke(Object obj, Object... args) : 调用指定对象的方法。 第四章:经典漏洞链分析 - URLDNS 4.1 链分析 URLDNS是ysoserial工具中的一个Gadget Chain,它不直接执行命令,而是用于 检测 目标是否存在反序列化漏洞,因为它会产生一个DNS请求,非常直观。 调用链 : HashMap.readObject() -> HashMap.put() -> HashMap.hash() -> URL.hashCode() -> URLStreamHandler.hashCode() -> URLStreamHandler.getHostAddress() -> InetAddress.getByName() (触发DNS查询) 4.2 复现关键点 构造Payload : 创建一个 HashMap ,并放入一个 URL 对象作为key。 避免提前触发DNS : 在放入 HashMap 之前,需要利用反射将 URL 对象的 hashCode 字段设置为非 -1 的初始值,以防止在 put 操作时就触发DNS查询。真正的漏洞触发应在反序列化时的 readObject 中。 序列化与反序列化 : 将构造好的 HashMap 序列化成字节流,然后让目标程序反序列化该流。 4.3 学习意义 URLDNS链是学习Java反序列化的 最佳入门案例 ,因为它: 只依赖JDK内置类,通用性极高。 不涉及复杂的第三方库。 清晰地展示了从 readObject 到最终触发网络请求的完整链条。 无害,仅用于探测。 第五章:从探测到利用 - 命令执行(RCE) URLDNS链仅用于探测。要实现真正的命令执行(RCE),需要寻找更复杂的Gadget Chain,其最终会调用 Runtime.exec() 或类似方法。 核心思路 : 寻找一条从某个可序列化入口类(如 AnnotationInvocationHandler , BadAttributeValueExpException 等)的 readObject 方法出发,最终能够动态调用到 Runtime.getRuntime().exec("命令") 的调用链。这通常需要结合 反射 和 动态代理 等机制来绕过各种限制。 由于这些链通常涉及多个第三方库(如Commons-Collections),构造起来比URLDNS复杂得多,但基本原理相通: 控制反序列化数据,引导程序执行预设的恶意代码路径 。 总结 本教学文档系统性地梳理了Java反序列化漏洞的基础知识。理解这些概念是进一步学习复杂Gadget Chain(如CommonsCollections、Fastjson等)的基石。核心要点在于: 反序列化本身不是漏洞,但将不可信的数据交给反序列化机制处理,且程序中存在一条从入口点到危险操作的完整调用链,就构成了严重的安全漏洞。