教育目的のみ。 このラボは、安全で隔離された環境で実際の脆弱性を示すために存在します。自分が所有していないシステムに対して実行しないでください。意図的に壊れたフィルターコードを本番システムで再利用しないでください。
CVE-2023-24329 の自己完結型 Docker ラボです。この脆弱性は Python の urllib.parse.urlparse() におけるパーサーの差異であり、Python < 3.11.4 で URL スキームやホストのフィルターをバイパスすることを可能にします。
このラボでは、file:// URL と内部ホスト名を明示的にブロックする API が、先頭にスペースを付けることで騙されて自身のコンテナ内の /etc/passwd を読み取ったり、プライベートな内部サービスにアクセスしたりする様子を示します。さらに、同じエクスプロイトが修正済みの Python では失敗することを確認します。
Python の urlparse() と、その背後にある HTTP/ファイル取得関数は、先頭に空白を含む URL の扱いに関して一致していません。影響を受けるバージョンでは:
from urllib.parse import urlparse
urlparse(" file:///etc/passwd").scheme # → "" (空 — フィルターを通過)
urlparse(" file:///etc/passwd").hostname # → None (空 — フィルターを通過)
しかし、urllib.request.urlopen(" file:///etc/passwd") はスペースを取り除き、 をそのまま取得します。
file:///etc/passwdパーサーが見るものとフェッチャーが行うものの間のそのギャップ — それが脆弱性です。
Python 3.11.4 では、解析前に先頭の空白や制御文字を取り除くことでこの問題を修正し、ギャップを解消しました。
| ステップ | 何が表示されるか | 何を学ぶか |
|---|---|---|
| 1 — ベースライン | file:///etc/passwd → 403 blocked scheme | フィルターは一見妥当に見える |
| 2 — エクスプロイト | 同じ URL にスペースを追加 → 200 + /etc/passwd の内容と内部シークレット | 1 つのスペースでフィルター全体が無効になる |
| 3 — パッチ | 同じペイロードを Python 3.11.4 に送信 → 403 blocked | 修正済みの urlparse は先に空白を取り除くため、フィルターが正しく機能する |
隔離された Docker ブリッジネットワーク cve-lab-net 上の 4 つのサービス:
| サービス | Python バージョン | 役割 | ホストポート |
|---|---|---|---|
vulnerable-api | 3.11.3 | 単純な URL フィルターを持つターゲット API | 8000 |
fixed-api | 3.11.4 | 同じコード、修正済みインタプリタ | 8000 |
internal-service | 3.12 | 偽の内部メタデータエンドポイント | なし |
attacker | 3.12 | エクスプロイト実行ドライバ | なし |
internal-service にはホストポートのマッピングがありません。Docker ネットワーク内部からのみ到達可能で、実際の信頼境界を模擬しています。
git clone <repo-url>
cd CVE-2023-24329-lab
docker compose -f docker-compose.vulnerable.yml up --build -d
docker compose -f docker-compose.vulnerable.yml exec attacker python exploit.py baseline

docker compose -f docker-compose.vulnerable.yml exec attacker python exploit.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 exploit.py verify

docker compose -f docker-compose.fixed.yml down
CVE-2023-24329-lab/
├── docker-compose.vulnerable.yml # Python 3.11.3(影響あり)
├── docker-compose.fixed.yml # Python 3.11.4(修正済み)
├── vulnerable-api/
│ ├── app.py # 単純なフィルターを持つ Flask API
│ ├── requirements.txt
│ └── Dockerfile
├── internal-service/
│ ├── app.py # 偽の内部メタデータエンドポイント
│ ├── requirements.txt
│ └── Dockerfile
└── attacker/
├── exploit.py # デモドライバ(baseline / exploit / verify)
├── requirements.txt
└── Dockerfile
脆弱な API と修正済みの API は同一のソースコードを共有しており、ベースイメージの Python バージョンだけが異なります。これがこのラボの重要な科学的対照特性です。
脆弱な API のフィルター(簡略化):
parsed = urllib.parse.urlparse(url)
if parsed.scheme.lower() in {"file", "gopher", "ftp", "data"}:
return 403 # ブロック
if parsed.hostname in {"localhost", "127.0.0.1", "internal-service"}:
return 403 # ブロック
urllib.request.urlopen(url) # 元の未変更の文字列を取得
バイパスペイロードは先頭の 1 つのスペースです:
file:///etc/passwd
^
スペース(0x20)
Python ≤ 3.11.3 では、urlparse は空のスキームとホスト名なしと解釈するため、フィルターを通過します。urlopen はスペースを取り除き、file:///etc/passwd を取得します。
Python ≥ 3.11.4 では、urlparse が先にスペースを取り除くため、正しく scheme=file と認識し、フィルターが 403 でブロックします。
CPython issue #102153 — 修正では、解析前に URL の先頭から C0 制御文字とスペースを取り除きます。パッチ適用後はパーサーとフェッチャーの両方が同じ URL を認識するため、この方法ではフィルターをバイパスできなくなります。
Python のバージョンにかかわらず正しい防御パターン:
# 解析 → 各部分から再構築 → 再構築した URL を後続に渡す。
# フィルターとフェッチャーの両方が同じ文字列に対して動作する。
parsed = urllib.parse.urlparse(url)
safe_url = parsed.geturl() # 構成要素から再構築
urllib.request.urlopen(safe_url)
${jndi:...} バイパスなどに応用されています。internal-service のポートをホストに公開しないでください。vulnerable-api/app.py のフィルターコードは教育目的で意図的に壊されています — 実際のシステムにコピーしないでください。MIT — 教育目的での使用、共有、改変は自由です。クレジットを明記してください。