当前位置: 首页 > news >正文

CVE-2025-48976:Apache Commons FileUpload 协议解析层内存崩溃漏洞深度解析

1. 这个漏洞不是“上传文件被黑了”,而是整个解析逻辑崩了

Apache Commons FileUpload 是 Java 生态里最老牌、最被信任的文件上传处理库之一,从 2003 年发布第一个稳定版起,它就稳稳地嵌在 Struts2、Spring MVC(早期)、WebWork、甚至大量自研后台系统中。我经手过的政务系统、银行内部工单平台、教育管理平台,只要用 Java 做 Web 后端且支持附件上传,十有八九底层调的是commons-fileupload-1.5.jar或更早版本。它不炫技,不折腾,API 就两个核心类:DiskFileItemFactoryServletFileUpload,写法简单到像呼吸——你传一个HttpServletRequest进去,它就给你吐出List<FileItem>,每个FileItem里封装着文件名、内容类型、字节数组或临时文件路径。这种“拿来即用”的确定性,正是它被广泛采用的根本原因。

但 CVE-2025-48976 的出现,彻底打破了这种确定性。它不是传统意义上“攻击者上传了一个 .jsp 文件然后 getshell”那种链路清晰的漏洞;它发生在ServletFileUpload.parseRequest()方法最底层的 HTTP 请求体解析阶段——也就是请求还没真正进入业务逻辑、连FileItem对象都还没构造出来的时候。具体来说,当攻击者精心构造一段包含畸形Content-Disposition头字段的 multipart/form-data 请求体时,FileUpload 在解析该字段的filename参数过程中,会触发一个未经校验的字符串截断操作,导致后续对filename的长度判断完全失效。这个失效不是抛异常、不是返回空,而是让整个解析器进入一种“看似成功、实则错乱”的状态:它可能把本该属于下一个字段的值误认为是当前文件的文件名,也可能把 HTTP 请求头的一部分当作文件内容读入内存。我复现时用 Burp Suite 发送一个仅 387 字节的恶意请求,目标 Tomcat 进程的堆内存瞬间暴涨 120MB,GC 频率飙升至每秒 3 次,最终 OOM crash。这不是 DoS,这是解析引擎的“认知错乱”。

关键词“CVE-2025-48976”、“Apache Commons FileUpload”、“安全漏洞”在这句话里已经自然嵌入。这个漏洞影响的不是某个特定业务功能,而是所有依赖该库做 multipart 解析的 Java Web 应用——无论你用的是 Spring Boot 2.7 还是自己写的 Servlet,只要底层没绕过 FileUpload,就逃不掉。它适合两类人重点阅读:一是正在维护老旧 Java 系统的运维/开发工程师,你们的系统很可能还在用 1.3.x 版本;二是做 Java 安全审计的同行,这个漏洞的利用链非常干净,不需要 RCE 条件,纯协议层扰动就能达成服务中断。它不考验你的反序列化功底,只考验你对 HTTP 协议解析细节的敬畏心。

2. 漏洞根源不在代码行数,而在对 RFC 2045 的“选择性信任”

要真正理解 CVE-2025-48976,必须回到org.apache.commons.fileupload.FileUploadBase.parseRequest()方法的第 382 行——那个被打了补丁的parseFileName()调用。但比看代码更重要的是理解它为什么会在那里出问题。FileUpload 的设计哲学是“相信客户端传来的 Content-Disposition 头是符合 RFC 2045 规范的”。RFC 2045 明确规定:filename参数值必须用双引号包裹,且内部的双引号需用反斜杠转义,例如filename="my\"file.txt"。而 FileUpload 的原始实现,恰恰是基于这个“双引号必存在”的假设来定位 filename 值的起始和结束位置的。

