漏洞概述
漏洞名称:Jetty ConcatServlet 多重解码导致 WEB-INF 敏感信息泄露
漏洞编号:CVE-2021-28169
CVSS 评分:7.5
影响版本:
- Jetty 9.4.0 - 9.4.39
- Jetty 10.0.0 - 10.0.1
- Jetty 11.0.0 - 11.0.1
修复版本: - Jetty ≥ 9.4.40
- Jetty ≥ 10.0.2
- Jetty ≥ 11.0.2
漏洞类型:路径遍历/信息泄露
CVE-2021-28169 是 Eclipse Jetty 的 Servlets 组件(ConcatServlet
和 WelcomeFilter
)中的安全漏洞。当开发者主动使用这些组件时,攻击者通过构造双重URL编码的路径(如 /%2557EB-INF/web.xml
),利用多重解码逻辑缺陷绕过路径安全校验,直接访问 WEB-INF
或 META-INF
目录下的敏感文件(如 web.xml
、classes
等),导致应用配置、源码和凭证信息泄露。
技术细节与源码分析
漏洞成因
- 多重解码缺陷:
ConcatServlet
在处理请求参数时执行多次 URL 解码:- 容器层自动解码(如
%25
→%
) ConcatServlet
自身再次解码(如%57
→W
)
- 容器层自动解码(如
- 安全校验滞后:路径校验在首次解码后执行,未考虑二次解码结果。
关键源码分析
(1)请求处理入口(ConcatServlet.doGet()
)
代码定位:org.eclipse.jetty.servlets.ConcatServlet#doGet
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {String query = request.getQueryString();// 获取原始查询字符串(未解码)if (query == null) {response.sendError(204);return;} List<RequestDispatcher> dispatchers = new ArrayList<>();String[] parts = query.split("\\&");String type = null;for (String part : parts) {String path = URIUtil.canonicalPath(URIUtil.decodePath(part));// 漏洞点:第一次解码if (path == null) {response.sendError(404);return;} // 安全校验(易被绕过)if (startsWith(path, "/WEB-INF/") || startsWith(path, "/META-INF/")) {//response.sendError(404);return;} String t = getServletContext().getMimeType(path);if (t != null){if (type == null) {type = t;}else if (!type.equals(t)) {response.sendError(415);return;} }RequestDispatcher dispatcher = getServletContext().getRequestDispatcher(path);if (dispatcher != null) {dispatchers.add(dispatcher);}} if (type != null) {response.setContentType(type);}for (RequestDispatcher dispatcher : dispatchers){//容器自动二次解码dispatcher.include((ServletRequest)request, (ServletResponse)response);}}
String path = URIUtil.canonicalPath(URIUtil.decodePath(part));
问题分析:
URIUtil.decodePath(part)
:对参数执行首次 URL 解码(如%2557
→%57
)URIUtil.canonicalPath()
:路径规范化(但无法处理编码字符)- 容器自动二次解码:当
RequestDispatcher
处理路径时,对%57
再次解码为W
安全校验缺陷
if (startsWith(path, "/WEB-INF/") || ... )
问题分析:
- 校验时路径为首次解码结果(如
/%57EB-INF/web.xml
) - 未匹配
/WEB-INF/
规则 → 错误放行 - 未考虑二次解码后的真实路径(
/WEB-INF/web.xml
)
漏洞利用原理
攻击请求:
GET /static?/%2557EB-INF/web.xml HTTP/1.1
解码流程:
步骤 | 操作 | 路径变化 | 安全校验结果 |
---|---|---|---|
1. 容器自动解码 | %2557 → %57 | /%57EB-INF/web.xml | 绕过(不匹配 /WEB-INF ) |
2. URIUtil.decodePath() | %57 → W | /WEB-INF/web.xml | 未校验 |
3. RequestDispatcher | 实际访问文件 | /WEB-INF/web.xml | 敏感文件泄露 |
漏洞复现
环境搭建
1.使用 Vulhub 环境启动漏洞靶机
docker-compose up -d
2.访问访问 http://target:8080,确认服务正常运行
攻击步骤
1.正常通过/static?/WEB-INF/web.xml
无法访问到敏感文件web.xml:
2.对字母W
进行双URL编码,即可绕过限制访问web.xml:
http://your-ip:8080/static?/%2557EB-INF/web.xml
修复方案
方案1:移除二次解码(官方修复)
// 修改前(漏洞代码)
String path = URIUtil.canonicalPath(URIUtil.decodePath(part));// 修改后(修复代码)
String path = URIUtil.canonicalPath(part); // 仅规范化,不额外解码
修复效果:
- 保持路径为单次解码状态(如
/%57EB-INF
) - 安全校验可正确拦截非常规路径
方案2:增强安全校验
// 增加双重解码后校验
String decodedPath = URIUtil.decodePath(URIUtil.decodePath(part));
String canonicalPath = URIUtil.canonicalPath(decodedPath);if (startsWith(canonicalPath, "/WEB-INF/") || startsWith(canonicalPath, "/META-INF/")) {response.sendError(404);return;
}
优势:兼容旧版,但性能较低
方案3:输入过滤
// 拦截包含双重编码的请求
if (part.contains("%25")) { // 检测 % 的编码形式 %25response.sendError(400, "Malicious path detected");return;
}
修复验证对比
请求 | 修复前结果 | 修复后结果 |
---|---|---|
/?/css/style.css | 正常返回 | 正常返回 |
/?/%57EB-INF/web.xml | 绕过校验,文件泄露 | 拦截(路径未规范化) |
/?/%2557EB-INF/web.xml | 文件泄露 | 拦截(检测到 %25 ) |
漏洞启示
- 解码一致性原则:各层组件应统一解码策略,避免多重解码产生语义差异。
- 深度防御实践:敏感目录访问需在解码后、规范化后、业务逻辑前多层校验。
- 组件最小化:未使用的功能组件(如
ConcatServlet
)应及时禁用。
参考链接
- CVE-2021-28169 官方通告(GitHub Advisory)
- Vulhub 漏洞复现环境指南
- 企业级防御实践(Tenable 报告)
附:多重解码漏洞模式对比
漏洞 触发方式 防护关键点 CVE-2021-28164 单次编码绕过( %2e
)先解码后规范化 CVE-2021-28169 双重编码绕过( %2557
)避免多次解码