URI解析链语义鸿沟-深度解析
URI解析链语义鸿沟 - 深度解析
一次 CTF 实战的完整复盘:为什么”同一个 URI 在不同组件中被解释为不同含义”会成为高级 Web 手的分水岭。
题目设定
- 技术栈:Jetty 12 + Spring Boot(嵌入式)
- 目标:读取
/WEB-INF/flag.txt - 阻碍:① Jetty WEB-INF 保护 ② 自定义 ErrorController 吞掉所有异常
考点 1:WEB-INF 目录保护机制
为什么要有 WEB-INF?
这是 Java Servlet 规范规定的特殊目录,存放服务端资源:
1 | WEB-INF/ |
核心规则:WEB-INF 下的文件只能由服务端代码访问(ServletContext.getResource()、JSP 转发等),任何 HTTP 外部请求都不能直接命中。
在 Jetty 里它怎么实现的?
关键在 ContextHandler.getResource():
1 | public Resource getResource(String path) { |
为什么直接访问返回 500 而不是 404?
getResource() 返回 null 后,Jetty 把它当作内部错误(而不是”找不到”),于是走了 500 → Spring 的 ErrorController → 返回 {"status":999,"error":"None"}。
关键认知:你看到的 500 不是应用代码抛的,是 Servlet 容器安全层抛的。
考点 2:CVE-2026-8384 — 状态机缺陷
什么是”状态机”?
URIUtil.canonicalPath() 是一个字符一个字符地扫描路径,用两个变量记录状态:
1 | StringBuilder canonical = new StringBuilder(); // 规范化后的输出 |
slash 的作用:判断 . 或 .. 是否应该被当作路径遍历。只有”紧跟在 / 后面”的 . 才是特殊段;在字母后面的 . 就是普通字符。
正常流程(无漏洞)
1 | 输入: /x/../WEB-INF/flag.txt |
漏洞流程(; 介入)
1 | 输入: /x;/../WEB-INF/flag.txt |
代码层面的 BUG 位置
1 | case ';': |
一句话总结:case ';' 把 ../ 当参数吃掉后,追加了 / 却没更新 slash 标志,导致紧接着的 .. 逃过遍历检测。
触发条件
1 | ;[^/]*/. ← 正则描述 |
考点 3:URI 解析链语义鸿沟 ⭐ 核心
一个 URI 要过 6 道”翻译官”
1 | 浏览器发送: GET /x;/../WEB-INF/flag.txt HTTP/1.1 |
关键:每一层看到的都不一样
| 层 | 看到的路径 | 结果 |
|---|---|---|
| ③ canonicalPath | /x;/../WEB-INF/flag.txt |
因 BUG 未规范化 |
| ④ 安全检查 | 拿 ③ 的结果 → 不匹配 /WEB-INF/ |
放行 ✅ |
| ⑥ Spring | cleanPath 把 .. 消掉 → WEB-INF/flag.txt |
从 static/ 找 → 404 |
这就是”不同组件对同一 URI 的不同解释”:
- Jetty 安全层看到的是脏路径(有
;、有未消化的..) - Spring 应用层看到的是干净路径(
..被解析) - 安全层放行了,应用层却找不到文件 → 404
这类漏洞的通用模式
1 | 攻击面 = | 安全层对路径的理解 - 应用层对路径的理解 | |
任何让这两层理解不一致的输入,都可能造成:
- 认证绕过(安全层认为没登录,应用层认为登录了)
- 越权访问(安全层认为无权,应用层给了权限)
- 文件读取(安全层认为路径安全,应用层读到了敏感文件)
可迁移的真实案例
| 场景 | 不一致点 |
|---|---|
| Nginx alias 配置错误 | location /laravel(无斜杠)+ alias /var/www/public/(有斜杠)→ /laravel../.env 能读到 .env |
| Spring 路由 vs 文件系统 | @GetMapping("/{path}") 匹配了 /a/b,但文件系统认为是另一个文件 |
| WAF vs 后端 | WAF 解码一次 %252e,后端解码两次 → .. 穿透 |
| 反代 vs 源站 | 反代规范化路径,源站不规范化 → 访问控制被绕过 |
考点 4:JAR vs Exploded — 为什么部署方式决定成败
两种部署方式
1 | JAR 模式: Exploded 模式: |
ClassLoader.getResource() 的行为差异
1 | // JAR 模式 |
JAR 的 entry 名是”死”的——它不经过 OS 文件系统,所以 .. 不会被解析。Exploded 走真实文件系统,.. 由内核处理。
实战意义
同一个 payload 在不同部署下结果完全不同:
1 | /x;/../WEB-INF/flag.txt |
这就是为什么 CTF 里有的题”网上说这样打能通,我却打不通”——先确认目标部署方式。
考点 5:ErrorController 信息隐藏 — 安全设计的”第二道墙”
题目里的设计
1 | // 自定义 ErrorController |
它为什么是”双重防护”
1 | 第一道: Jetty WEB-INF 检查 → 拦截直接访问(500) |
效果:
- 无法区分文件存在与否:
/WEB-INF/flag.txt和/WEB-INF/nonexistent返回完全相同的响应 → 不能做存在性探测(oracle) - HTML 模式 Content-Length: 0:连 Spring 默认的 Whitelabel 错误页都不给 → 彻底隐藏
攻击者的应对思路
这种”信息隐藏”设计逼迫攻击者走应用逻辑本身(本题就是绕过第一道墙后直接读文件),而不是靠错误信息猜路径。
总结:这一题教会你的”思维框架”
1 | 拿到一个 Web 题 → |
这道题难不在漏洞利用本身,而在于”理解整个处理管道”。能把”一个 URI 从网线到文件的旅程”讲清楚的人,才是真正懂 Web 安全的人。
参考
- CVE-2026-8384:Jetty 12 路径参数规范化漏洞(
URIUtil.canonicalPath()状态机缺陷) - 受影响版本:Jetty 12.0.0–12.0.34 / 12.1.0–12.1.8
- 修复版本:12.0.35 / 12.1.9