问题来了:当攻击者发送一个不带双引号的filename=../../etc/passwd时,FileUpload 会尝试从等号后第一个非空白字符开始找“下一个双引号”,结果一直扫描到请求体末尾都没找到。这时,它没有选择报错退出,而是用了一个“兜底逻辑”:取从等号后到当前扫描位置(即整个请求体末尾)的所有字符作为 filename。这个逻辑本身没问题,但致命的是——它没有对这个“兜底截取”的字符串长度做任何限制。而parseFileName()方法后续会调用String.substring(),其参数是startend两个整数索引。当end索引被恶意设置为一个远超实际字符串长度的极大值(比如Integer.MAX_VALUE)时,JVM 的 substring 实现会直接抛出StringIndexOutOfBoundsException。但 FileUpload 的异常处理机制在这里有个关键疏漏:它只捕获了FileUploadException及其子类,而StringIndexOutOfBoundsExceptionRuntimeException,被直接吞掉了。于是解析流程继续向下执行,把一个 null 或未初始化的fileName传给后续的FileItem构造逻辑,最终导致内存分配失控。

我们来算一笔账:一个标准的 multipart 请求,boundary 是----WebKitFormBoundary7MA4YWxkTrZu0gW,那么一个普通文件字段的Content-Disposition头大致长这样:

Content-Disposition: form-data; name="file"; filename="test.pdf"

共 62 字符。而攻击载荷只需改成:

Content-Disposition: form-data; name="file"; filename=../../../../../../../../etc/passwd%00

注意这里没有双引号,结尾还加了 URL 编码的空字节%00。当 FileUpload 的解析器扫描到%00时,它会错误地认为这是 filename 值的结束,但实际上%00在 HTTP 请求体中只是普通字节,后面还跟着Content-Type和真正的文件二进制数据。于是它把从filename=开始一直到整个请求体末尾(可能几 MB)的所有字节都当作 filename 字符串加载进内存。Java 的 String 对象在 JVM 中是以 UTF-16 编码存储的,一个字节的恶意输入可能膨胀成两个 char,再加上对象头、数组引用等开销,1MB 的请求体就能吃掉 3~4MB 堆内存。这就是为什么一个 387 字节的请求能让服务 OOM——它触发的是指数级的内存误分配,而非线性增长。

提示:很多团队在做漏洞修复时,第一反应是“升级到 1.6 版本就行”。但如果你的系统里同时存在commons-fileupload-1.5.jarcommons-io-2.11.jar(后者常被其他组件间接引入),而commons-ioIOUtils.copy()方法又被 FileUpload 内部调用,那么即使你替换了 main jar,旧版的IOUtils仍可能被 ClassLoader 加载,导致补丁失效。验证是否真修复,必须用jstack抓取崩溃时的线程栈,确认parseFileName()调用链已指向新版本 class。

3. 复现不是为了炫技,而是为了看清“崩溃前的最后一秒”

复现 CVE-2025-48976 的过程,本质上是一次对 Java Web 容器请求解析生命周期的逆向解剖。我用的是最典型的组合:Spring Boot 2.5.14 + Tomcat 9.0.65 + commons-fileupload-1.5。之所以选这个组合,是因为它代表了大量生产环境的真实快照——Spring Boot 2.5 是 LTS 版本,Tomcat 9 是 Java 8/11 的主流容器,而 1.5 版本的 FileUpload 正是 Spring 5.3.x 默认携带的版本。整个复现不依赖任何第三方 exploit 工具,只用 curl 和一个文本编辑器,确保你能 100% 复现并理解每一步。

第一步,准备一个最简 Spring Boot Controller:

@RestController public class UploadController { @PostMapping("/upload") public String handleUpload(@RequestParam("file") MultipartFile file) { return "OK, size: " + file.getSize(); } }

注意,这里用的是 Spring 的MultipartFile,它底层默认就是CommonsMultipartResolver,也就是 FileUpload 的封装。启动应用后,用curl -v发送一个正常请求,确认服务可用:

curl -X POST http://localhost:8080/upload \ -F "file=@/tmp/test.txt"

第二步,构造恶意 payload。关键点有三个:无引号 filename、超长路径、结尾 %00。我写了一个 Python 脚本生成精确控制的二进制请求体:

import sys boundary = b'----WebKitFormBoundary7MA4YWxkTrZu0gW' payload = b'''--%s\r\nContent-Disposition: form-data; name="file"; filename=../../../../../../../../etc/passwd%00\r\n\r\n\x00\x00\x00\x00\r\n--%s--\r\n''' % (boundary, boundary) with open('poc.bin', 'wb') as f: f.write(payload)

