
Proof-of-Concept-Exploit für CVE-2026-47627, eine Path-Traversal-Schwachstelle im NVIDIA Triton Inference Server, die über Zip-Slip zu beliebigem Dateischreiben führt. Enthält detaillierte Analyse, Laborumgebung und Ein-Klick-Exploit-Skripte.
Vollständiges Writeup der Forschungsergebnisse & PoC in diesem Repo (Image
nvcr.io/nvidia/tritonserver:26.05-py3/server v2.69.0). Das Repo ist so eingerichtet, dass es immerexplicitist, damit der PoC ohne Neustart über gRPC funktioniert.
| Feld | Wert |
|---|---|
| CVE | CVE-2026-47627 |
| CNA | [email protected] |
| Veröffentlicht | 2026-08-18 (NVD Awaiting Analysis) |
| Bulletin | NV 5865 → https://github.com/NVIDIA/product-security/tree/main/2026/5865 |
| CVSS 3.1 | 9.8 KRITISCH AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H |
| CWE | CWE-22 Path Traversal |
| Betroffen | 0.0-26.05 (server v2.69.0 / core r26.05) |
| Behoben | 26.06 (server v2.70.0 / core r26.06) |
| Auswirkung | DoS (CNA-Beschreibung) — diese Forschung beweist Datei-Schreiben außerhalb von /models via Zip-Slip, was zu DoS/Ressourcenerschöpfung erweitert werden kann |
| Credit | Martin Brodeur |
Ehrlicher Hinweis: Die CNA-Beschreibung nennt nur
path traversal → DoSohne Details. Dieses Writeup rekonstruiert die Bug-Stelle über den Diffr26.05..r26.06und direkte Tests im Container26.05-py3.
Image: nvcr.io/nvidia/tritonserver:26.05-py3 (TRITON_VERSION 2.69.0)
Compose ist immer explicit (in diesem Repo bereits gepatcht):
# 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 sind ebenfalls bereits explicit.
Triton-Modi:
NONE (Standard-Upstream) → lädt einmal beim Start, POST /v2/repository/models/.../load → 503, Neustart erforderlich, um neue Modelle zu laden.POLL → scannt das Repo jede Sekunde, lädt automatisch, blockiert weiterhin explizites Laden.EXPLICIT (dieses Repo) → scannt nicht, öffnet aber gRPC load_model / POST /load → PoC ohne Neustart über gRPC.Ports:
| 8000 | HTTP REST | v2/health, v2/models, v2/repository |
| 8001 | gRPC | RepositoryModelLoad, ModelInfer |
| 8002 | Metriken | Prometheus |
Ausgangsmodelle:
model_repository/
├── echo_python/ (python backend, model.py)
│ └── 1/model.py
└── identity_onnx/ (onnxruntime, model.onnx ~1KB)
Schnellstart:
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 ohne canonicalize oder eine falsche rfind(parent,0)==0-Prüfung (Teiltreffer /models vs /models_evil) verwendet wird, kann ../ ausbrechen./dev/zero, /proc/self/mem ist oder das Tar ../../-Einträge enthält, die Systemdateien überschreiben → Hang / OOM / Crash.Drei Forschungsfragen (aus dem ersten Bericht):
model_name in gRPC RepositoryModelLoad, EXECUTION_ENV_PATH-Tar-Eintrag, file:-Override, TRITON_BATCH_STRATEGY_PATH.posixpath.join(repo, model_name) / temp_dir + "/" + file_name + IsChildPathEscapingParentPath.DoS über Ressourcenerschöpfung / Hang. In diesem Repo wird Datei-Schreiben nach /tmp/poc_marker nachgewiesen.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 bei Teiltreffer: "/models"-Präfix von "/models_evil/file" wird als inside betrachtet
// FIXED (r26.06):
canonical_child.find(canonical_parent,0)==0 &&
((child.size() > parent.size() && child[parent.size()]=='/') || child.size()==parent.size())
// → muss '/' nach dem Präfix sein, "/models_evil" ist jetzt korrekt outside
Verwendet in src/backend_model.cc:196 (TRITON_BATCH_STRATEGY_PATH) und src/model_repository_manager/model_repository_manager.cc:162 (file:).
src/model_repository_manager/model_repository_manager.cc:62 ValidateModelName — core#472/#481 (März 2026, bereits in 26.05, aber in 26.06 verschärft):
if (trimmed==".." || trimmed.find('/')!=npos) return INVALID_ARG "must not contain path traversal"
Das bewirkt, dass gRPC load_model('../tmp/pwn') → 400 INVALID_ARGUMENT in 26.05 (bereits blockiert). Aber %2f (encodiertes /) kommt durch, weil es kein literales / ist → 500-Literal-Poll.
python_be.cc:292 EXECUTION_ENV_PATH — e520f8c7 Test zipslip_test.py:
Tar wird ohne Prüfung auf .. / absolute Pfade extrahiert. Eintrag ../../poc_marker aus /models/poc_exploit/malicious_env.tar.gz wird nach /tmp/poc_marker extrahiert (außerhalb des Modellverzeichnisses). Fix in 26.06: Verwendung von ARCHIVE_EXTRACT_SECURE_NODOTDOT|NOABSOLUTEPATHS + Prüfung IsChildPathEscapingParentPath.
Repo server (r26.05..r26.06): nur sagemaker_server.cc:1027 (RE2::FullMatch-Prüfung) + http_server.cc:2440 (atoi→stoi), nicht der Pfad dieser CVE.
Fazit: Der model_name-Pfad über HTTP/gRPC ist bereits in 26.05 durch ValidateModelName blockiert, aber der Zip-Slip im Tar ist es nicht — genau das nutzt der PoC dieses Repos aus.
| Vektor | Payload | 26.05 Ergebnis | Betroffen? |
|---|---|---|---|
HTTP roh ../ | POST /v2/repository/models/../tmp/pwn/load | 404 (Routing normalisiert) | ❌ |
HTTP encodiert ..%2f | POST /v2/repository/models/..%2ftmp%2fpwn/load | 500 literal, Poll /models/..%2ftmp%2fpwn existiert nicht | ❌ (Fake-500, keine Traversal) |
gRPC roh ../tmp/pwn | load_model('../tmp/pwn') | 400 INVALID_ARGUMENT | ❌ (bereits blockiert) |
gRPC encodiert ..%2f | load_model('..%2ftmp%2fpwn') | 500 Literal-Poll | ❌ |
file:-Override ../models_evil | file:../models_evil/pwn | 400/500 je nach Teiltreffer | ⚠️ (abhängig vom IsChildPathEscapingParentPath-Bug, aber für model_name bereits durch ValidateModelName blockiert) |
EXECUTION_ENV_PATH-Tar ../../poc_marker | malicious_env.tar.gz-Eintrag ../../poc_marker | 200 + Datei in /tmp/poc_marker | ✅ VULN |
Die Analogie
500 vs 400im alten Writeup war falsch:500= Datei existiert nicht (Tür existiert nicht),400= durch Validierung abgelehnt (Tür ist verschlossen). Beides führt nicht hinein. Nur200+ Datei in/tmpist der Beweis.