
Автономная Docker-лаборатория, демонстрирующая CVE-2023-24329 — различие в парсере urllib языка Python, которое обходит фильтры схемы URL и хоста, с уязвимым и исправленным окружениями для практического обучения безопасности.
Только для образовательных целей. Эта лаборатория существует для демонстрации реальной уязвимости в безопасной изолированной среде. Никогда не запускайте это на системах, которые вам не принадлежат. Никогда не используйте намеренно сломанный код фильтра в любой производственной системе.
Автономная Docker-лаборатория, демонстрирующая CVE-2023-24329 — дифференциал парсера в urllib.parse.urlparse() Python, который позволяет обойти фильтры схемы URL и хоста на Python < 3.11.4.
Лаборатория показывает API, который явно блокирует URL file:// и внутренние имена хостов, но может быть обманут для чтения /etc/passwd из собственного контейнера и доступа к частному внутреннему сервису — а затем доказывает, что тот же эксплойт не работает на исправленном Python.
urlparse() в Python и нижележащие 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 и внутренний секрет | Один пробел обходит весь фильтр |
| 3 — Патч | Та же нагрузка на Python 3.11.4 → 403 blocked | Исправленный urlparse удаляет пробелы первым; фильтр корректно срабатывает |
Четыре сервиса в изолированной сети Docker bridge (cve-lab-net):
| Сервис | Версия Python | Роль | Хост-порт |
|---|---|---|---|
vulnerable-api | 3.11.3 | Целевой API с наивным фильтром URL | 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-сервисы используют один и тот же исходный код — различается только версия Python в базовом образе. Это ключевое свойство научного контроля лаборатории.
Упрощённый фильтр уязвимого API:
parsed = urllib.parse.urlparse(url)
if parsed.scheme.lower() in {"file", "gopher", "ftp", "data"}:
return 403 # blocked
if parsed.hostname in {"localhost", "127.0.0.1", "internal-service"}:
return 403 # blocked
urllib.request.urlopen(url) # fetch the original, unmodified string
Нагрузка обхода — это один ведущий пробел:
file:///etc/passwd
^
пробел (0x20)
В Python ≤ 3.11.3 urlparse видит пустую схему и отсутствие имени хоста → фильтр пропускает. urlopen удаляет пробел → загружает file:///etc/passwd.
В Python ≥ 3.11.4 urlparse сначала удаляет пробел → корректно видит scheme=file → фильтр блокирует с кодом 403.
CPython issue #102153 — исправление удаляет управляющие символы C0 и пробелы с начала URL перед синтаксическим анализом. После патча и парсер, и загрузчик согласуются в том, что представляет собой URL, поэтому фильтр не может быть обойдён таким образом.
Правильный защитный шаблон независимо от версии Python:
# Parse → reconstruct from parts → pass the rebuilt URL downstream.
# Both the filter and the fetcher then operate on the same string.
parsed = urllib.parse.urlparse(url)
safe_url = parsed.geturl() # rebuilt from components
urllib.request.urlopen(safe_url)
${jndi:...} в log4j.internal-service для хоста.vulnerable-api/app.py намеренно сломан для учебных целей — не копируйте его в любую реальную систему.MIT — можно свободно использовать, распространять и адаптировать в образовательных целях с указанием авторства.