生成的poc.bin文件大小只有 128 字节,但其中filename=后面的路径字符串长度为 47 字符,加上%00,足以触发解析器的越界扫描。第三步,用 curl 发送这个二进制文件,并开启 JVM GC 日志观察:

curl -X POST http://localhost:8080/upload \ -H "Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW" \ --data-binary "@poc.bin"

此时,你会看到 Tomcat 日志里没有报错,但jstat -gc <pid>显示 Eden 区在 2 秒内从 20MB 涨到 180MB,Old 区开始频繁 GC。再过 5 秒,进程直接 OOM 退出,日志里只有一行:

java.lang.OutOfMemoryError: Java heap space

没有堆栈,没有异常类名,这就是 CVE-2025-48976 最狡猾的地方:它不留下明显的攻击痕迹。它不像 SQL 注入那样在日志里留下SELECT * FROM users WHERE name='admin'--,也不像 XSS 那样在响应里回显<script>标签。它只是让 JVM 的内存管理器“发疯”,然后静悄悄地死掉。我在某省社保系统的渗透测试中,就是靠监控jstat输出的 GC 频率突增,才定位到这个漏洞的存在——他们的 WAF 日志里没有任何可疑请求,IDS 也毫无告警,但服务器每到上午 10 点(业务高峰期)就会规律性重启。

注意:不要在生产环境直接用上述 curl 命令测试!务必先在隔离的测试机上完成。因为一旦触发,服务会立即不可用,且 JVM 进程无法优雅关闭。更稳妥的复现方式是用 JMeter 写一个线程组,设置 5 个线程循环发送该 payload,观察 GC 日志变化,这样能更清晰地看到内存泄漏的渐进过程。

4. 修复不是换 jar 包那么简单,而是三道防线的协同作战

官方给出的修复方案是升级到commons-fileupload-1.6,这没错,但如果你以为把pom.xml里的 version 改成 1.6 就万事大吉,那离线上事故就不远了。我见过太多团队踩在这个坑里:他们升级了主 jar,却忽略了 classpath 里潜伏的旧版 transitive dependency;他们改了 pom,却没清理 Maven 本地仓库里被污染的 jar;他们重启了应用,却没验证ClassLoader是否真的加载了新版本。CVE-2025-48976 的修复,必须是“编译期—打包期—运行期”三道防线的协同。

第一道防线:编译期依赖树净化。执行mvn dependency:tree | grep fileupload,确保输出里只有一行org.apache.commons:commons-fileupload:jar:1.6:compile。如果看到compile下面还挂着runtimetest节点的 1.5 版本,说明有其他依赖(比如struts2-corespring-webmvc的某个老版本)偷偷引入了旧版。这时不能简单exclusion,因为exclusion可能导致NoSuchMethodError。正确做法是:用mvn dependency:analyze-duplicate找出所有重复引入的坐标,然后在dependencyManagement里统一锁定版本:

<dependencyManagement> <dependencies> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-fileupload</artifactId> <version>1.6</version> </dependency> </dependencies> </dependencyManagement>

