
Proof-of-concept exploit for CVE-2026-47627, a path traversal vulnerability in NVIDIA Triton Inference Server leading to arbitrary file write via Zip-Slip. Includes detailed analysis, lab environment, and one-click exploit scripts.
Writeup lengkap hasil riset & PoC di repo ini (image
nvcr.io/nvidia/tritonserver:26.05-py3/server v2.69.0). Repo di-setup untuk selaluexplicitagar PoC tanpa restart via gRPC.
| Field | Nilai |
|---|---|
| CVE | CVE-2026-47627 |
| CNA | [email protected] |
| Publish | 2026-08-18 (NVD Awaiting Analysis) |
| 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 Path Traversal |
| Affected | 0.0-26.05 (server v2.69.0 / core r26.05) |
| Fixed | 26.06 (server v2.70.0 / core r26.06) |
| Impact | DoS (deskripsi CNA) — riset ini membuktikan file write outside /models via Zip-Slip, yang bisa diperluas ke DoS/resource exhaustion |
| Credit | Martin Brodeur |
Catatan jujur: Deskripsi CNA hanya
path traversal → DoStanpa detail. Writeup ini merekonstruksi letak bug via diffr26.05..r26.06dan pengujian langsung di container26.05-py3.
Image: nvcr.io/nvidia/tritonserver:26.05-py3 (TRITON_VERSION 2.69.0)
Compose selalu explicit (sudah di-patch di repo ini):
# 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 juga sudah explicit.
Mode Triton:
NONE (default upstream) → load sekali pas start, POST /v2/repository/models/.../load → 503, harus restart untuk load model baru.POLL → scan repo tiap detik, auto-load, tetap block explicit load.EXPLICIT (repo ini) → tidak scan, tapi buka gRPC load_model / POST /load → PoC tanpa restart via gRPC.Port:
| 8000 | HTTP REST | v2/health, v2/models, v2/repository |
| 8001 | gRPC | RepositoryModelLoad, ModelInfer |
| 8002 | Metrics | Prometheus |
Model awal:
model_repository/
├── echo_python/ (python backend, model.py)
│ └── 1/model.py
└── identity_onnx/ (onnxruntime, model.onnx ~1KB)
Quickstart:
docker compose up -d
docker compose logs -f
py scripts/client_test.py # health + infer identity_onnx
User input (model_name / tar entry) ──► join(parent, child) ──► canonicalize ──► cek "child di dalam parent?" ──► buka file
│ │ │ │ │
└──► "../tmp/pwn" /models + "/../tmp/pwn" = /models/../tmp/pwn → /tmp/pwn rfind vs find + '/' check FileExists / extract
join tanpa canonicalize atau cek rfind(parent,0)==0 yang salah (partial-match /models vs /models_evil), maka ../ bisa escape./dev/zero, /proc/self/mem, atau tar berisi ../../ yang overwrite file sistem → hang / OOM / crash.Tiga pertanyaan riset (dari laporan awal):
model_name di gRPC RepositoryModelLoad, EXECUTION_ENV_PATH tar entry, file: override, TRITON_BATCH_STRATEGY_PATH.posixpath.join(repo, model_name) / temp_dir + "/" + file_name + IsChildPathEscapingParentPath.DoS via resource exhaustion / hang. Di repo ini dibuktikan file write ke /tmp/poc_marker.Repo core (r26.05..r26.06):
src/filesystem/api.cc:407 IsChildPathEscapingParentPath — fix dcb315d + 72f3d9b:
// VULN (r26.05):
absolute_child.rfind(absolute_parent,0) != 0
// → bug partial-match: "/models" prefix dari "/models_evil/file" dianggap inside
// FIXED (r26.06):
canonical_child.find(canonical_parent,0)==0 &&
((child.size() > parent.size() && child[parent.size()]=='/') || child.size()==parent.size())
// → harus '/' setelah prefix, "/models_evil" sekarang correctly outside
Dipakai di src/backend_model.cc:196 (TRITON_BATCH_STRATEGY_PATH) dan src/model_repository_manager/model_repository_manager.cc:162 (file:).
src/model_repository_manager/model_repository_manager.cc:62 ValidateModelName — core#472/#481 (Maret 2026, sudah di 26.05 tapi diperketat di 26.06):
if (trimmed==".." || trimmed.find('/')!=npos) return INVALID_ARG "must not contain path traversal"
Ini yang bikin gRPC load_model('../tmp/pwn') → 400 INVALID_ARGUMENT di 26.05 (sudah blocked). Tapi %2f (encoded /) lolos karena bukan / literal → 500 poll literal.
python_be.cc:292 EXECUTION_ENV_PATH — e520f8c7 test zipslip_test.py:
Tar diekstrak tanpa cek .. / absolute. Entry ../../poc_marker dari /models/poc_exploit/malicious_env.tar.gz diekstrak ke /tmp/poc_marker (di luar model dir). Fix di 26.06: pakai ARCHIVE_EXTRACT_SECURE_NODOTDOT|NOABSOLUTEPATHS + cek IsChildPathEscapingParentPath.
Repo server (r26.05..r26.06): hanya sagemaker_server.cc:1027 (RE2::FullMatch check) + http_server.cc:2440 (atoi→stoi), bukan jalur CVE ini.
Kesimpulan: Jalur model_name via HTTP/gRPC sudah di-block di 26.05 oleh ValidateModelName, tapi Zip-Slip tar belum — inilah yang dieksploitasi PoC repo ini.
| Vektor | Payload | 26.05 Hasil | Kena? |
|---|---|---|---|
HTTP raw ../ | POST /v2/repository/models/../tmp/pwn/load | 404 (routing normalize) | ❌ |
HTTP encoded ..%2f | POST /v2/repository/models/..%2ftmp%2fpwn/load | 500 literal, poll /models/..%2ftmp%2fpwn tidak ada | ❌ (500 fake, bukan traversal) |
gRPC raw ../tmp/pwn | load_model('../tmp/pwn') | 400 INVALID_ARGUMENT | ❌ (sudah di-block) |
gRPC encoded ..%2f | load_model('..%2ftmp%2fpwn') | 500 poll literal | ❌ |
file: override ../models_evil | file:../models_evil/pwn | 400/500 tergantung partial-match | ⚠️ (tergantung IsChildPathEscapingParentPath bug, tapi sudah di-block ValidateModelName untuk model_name) |
EXECUTION_ENV_PATH tar ../../poc_marker | malicious_env.tar.gz entry ../../poc_marker | 200 + file di /tmp/poc_marker | ✅ VULN |
Analogi
500 vs 400di writeup lama salah:500= file tidak ada (pintu tidak ada),400= ditolak validasi (pintu dikunci). Keduanya tidak masuk. Hanya200+ file di/tmpyang bukti.
Ide: Python backend parameter EXECUTION_ENV_PATH menunjuk ke $$TRITON_MODEL_DIRECTORY/malicious_env.tar.gz. Saat load_model('poc_exploit'), backend pb_env.cc:292 extract tar tanpa sanitasi. Entry ../../poc_marker escape dari .../poc_exploit/ ke /tmp/poc_marker.