CVE-2026-24880 — Apache Tomcat chunk 扩展字段请求走私¶
发布于 2026-06-06
已验证复现
本页记录了 CVE-2026-24880 的完整端到端复现过程。该漏洞利用在受控实验室环境中运行;请勿将其用于你未获授权的系统。
| 字段 | 值 |
|---|---|
| Project | Apache Tomcat |
| 受影响组件 | ChunkedInputFilter / coyote 模块(java/org/apache/coyote/http11/filters/ChunkedInputFilter.java) |
| Severity | HIGH |
| CVSS | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N(评分 7.5;NVD) |
| CWE | CWE-444 |
| 受影响版本 | 11.0.0-M1 – 11.0.18 · 10.1.0-M1 – 10.1.52 · 9.0.0.M1 – 9.0.115 · 8.5.0 – 8.5.100 · 7.0.0 – 7.0.109 |
| 修复版本 | 11.0.20 · 10.1.53 · 9.0.116(以及 8.5.x / 7.0.x 回溯补丁) |
| 安全公告 | GHSA-563x-q5rq-57qp |
1. 漏洞概述¶
Apache Tomcat 是广泛部署的 Java Servlet 容器,通过 ChunkedInputFilter 组件处理 HTTP/1.1 分块传输编码。CVE-2026-24880 是一个请求走私漏洞:若前端反向代理允许 HTTP chunk 扩展字段中的原始 CRLF 序列原样透传,攻击者便可在 Tomcat 当作无害扩展数据处理的内容中嵌入第二条隐藏请求。Tomcat 将该隐藏请求视为合法的新请求处理,跨越前端代理原本应执行的授权边界,进而访问受保护端点、绕过访问控制,或在同一 keep-alive 连接上污染后续请求的状态。
NVD 将本漏洞评级为 HIGH(CVSS 7.5)。Apache 官方评级为 Low,理由是利用前提是前端代理存在宽松配置。但只要该前提成立,攻击复杂度为 Low,且无需任何身份认证。
根本原因¶
文件: java/org/apache/coyote/http11/filters/ChunkedInputFilter.java
函数: parseChunkHeader() 和 skipChunkHeader()
修复前,解析器一旦在 chunk 头中遇到分号,就会设置一个布尔标志位(parsingExtension),此后接受所有后续字节作为扩展数据,不做任何结构性校验——仅有一个可选的大小上限:
// BEFORE (vulnerable)
} else if (chr == Constants.SEMI_COLON && !parsingExtension) {
parsingExtension = true;
extensionSize.incrementAndGet();
} else if (!parsingExtension) {
// hex digit processing
} else {
// Extension 'parsing'
// Note that the chunk-extension is neither parsed nor
// validated. Currently it is simply ignored.
long extSize = extensionSize.incrementAndGet();
if (maxExtensionSize > -1 && extSize > maxExtensionSize) {
throwBadRequestException(sm.getString("chunkedInputFilter.maxExtension"));
}
}
注释"neither parsed nor validated. Currently it is simply ignored"说明了问题所在:包括 \r\n 在内的任意字节均被当作扩展内容接收。配置宽松的代理可以转发一个 chunk 扩展字段,其中包含原始 \r\n 序列和完整的第二条 HTTP 请求;Tomcat 不加校验地读入这些字节,随后在请求处理循环中将流水线化的内层请求重新解析为独立的新请求。
修复方案(10.x/11.x 的主要提交 f07df938;9.x 的回溯补丁 1b586d6a/6d478dbe)将布尔标志替换为枚举字段 extensionState,并将所有字节级解析委托给新类 ChunkExtension,该类实现了符合 RFC 7230 规范的状态机:
// AFTER (fixed) — ChunkedInputFilter.java
- private volatile boolean parsingExtension = false;
+ private volatile State extensionState = null;
// On semicolon: enter validated extension parsing
} else if (chr == Constants.SEMI_COLON) {
extensionState = State.PRE_NAME;
long extSize = extensionSize.incrementAndGet();
...
}
// Per byte: delegate to ChunkExtension state machine
if (extensionState != null) {
extensionState = ChunkExtension.parse(chr, extensionState);
if (extensionState == State.CR) {
if (!parseCRLF()) { return false; }
eol = true;
extensionState = null;
} else { /* size check */ }
}
新的 ChunkExtension 类实现了流式状态机(状态包括:PRE_NAME、NAME、POST_NAME、EQUALS、VALUE、QUOTED_VALUE、POST_VALUE、CR),对任何不符合 RFC 语法的字节抛出 IOException。原始 \r 只有作为终止 \r\n 序列的起始字符时才合法(进入状态 CR);若 \n 出现在其他任何状态下则抛出异常,从而阻止通过 chunk 扩展字段进行 CRLF 注入。
2. 漏洞环境¶
实验室使用两对 Docker Compose 服务在私有桥接网络上复现该漏洞。所有环境文件可下载 env.zip,各文件也可通过下方链接单独浏览。
拓扑结构
| 服务 | 角色 | 镜像 | 对宿主机暴露 |
|---|---|---|---|
frontend-vuln |
CRLF 宽松字节转发代理(位于存在漏洞的后端前) | Python stdlib | 127.0.0.1:8000 |
backend-vuln |
存在漏洞的 Tomcat 9.0.115(9.x 最后一个受影响版本) | tomcat:9.0.115-jdk17-temurin |
仅内部可达 |
frontend-fixed |
相同代理(位于已修复后端前,用于对照实验) | Python stdlib | 127.0.0.1:8001 |
backend-fixed |
已修复的 Tomcat 9.0.116(9.x 第一个修复版本) | tomcat:9.0.116-jdk17-temurin |
仅内部可达 |
后端容器不对宿主机直接暴露,只能通过各自的前端代理访问,前端到后端的授权边界因此是真实成立的。前端代理(env/config/frontend_proxy.py)只解析外层请求头,随后将请求体以原始字节流转发给 Tomcat,不重新解析或重新分块;chunk 扩展字段中的原始 CRLF 会原样到达 Tomcat。代理执行唯一一项访问控制:路径以 /internal 开头的外层请求一律返回 403 Forbidden 并拒绝转发。
环境文件
- env/docker-compose.yml — 四个容器的 Compose 定义
- env/Dockerfile.backend — 后端镜像(部署
LabServlet,即记录请求到达的 Servlet) - env/Dockerfile.frontend — 前端代理镜像
- env/config/frontend_proxy.py — 字节转发代理源码
- env/config/backend-entrypoint.sh — 每次容器启动时生成新的 UUID boot nonce 并清空到达日志
- env/app/src/LabServlet.java — 提供 nonce 并记录请求到达的 Servlet
- env/app/WEB-INF/web.xml — Servlet 部署描述符
启动环境
验证环境为存在漏洞的版本
启动时,每个后端容器向 /nonce/boot_nonce 写入新的随机 UUID,并清空 /nonce/arrivals.log。该 nonce 也通过代理在 GET /public/nonce 端点提供。运行以下冒烟测试,确认四个容器正常运行、nonce 可访问,且 /internal 路径受到门控:
# 检查四个容器是否均在运行:
docker compose -f env/docker-compose.yml ps
# 通过特权通道直接从后端读取 boot nonce:
docker exec cve-2026-24880-backend-vuln cat /nonce/boot_nonce
# 通过前端代理读取同一 nonce(两者应一致):
curl -s http://127.0.0.1:8000/public/nonce
# 确认基线状态下到达日志为空:
docker exec cve-2026-24880-backend-vuln sh -c 'wc -c < /nonce/arrivals.log'
# 确认代理对外层 /internal 请求返回 403:
curl -s -o /dev/null -w '%{http_code}\n' "http://127.0.0.1:8000/internal/arrival?nonce=x"
预期结果:四个容器处于 Up/healthy 状态,docker exec 和 curl 返回的 UUID 一致,到达日志大小为 0,外层 /internal 探测返回 HTTP 403。各后端运行的具体构建可通过 Tomcat 版本字符串确认:
backend-vuln:Server version: Apache Tomcat/9.0.115(9.x 最后一个存在漏洞的版本)backend-fixed:Server version: Apache Tomcat/9.0.116(9.x 第一个修复版本)
3. 漏洞利用方法¶
漏洞利用脚本为单个 Python 文件(exploit/smuggle.py),可下载 exploit.zip。脚本仅使用 Python 标准库,无第三方依赖。
利用原理¶
脚本在同一连接上对前端代理执行两次顺序的原始 socket 操作:
-
读取 nonce — 连接前端代理,发送
GET /public/nonce HTTP/1.1,对响应进行反分块处理并记录 UUID。这是脚本在运行时获知 nonce 值的唯一方式;该值不会被硬编码。 -
走私内层请求 — 发送单个外层
POST /public/ingest请求,其 chunked 请求体的最后一个(大小为 0 的)chunk 携带不符合 RFC 规范的扩展字段(0;ext=/x)。在 chunked body 终止符之后,脚本在同一连接上立即写入内层请求:
字节转发前端将外层请求体原封不动地转发。在存在漏洞的 Tomcat 9.0.115 后端,ChunkedInputFilter.parseChunkHeader 不加校验地接收 ext=/x 字节;外层请求完成后,Tomcat 将流水线化的字节重新解析为独立的第二请求——GET /internal/arrival?nonce=<nonce>——后端对其处理并记录。在已修复的 Tomcat 9.0.116 后端,新的 ChunkExtension 状态机将扩展名称位置上的 / 字符识别为非法,返回 400 Bad Request,内层请求不会被解析。
分步操作说明¶
第 1 步: 确保环境已启动(参见第 2 节)。
第 2 步: 针对存在漏洞的通道运行利用脚本:
脚本将向 stderr 打印读取到的 nonce 及发送的精确 wire 字节,向 stdout 打印在该连接上从后端收到的原始字节。
第 3 步: 通过特权带外通道(独立于利用脚本自身输出)确认内层请求已被处理:
docker exec cve-2026-24880-backend-vuln cat /nonce/boot_nonce
docker exec cve-2026-24880-backend-vuln cat /nonce/arrivals.log
漏洞触发的证明¶
成功证据来自对后端内部状态的独立观测,而非利用脚本的 stdout 输出。
后端的 LabServlet 每次处理 GET /internal/arrival 请求时,都向 /nonce/arrivals.log 追加 ARRIVAL <nonce>。该日志只能由后端本身写入,利用脚本无法操纵。验证者通过 docker exec 分别读取日志和真实 boot nonce,核实日志中的 nonce 与当前 boot nonce 一致。
已验证运行中的实际观测证据:
# 真实 boot nonce(特权读取):
docker exec cve-2026-24880-backend-vuln cat /nonce/boot_nonce
-> af77f0a0-5e91-480e-94df-3768cab17ee1
# 利用脚本运行后的到达日志(特权读取):
docker exec cve-2026-24880-backend-vuln cat /nonce/arrivals.log
-> ARRIVAL af77f0a0-5e91-480e-94df-3768cab17ee1
到达日志中的 nonce af77f0a0-5e91-480e-94df-3768cab17ee1 与真实 boot nonce af77f0a0-5e91-480e-94df-3768cab17ee1 一致。在 wire 层面,脚本在同一连接上收到了两个 200 响应:第一个对应外层 POST /public/ingest(outer-ok),第二个对应走私的 GET /internal/arrival(recorded)。
前端代理的 403 门控确保 /internal/arrival 无法通过直接的外层请求到达(基线验证:curl 返回 403)。日志中的 ARRIVAL 条目因此只能由内层请求通过 chunk 扩展字段解析漏洞跨越前端→后端边界后写入。
对照实验(负控制)¶
对已修复通道运行相同脚本,Tomcat 9.0.116 返回 400 Bad Request,堆栈帧定位于 org.apache.coyote.http11.filters.ChunkedInputFilter.parseChunkHeader(ChunkedInputFilter.java:365),已修复后端的到达日志保持为空:
# 对端口 8001 运行后,已修复后端的到达日志:
docker exec cve-2026-24880-backend-fixed cat /nonce/arrivals.log
-> (empty, size 0)
已修复的 ChunkExtension 状态机拒绝了扩展字段中的非法 / 字节,内层请求从未被解析,到达日志保持为空。这将该利用明确归因于 chunk 扩展字段解析漏洞,而非其他 HTTP 流水线路径。
环境清理¶
完成后,停止并移除容器和网络:
4. 安全建议¶
修复方案¶
升级 Apache Tomcat 至已修复版本。修复已包含在以下版本中:
- 11.0.20 或更高版本(提交
fde1a823、2cb06c34) - 10.1.53 或更高版本(提交
f07df938、1e71441a) - 9.0.116 或更高版本(提交
1b586d6a、6d478dbe) - 8.5.x 和 7.0.x 也有回溯补丁——请参阅对应分支的 Apache 安全页面
注意:11.0.19 未作为包含此修复的版本公开发布;第一个公开可用的已修复 11.x 版本为 11.0.20。
缓解措施与临时方案¶
若无法立即升级,可在反向代理层采取以下措施:
- 在前端代理处拒绝或规范化 chunk 扩展字段。 配置 HAProxy、nginx 或其他代理,在转发至 Tomcat 之前剥除 chunk 扩展字段,或拒绝携带 chunk 扩展字段的请求。大多数生产代理配置默认不会在 chunk 扩展字段中透传原始 CRLF,这是目前可用的最有效短期缓解手段。
- 禁用代理与 Tomcat 之间的 HTTP/1.1 keep-alive 连接,可限制走私请求的利用价值,但不能完全消除攻击面。
根本原因在于 Tomcat 的 ChunkedInputFilter 接受 chunk 扩展字段中的任意字节序列。修复方案引入了严格符合 RFC 7230 规范的状态机,对不合规字节直接拒绝。在 Tomcat 层面,打补丁是唯一能根本解决问题的方法。
参考资料¶
- GHSA-563x-q5rq-57qp — 权威的受影响/修复版本信息、致谢、CWE
- NVD CVE-2026-24880 — CVSS 评分、CWE、漏洞描述
- Apache Tomcat 11 安全页面 — 11.0.20 修复
- Apache Tomcat 10 安全页面 — 10.1.53 修复
- Apache Tomcat 9 安全页面 — 9.0.116 修复
- 补丁提交 f07df938 — 10.x/11.x 主要修复:新增
ChunkExtension状态机 - 补丁提交 1e71441a — 跟进修复:处理仅含名称的扩展字段及非阻塞读取边界情况
- 补丁提交 1b586d6a — 9.x 主要回溯补丁
- 补丁提交 6d478dbe — 9.x 跟进回溯补丁
- 补丁提交 fde1a823 — 11.x 主要补丁
- 补丁提交 2cb06c34 — 11.x 跟进补丁
- OSS-Security 公告 2026-04-09 — 公开披露;致谢 Xclow3n 为报告者
- HeroDevs 漏洞目录 — 攻击机制描述
- Rapid7 漏洞数据库 — 补充背景信息