第二道防线:打包期 jar 校验。Spring Boot 的mvn spring-boot:repackage会把所有依赖打成 fat jar。你需要解压生成的target/*.jar,进入BOOT-INF/lib/目录,用ls -la | grep fileupload确认里面只有一个commons-fileupload-1.6.jar。更进一步,用jar -tvf commons-fileupload-1.6.jar | grep parseFileName,检查FileUploadBase.class的字节码里是否包含if (fileName.length() > 255) { fileName = fileName.substring(0, 255); }这行补丁逻辑(这是 1.6 版本的核心修复点)。我曾经在一个金融客户的项目里,发现他们 CI 流水线里有一个mvn clean install -Dmaven.test.skip=true的步骤,而clean会清掉本地仓库,导致dependencyManagement的锁定失效,最终打包进去了一个 1.5 版本的 jar——这个 bug 在 UAT 环境跑了两周才被发现。

第三道防线:运行期 class 加载验证。这是最容易被忽视,也是最关键的一环。启动应用后,用jcmd <pid> VM.native_memory summary查看 native memory 分配,再用jcmd <pid> VM.class_hierarchy | grep FileUploadBase确认类加载路径。但最直接的办法是:在FileUploadBase.parseRequest()方法入口处加一个System.out.println("Loaded from: " + FileUploadBase.class.getProtectionDomain().getCodeSource().getLocation());,然后看控制台输出的 jar 路径是不是你预期的commons-fileupload-1.6.jar。如果输出的是file:/opt/app/lib/commons-fileupload-1.5.jar,说明 classpath 里还有幽灵 jar 在作祟。

提示:对于无法立即升级的遗留系统(比如某些强耦合 Struts2 的政府系统),有一个临时缓解方案:在web.xml里配置MultipartConfigElement,将maxFileSizemaxRequestSize都设为极小值(如 1KB),并配合 Nginx 层的client_max_body_size 1k;。虽然这会牺牲正常业务,但它能 100% 拦截 CVE-2025-48976 的利用载荷——因为攻击 payload 必须包含足够长的路径字符串才能触发越界,而 1KB 的限制会让请求在到达 Tomcat 的 Servlet 容器之前就被 Nginx 拒绝。这是一种“宁可错杀一千,不可放过一个”的保守策略,适用于安全红线极高的场景。

5. 从这个漏洞看 Java Web 安全的“信任边界”正在坍塌

CVE-2025-48976 给我的最大触动,不是它多难利用,而是它暴露了 Java Web 开发中一个被长期忽视的“信任边界”问题。过去十年,我们的安全焦点几乎全部集中在“业务逻辑层”:SQL 注入防不胜防,所以有了 MyBatis 的#{};XSS 如影随形,所以有了 Thymeleaf 的自动转义;反序列化风险巨大,所以有了 Jackson 的enableDefaultTyping(false)。我们花了大量精力加固这些“应用层”的大门,却忘了在大门之外,还有一个更底层、更基础的“协议解析层”——HTTP 请求体的解析。而这一层,恰恰是由commons-fileuploadtomcat-coyotenetty-http这些基础库默默承担的。

这个漏洞的根因,本质上是一种“信任错位”:FileUpload 信任 RFC 2045,Tomcat 信任 FileUpload,Spring 信任 Tomcat,最终开发者信任整个链条。但 RFC 2045 是一个“理想协议”,它假设所有客户端都严格遵守规范;而现实世界里,攻击者的第一要务就是打破规范。当 FileUpload 把“双引号缺失”当作一个需要容错的边缘 case,而不是一个必须拒绝的非法输入时,它就已经把信任边界划错了位置。正确的边界应该在“协议解析完成之后、业务逻辑开始之前”——也就是说,任何不符合 RFC 的请求,在解析阶段就应该被400 Bad Request拒绝,而不是被“尽力而为”地解析,再交给业务层去头疼。

这种边界坍塌的趋势,在近年的 Java 漏洞中越来越明显。比如 CVE-2023-42793(Jackson 的@JsonCreator反序列化绕过),它的利用链也是从 JSON 解析器的JsonParser开始,一路穿透到业务对象的构造方法;再比如 CVE-2022-22965(Spring Core 的 “Spring4Shell”),其本质是DataBinder在绑定请求参数时,对class.*这种特殊属性名的处理逻辑存在盲区。它们的共同点是:攻击面不再局限于@RequestMapping方法体内的代码,而是下沉到了框架最基础的“数据绑定”和“协议解析”模块。这意味着,一个 Java 工程师的安全能力,不能再只停留在“怎么写 secure code”,而必须延伸到“怎么理解框架如何解析 HTTP、JSON、XML”。

我在给某大型电商做安全培训时,专门设计了一个实验:让学员用curl -v发送一个Content-Type: multipart/form-data; boundary=xxx但 body 里根本没有--xxxboundary 的请求,观察 Tomcat 的响应。90% 的学员预期会看到400,但实际得到的是500 Internal Server Error,并且日志里爆出IllegalStateException: No multipart boundary found。这说明,连 Tomcat 自己的 multipart 解析器,在面对“协议层面的故意破坏”时,都没有做到优雅降级。CVE-2025-48976 只是把这个事实,用一种更剧烈的方式摆在了我们面前。

所以,如果你今天只做了一件事,那就是打开你的pom.xmlbuild.gradle,搜索commons-fileupload,确认版本 >= 1.6,并用jcmd验证运行时加载的确实是这个版本——这已经比很多团队做得更好了。但如果你想走得更远,建议把org.apache.commons.fileupload的源码下载下来,花一小时读一遍FileUploadBase.javaparseRequest()方法,重点关注它对Content-TypeContent-DispositionContent-Transfer-Encoding这三个头字段的解析逻辑。你会发现,那些看似枯燥的indexOf('"')lastIndexOf(';')substring()调用,才是守护你系统安全的第一道,也是最后一道,真正的城墙。

http://www.jsqmd.com/news/881798/

相关文章:

  • 告别瞎猜!用DBSCAN和K-means搞定毫米波雷达点云聚类,附完整Matlab代码与数据集
  • CentOS 7最小化安装后,复制粘贴和网络配置的保姆级教程(附图形界面切换)
  • XGBoost处理缺失值:构建面向天文大数据的极冷矮星智能发现系统
  • 告别传统地形!用Unreal Engine的Voxel Plugin,5分钟打造一个可实时编辑的无限世界
  • 避坑指南:UE5多人联机时,玩家角色生成(Spawn)的5个常见错误与修复方法
  • 别再为Unity视频播放发愁了!Video Player从创建到避坑,保姆级教程带你搞定
  • 基于退火序贯蒙特卡洛的符号回归:从高维物理数据中自动发现多项式约束
  • (干货整理)实测好用的AI写作辅助网站,毕业党收藏备用
  • SSD健康预测:BiGRU-MHA混合模型技术解析
  • 告别传统地形!用Unreal Engine的Voxel Plugin手把手教你做可破坏的无限世界(含动态NavMesh配置)
  • Unity新手避坑指南:从预制体变体到导航网格,这些基础概念别再搞混了
  • 基于Wi-Fi CSI与LSTM的非接触式心肺监测系统PulseFi详解
  • GameFramework资源管理实战:从Resource Editor配置到ProcedureLaunch初始化的完整代码解析
  • UE5多人联机开发:从大厅到游戏,如何让玩家带着自定义名字‘出生’?
  • 告别卡顿!用IL2CPP优化你的Unity游戏:性能提升与包体瘦身实测
  • 《AI推理优化实战:从高延迟高成本到高效低耗,企业级AI落地必备技术》
  • 模块化触觉显示系统:个性化人机交互的硬件与算法创新
  • 流式处理与可解释AI:构建实时电竞胜率预测系统的核心技术
  • UE5 RPG实战:告别旧输入系统,用增强输入(Enhanced Input)优雅触发你的技能
  • UE4.27 + PICO 3 避坑实录:从Android环境配置到VR插件集成的完整流程
  • 不止于切换:用Unity和PICO4 SDK打造一个可交互的VR场景导航菜单
  • Unity 2D游戏地图制作:从零上手Tile Palette的7个核心工具(附快捷键清单)
  • Unity无边框窗口保任务栏与Alt+Tab的Windows API方案
  • 别再死记硬背了!用‘橡皮筋’和‘电线杆’比喻,5分钟彻底搞懂Unity UI锚点(Anchors)
  • 用Unity做个会走会看的小人:手把手实现角色控制与反向动力学(IK)动画
  • 别再手动拖拽了!用Unity XR Interaction Toolkit + PICO4 SDK,5分钟搞定VR场景切换UI
  • 2026年智己LS8与问界M7深度分析:家庭增程SUV场景的配置与性能代差困境 - 品牌推荐
  • Unity新手避坑指南:从零搭建第一个3D场景,这些基础概念千万别搞错
  • 避坑指南:用Unity给PICO4打包APK时,SDK配置与场景管理的那些‘坑’
  • 避开Unity TileMap新手坑:关于Tile Palette编辑模式的那个‘小星星’到底怎么用?