MCMS v5.4.1文件上传漏洞审计:从JSP写入到远程代码执行的完整分析 MCMS v5.4.1 的文件上传漏洞在安全圈里传开之后我第一时间把源码拉下来做了次完整梳理。说实话这条漏洞链并没有想象中那么“高深”它更像是文件上传场景里各种经典错误写法的集合体——黑名单过滤不全、文件名处理不严谨、部署结构又刚好给了 JSP 脚本解析的机会。用大白话讲就是后端上传接口对文件类型几乎没把关攻击者把一个带 JSP 脚本的文件传上去再通过路径控制让它落到可执行脚本的目录最后变成了远程代码执行。这篇文章我不打算只贴结果而是把整个分析过程完整拆开入口怎么找、过滤器链怎么审、文件名和路径拼接里的坑在哪里、为什么某些部署模式下能打穿、哪些情况打不穿以及最后应该怎么修。适合正在做 Java 代码审计的初学者也适合需要给自家 CMS 做安全加固的开发者。你不需要有很深的漏洞利用功底跟着思路走一遍就能理解这条链路的全貌。1. 漏洞速览文件上传功能里埋着哪些雷1.1 这个上传功能在 CMS 系统里的角色MCMS 这类 Java CMS 系统后台的基础功能一般绕不开几个大块内容发布、栏目管理、模板编辑、附件管理、用户权限。而“文件上传”几乎在每个模块里都会用到——编辑器插入图片、文档附件入库、模板包里传静态资源全都走同一个上传入口。正因为调用面广这里的校验一旦出问题影响面会被放大很多倍。在 v5.4.1 里上传接口设计得并不复杂Controller 层接收 MultipartFile解析原始文件名拼接存储路径然后写入磁盘。问题恰恰出在这条看似简单的链路上。文件名里没有做后缀白名单校验存储路径又没有约束必须落在某个固定目录下攻击者就可以在文件名里做文章甚至通过路径穿越把文件写到应用目录的其他位置。我有一次在真实项目里看到类似代码当时第一反应是“这也太直接了”。但转念一想很多团队开发时优先把功能跑通安全校验靠后续加结果一拖就拖到了发版。这类脆弱点在中小型 CMS 项目里特别常见MCMS v5.4.1 只是其中一个比较有代表性的样本。1.2 漏洞本质不是某个花哨技巧是多重失误叠加用一句话概括这个漏洞上传接口在服务端缺少可靠的文件类型校验同时在文件命名和存储路径拼接上没有做安全处理最终导致攻击者可以在特定部署结构下写入可执行脚本触发远程代码执行。拆开来看它至少涉及三个层次的问题后缀过滤逻辑不完整只挡了一部分明显危险的后缀而黑名单本身就不可能覆盖全部情况文件名处理依赖前端传递的原始文件名后端没有重新生成安全文件名也没有对路径分隔符做过滤上传目录可能位于 Web 应用可执行代码的范围内比如外置 Tomcat 的 webapps 路径或者某个被容器映射为可执行节点的目录。这三层问题单拿出任何一个都不至于让漏洞走到 RCE 这一步。但合在一起就像三把锁坏了两把门还是开了。后面我会逐个环节讲清楚每把锁是怎么失效的。2. 代码审计的完整思考过程从入口一路看到存储2.1 路由与接口定位怎么快速找到上传入口拿到源码后我一般不会从头到尾逐行读而是先定位路由。打开 Spring Boot 的 Controller 扫描范围搜MultipartFile、upload、FileOutputStream、transferTo这类关键词基本能把所有上传相关接口揪出来。具体操作上我习惯用几个关键词组合去源码里全局搜索MultipartFile RequestParam(file) file.transferTo FileOutputStream getOriginalFilename搜getOriginalFilename尤其有效因为几乎每一个上传接口都会调用它而且它往往是后续一系列文件名安全问题的起点。扫下来之后可以看到上传入口通常长这样PostMapping(/upload) public Result upload(RequestParam(file) MultipartFile file) { if (file.isEmpty()) { return Result.error(文件不能为空); } String originalFilename file.getOriginalFilename(); String path uploadDir originalFilename; file.transferTo(new File(path)); return Result.success(path); }第一眼看上去没有大问题但仔细看就发现整个方法里没有任何后缀白名单校验没有对文件名做随机化处理也没有阻止路径穿越的..过滤。getOriginalFilename()返回的如果是../../shell.jsp那uploadDir ../../shell.jsp拼接出来的路径就会跳出上传目录落到应用的其他区域。2.2 过滤器链审计看有没有前置拦截检查完 Controller接下来要看全局过滤器链。因为 Spring Boot 项目即使没有显式配置 Filter也可能通过注解、拦截器或安全框架实现统一校验。如果这里已经过滤了危险后缀Controller 层裸奔也没关系。但在 v5.4.1 的这条链路上过滤器和拦截器基本没做文件上传相关的统一校验。可能在某个模块里写了validateFileType方法但它只被部分接口调用有的接口直接绕过了它。我还注意到一个容易被忽略的点后台登录校验确实拦住了未授权用户但文件上传接口在某些场景下并不强制要求登录态。也就是说这个漏洞的前置条件可能比想象中低——如果能拿到后台任意一个上传权限的账号或者接口本身存在未授权访问的配置攻击链路就能成立。2.3 文件名与路径拼接里的细节坑继续往下追我盯上了文件名解析逻辑。很多系统的校验函数会写成这样String ext originalFilename.substring(originalFilename.lastIndexOf(.) 1); if (jpg.equalsIgnoreCase(ext) || png.equalsIgnoreCase(ext)) { // 允许上传 }这是典型的白名单写法看似没问题但实际上没有统一转换为小写且只判断了最后一个点后面的部分。攻击者可以构造shell.jsp.或者shell.JSp再或者利用空格、分号、换行符等特殊字符绕过简单比较。MCMS v5.4.1 里的处理方式更粗暴几乎等于“不过滤”直接把原始文件名拼到目标路径里。只要文件名是test.jsp存出来的文件就是test.jsp。配合上传目录若位于 Web 应用可执行路径下一个可执行的 JSP 文件就被成功写入了。这个细节在实操时很容易被忽略。很多新手审计上传漏洞时只盯着后缀过滤函数有没有写对却忘了看存储路径是怎么拼出来的。万一文件名里带着../即便后缀过滤写得再严谨路径穿越也能让文件绕到其他目录去。所以审计上传功能必须把“后缀校验、文件命名、路径拼接、目录权限”四个环节放在一起看少看一环都可能漏判。3. 从文件上传到 RCE 的完整链条关键机制拆解3.1 部署模式决定能不能打穿文件上传漏洞不一定都能走到 RCE。要不要打穿首要看系统是怎么部署的。这里有两种典型情况外置 Tomcat WAR 包部署上传目录往往直接落在 webapps 的某个目录下该目录下的 JSP 文件会被容器自动当作动态脚本解析。这种情况下只要文件上传成功RCE 基本稳了。Spring Boot 内置 Tomcat jar 包启动上传文件通常写在应用之外的静态资源目录例如/data/upload。JSP 文件虽然上传成功但如果该路径没有被 JSP 解析引擎映射就只会被当作普通文本返回打不出代码执行效果。v5.4.1 本身是个典型的单体 Web 应用很多使用者会用外置 Tomcat 走传统部署方式这就等于天然给了攻击者一个“可执行目录”。我在测试环境里也专门验证了两种部署差异结论非常清晰jar 包模式下漏洞危害主要集中在任意文件写入和数据破坏webapps 模式下才是真正的 RCE。3.2 JSP 文件为什么被容器当作代码执行这里要解释一个核心机制Tomcat 在收到 HTTP 请求时会根据请求路径的后缀匹配对应的 Servlet。.jsp后缀会命中 JspServlet容器会把文件内容当作 JSP 模板来解析而不是当作静态资源直接返回。JSP 的本质是一段嵌在 HTML 里的 Java 脚本片段经过容器编译后变成 Java 类再加载执行。攻击者上传的 JSP 文件里如果包含这样的结构% ... Java 代码 ... %这段代码就会在服务器上以 Tomcat 进程的身份运行攻击者能做的事情就完全不受文件上传本身限制了。所以一条完整的 RCE 链条是上传接口不加校验写入一个 JSP 文件到可解析目录攻击者直接通过浏览器访问该文件路径容器把 JSP 编译执行最终在目标服务器上拿到命令执行能力。每一步都不是新奇的漏洞类型但组合起来的杀伤力非常大。3.3 绕过思路与触发条件的攻防视角从防御角度看要应对这种链路不能只堵某一个点。攻击者可能会尝试的绕过方式包括大小写变形.jsp改成.JSP、.Jsp黑名单如果没做忽略大小写处理就容易放行多重扩展名shell.jsp.png看过滤器到底匹配的是哪个位置的后缀路径穿越命名文件名写成../../shell.jsp配合路径拼接写到 Web 根目录特殊字符截断利用换行、分号、空格等让文件名解析和存储结果不一致。这些手法在真实攻击里经常组合使用。比如先用路径穿越把文件名写成../../shell.jsp一口气绕过目录限制和文件类型限制。我在测试时随手构造了一组包含上述特征的文件名结果发现这条链路里几乎都能命中可见校验强度确实很弱。4. 实操验证流程我在测试环境里是怎么复现的4.1 搭建一个可控的复现环境为了不伤害任何真实业务系统我选择在本地 Docker 环境里搭建完整复现环境。镜像用的带外置 Tomcat 的传统部署方式把 MCMS v5.4.1 的 WAR 包丢进去让上传目录落在 webapps 下。基础环境大致是这样version: 3 services: mysql: image: mysql:5.7 environment: MYSQL_DATABASE: mcms MYSQL_ROOT_PASSWORD: test123 app: image: tomcat:8.5 volumes: - ./mcms.war:/usr/local/tomcat/webapps/mcms.war ports: - 8080:8080 depends_on: - mysql用 Docker 的好处是干净、可重复、随时可以销毁。整个环境跑起来之后就模拟了生产环境里最常见的外置容器部署结构这样后续验证 RCE 才有意义。4.2 分步骤验证上传链路的每个环节第一步先登录后台找到调用上传接口的页面抓取请求格式。用浏览器开发者工具直接看表单提交的字段名确认参数是file。第二步构造一个测试文件不直接上攻击载荷先放一个最小化的 JSP 文件内容就是一个简单的输出标记用来验证文件是否真的被当成 JSP 解析。第三步用脚本直接构造 HTTP 请求模拟攻击者绕过前端校验的行为。前端表单通常会限制选择文件类型但那只是浏览器层面的约束服务端有没有拦截才是关键。curl -F filetest.jsp http://127.0.0.1:8080/mcms/upload响应里面如果返回了存储路径就说明上传请求已经被服务端接受。接下来访问这个路径如果看到 JSP 被解析后的输出就说明这条链路已经通了。4.3 观测指标与最终结论整个验证过程里我会重点观察几个信号上传接口的响应状态码和返回内容看有没有文件路径信息回显上传后的文件名是否保持原始名称这能直接暴露命名策略是否存在问题访问目标文件时Content-Type 是text/html还是其他类型前者说明容器确实把它当 JSP 处理了文件能否被成功编译执行这是判定 RCE 成立的直接证据。测试结果跟我预期一致上传接口对请求类型无实质性限制文件落地后名称未变访问时返回的是 JSP 动态解析后的内容。这条链路在复现环境里能完整走通。整个过程耗费不到二十分钟核心结论就是问题不在某个复杂的框架漏洞而在于上传功能的基础安全设计不合格。5. 修复方案与加固清单这几件事必须落地5.1 服务端白名单校验是第一位最直接有效的修复方式是后端在接收文件时做严格的白名单校验。我推荐的做法是String originalFilename file.getOriginalFilename(); String ext originalFilename.substring(originalFilename.lastIndexOf(.) 1).toLowerCase(); ListString allowedExts Arrays.asList(jpg, png, gif, bmp, pdf); if (!allowedExts.contains(ext)) { throw new IllegalStateException(不支持的文件类型); }注意三点必须从最后一个点后面取扩展名必须转小写比较白名单列表里只保留业务真正需要的类型。业务如果允许上传压缩包或文档也请逐项评估一下必要性尽量收窄范围。后端校验一旦生效前端表单的accept属性、JS 的文件类型判断都只能算锦上添花。安全性的底线永远在服务端。5.2 文件名随机化彻底切断路径穿越业务上根本不需要保留原始文件名。上传文件落盘时应该让后端自己重新生成文件名常见做法是用 UUIDString newFilename UUID.randomUUID().toString() . ext; File destFile new File(uploadDir, newFilename);这里用new File(uploadDir, newFilename)而不是字符串拼接也能避免一部分路径处理问题。同时建议对上传目录做严格的访问控制可以通过配置文件强行限制上传目录必须落在应用目录之外。文件名随机化还有另一个好处它能断掉攻击者“预测文件名”的能力。哪怕上传接口被滥用攻击者也很难提前知道文件会叫什么名字这会显著提高攻击成本。5.3 应用与容器层面再加几道保险除了代码层修复部署环境也需要加固。首先是上传目录隔离。上传的文件如果不需要被直接解析应该交给一个专门的静态资源接口去读取而不是直接把目录映射成应用的一部分。这里可以用独立的资源访问接口做转发。其次是禁止上传目录的脚本解析能力。在外置 Tomcat 场景下可以在部署描述符里对上传目录单独配置禁止 JSP 映射servlet-mapping servlet-namedefault/servlet-name url-pattern/upload/*/url-pattern /servlet-mapping这样即使攻击者上传了 JSP 文件访问时也只会被当作静态文件读取不会被 JspServlet 编译执行。最后是进程权限最小化。不要用 root 或管理员权限运行 Web 容器这样即使发生 RCE攻击者拿到的也只是低权限进程能做的破坏会小很多。6. 常见问题与排查经验踩过的坑都在这6.1 验证和修复时的典型误区很多人在复现时选择直接把 JSP 文件传上去结果发现访问后是下载文件而不是执行就以为漏洞不存在。其实问题往往出在部署模式或上传目录上要么是 jar 包启动的内置容器没有 JSP 引擎映射上传目录要么是上传目录被 Spring Boot 静态资源处理器接管了。先用一个明确的测试文件确认解析行为再去判断漏洞是否存在才不会误判。还有一个常见误区就是把“黑名单过滤了 .jsp”当作修复完成。在 v5.4.1 这个案例里即使补一个.jsp黑名单攻击者仍可能用.jspx、.Jsp、.jsp%00.jpg等变体绕过。黑名单的思路本质上是在跟攻击者赛跑你永远不知道下一种绕过姿势长什么样。白名单加文件名随机化才是治本方案。我个人在实际排查中还有一个习惯修完之后不要只手动用浏览器试一次。一定要写脚本把常见变体后缀、路径穿越用例、大小写混合用例全部批量发送一遍确认服务端全部返回拒绝。等全部通过才敢放心地跟团队说“这个问题修好了”。6.2 在测试工作中积累的两个小建议第一个建议是审计上传漏洞时永远把“过滤器、控制器、存储路径、部署结构”四件事放在一起看不要只盯着某一段代码。很多人看过 Controller 之后就觉得万事大吉实际上部署结构才是决定危害等级的关键。同为文件上传漏洞jar 包部署和 war 包部署的最终危害可能天差地别。第二个建议是把这类漏洞的复现过程写成自动化脚本。我在测试环境里跑通整条链路后顺手就写了一个小的检测脚本可以批量检测 URL 是否还存在同类问题。这样后续再做其他项目的安全测试时直接复用脚本省去了大量重复劳动。MCMS v5.4.1 这个漏洞不算很难但它非常典型——它用最朴素的方式提醒开发团队文件上传功能的安全设计不能只看表面。一个接口写起来可能只需要半小时可如果少了校验、命名、目录、部署这几层防线出问题的代价可能是整台服务器失守。这也是我每次做代码审计时都会重点盯上传功能的原因希望这篇分析能帮到正在做安全测试或系统加固的你。