Spring Boot Fat Jar 写文件漏洞到稳定 RCE 的探索
字数 1694 2025-08-10 08:29:04

Spring Boot Fat Jar 写文件漏洞到稳定 RCE 的探索

0x00: 背景

Spring Boot 项目通常打包成 FatJar(包含所有依赖的 jar),运行时 classpath 包括:

  • BOOT-INF/classes 目录
  • BOOT-INF/lib 目录下的所有 jar

这使得在运行时无法直接往 classpath 中增加文件。传统 webshell 方法难以奏效,因此需要探索新的 RCE 方法。

0x01: 类装载与类初始化

关键区别

类装载(Class loading):

  • 由 ClassLoader 完成
  • 被动触发(引用类时)或主动触发(loadClass/forName)
  • 完成字节码读取和 Class 类型定义
  • 不执行任何类代码

类初始化(Class initialization):

  • 发生在类装载之后
  • 触发时机:访问静态属性/方法、new、newInstance、Class.forName等
  • 会执行 static 代码块和构造器代码

代码示例

// 仅类装载
classLoader.loadClass(className);
Class.forName(className, false, classLoader);

// 类初始化
classLoader.loadClass(className).newInstance();
Class.forName(className, true, classLoader);

0x02: 漏洞利用思路

通过写文件漏洞控制类初始化过程,实现代码执行:

  1. 找到可控的类初始化触发点
  2. 通过写文件覆盖关键系统类
  3. 触发初始化执行恶意代码

0x03: 文件写入目标

目标选择

由于无法直接写入应用 classpath,选择写入 JDK HOME 目录下的系统 jar 文件:

  • JVM 采用"懒加载"机制,不会一开始加载所有系统 jar
  • 需要选择启动后未被"Opened"的系统 jar

理想目标

/jre/lib/charsets.jar 是理想目标,因为:

  1. 默认不加载(除非使用 Charset.forName
  2. 包含字符集相关类
  3. 可被多种场景触发

常见 JDK 目录

/usr/lib/jvm/java-8-oracle/jre/lib/
/usr/lib/jvm/java-1.8-openjdk/jre/lib/
/usr/lib/jvm/java-8-openjdk-amd64/jre/lib/

0x04: 可控类初始化场景

场景零:Spring 原生场景

触发路径:

  1. 请求头 AcceptHeaderContentNegotiationStrategy 处理
  2. 调用 MediaType.parseMediaTypes()
  3. 最终调用 Charset.forName(value)

利用方式:

GET / HTTP/1.1
Accept: text/html;charset=GBK

场景一:Fastjson 最新版(1.2.76)默认配置

利用条件:

  • Fastjson 默认配置
  • java.nio.charset.Charset 在白名单中

触发路径:

  1. 解析 JSON 时找到 Charset
  2. 调用 Charset.forName(strVal)
  3. 加载自定义字符集类

利用 payload:

{
    "x":{
        "@type":"java.nio.charset.Charset",
        "val":"GBK"
    }
}

场景二:Jackson 开启 enableDefaultTyping

(详细实现参考原文或测试代码)

场景三:JDBC URL getConnection

(详细实现参考原文或测试代码)

场景四:直接 Class.forName

(详细实现参考原文或测试代码)

场景五:直接 loadClass newInstance

(详细实现参考原文或测试代码)

0x05: 测试环境搭建

测试代码和 Docker 环境已托管在 GitHub:
https://github.com/landgrey/spring-boot-upload-file-lead-to-rce-tricks

包含:

  • 漏洞测试环境
  • 各种利用场景的示例代码
  • 详细实现细节

防御建议

  1. 严格控制文件写入权限
  2. 监控 JDK 目录的文件变更
  3. 及时更新 Fastjson/Jackson 等组件
  4. 禁用不必要的 JSON 类型推导功能
  5. 使用安全的字符集处理方法

参考文章

  1. when-class-loading-initialization
  2. Improving Performance with Caching
  3. Understanding Extension Class Loading
  4. 深入探讨 Java 类加载器
  5. 深入理解Java类加载
  6. 细说Class.forName()底层实现
  7. LeadroyaL/fastjson-blacklist

原文首发: https://landgrey.me/blog/22/

Spring Boot Fat Jar 写文件漏洞到稳定 RCE 的探索 0x00: 背景 Spring Boot 项目通常打包成 FatJar(包含所有依赖的 jar),运行时 classpath 包括: BOOT-INF/classes 目录 BOOT-INF/lib 目录下的所有 jar 这使得在运行时无法直接往 classpath 中增加文件。传统 webshell 方法难以奏效,因此需要探索新的 RCE 方法。 0x01: 类装载与类初始化 关键区别 类装载(Class loading) : 由 ClassLoader 完成 被动触发(引用类时)或主动触发(loadClass/forName) 完成字节码读取和 Class 类型定义 不执行任何类代码 类初始化(Class initialization) : 发生在类装载之后 触发时机:访问静态属性/方法、new、newInstance、Class.forName等 会执行 static 代码块和构造器代码 代码示例 0x02: 漏洞利用思路 通过写文件漏洞控制类初始化过程,实现代码执行: 找到可控的类初始化触发点 通过写文件覆盖关键系统类 触发初始化执行恶意代码 0x03: 文件写入目标 目标选择 由于无法直接写入应用 classpath,选择写入 JDK HOME 目录下的系统 jar 文件: JVM 采用"懒加载"机制,不会一开始加载所有系统 jar 需要选择启动后未被"Opened"的系统 jar 理想目标 /jre/lib/charsets.jar 是理想目标,因为: 默认不加载(除非使用 Charset.forName ) 包含字符集相关类 可被多种场景触发 常见 JDK 目录 0x04: 可控类初始化场景 场景零:Spring 原生场景 触发路径 : 请求头 Accept 被 HeaderContentNegotiationStrategy 处理 调用 MediaType.parseMediaTypes() 最终调用 Charset.forName(value) 利用方式 : 场景一:Fastjson 最新版(1.2.76)默认配置 利用条件 : Fastjson 默认配置 java.nio.charset.Charset 在白名单中 触发路径 : 解析 JSON 时找到 Charset 类 调用 Charset.forName(strVal) 加载自定义字符集类 利用 payload : 场景二:Jackson 开启 enableDefaultTyping (详细实现参考原文或测试代码) 场景三:JDBC URL getConnection (详细实现参考原文或测试代码) 场景四:直接 Class.forName (详细实现参考原文或测试代码) 场景五:直接 loadClass newInstance (详细实现参考原文或测试代码) 0x05: 测试环境搭建 测试代码和 Docker 环境已托管在 GitHub: https://github.com/landgrey/spring-boot-upload-file-lead-to-rce-tricks 包含: 漏洞测试环境 各种利用场景的示例代码 详细实现细节 防御建议 严格控制文件写入权限 监控 JDK 目录的文件变更 及时更新 Fastjson/Jackson 等组件 禁用不必要的 JSON 类型推导功能 使用安全的字符集处理方法 参考文章 when-class-loading-initialization Improving Performance with Caching Understanding Extension Class Loading 深入探讨 Java 类加载器 深入理解Java类加载 细说Class.forName()底层实现 LeadroyaL/fastjson-blacklist 原文首发: https://landgrey.me/blog/22/