
Selbstständiges Docker-Lab, das CVE-2023-24329 demonstriert, einen Python-urllib-Parser-Differential, der URL-Schema- und Host-Filter umgeht, mit verwundbaren und gepatchten Umgebungen für praxisorientierte Sicherheitsschulungen.
Nur zu Bildungszwecken. Dieses Labor existiert, um eine echte Schwachstelle in einer sicheren, isolierten Umgebung zu demonstrieren. Führen Sie dies niemals gegen Systeme aus, die Ihnen nicht gehören. Verwenden Sie den absichtlich defekten Filtercode niemals in einem Produktionssystem.
Ein in sich geschlossenes Docker-Labor, das CVE-2023-24329 demonstriert – ein Parser-Differential in Pythons urllib.parse.urlparse(), das die Umgehung von URL-Schema- und Host-Filtern unter Python < 3.11.4 ermöglicht.
Das Labor zeigt eine API, die explizit file://-URLs und interne Hostnamen blockiert, die dazu verleitet wird, /etc/passwd aus ihrem eigenen Container zu lesen und einen privaten internen Dienst zu treffen – und beweist dann, dass derselbe Exploit auf gepatchtem Python fehlschlägt.
Pythons urlparse() und die zugrundeliegenden HTTP-/Datei-Abrufer sind sich uneinig, wie URLs mit führenden Leerzeichen zu behandeln sind. In betroffenen Versionen:
from urllib.parse import urlparse
urlparse(" file:///etc/passwd").scheme # → "" (leer — Filter besteht)
urlparse(" file:///etc/passwd").hostname # → None (leer — Filter besteht)
Aber urllib.request.urlopen(" file:///etc/passwd") entfernt das Leerzeichen und ruft trotzdem file:///etc/passwd ab.
Diese Lücke zwischen dem, was der Parser sieht, und dem, was der Abrufer tut – das ist die Schwachstelle.
Python 3.11.4 hat dies behoben, indem vor dem Parsen führende Leerzeichen/Steuerzeichen entfernt werden, wodurch die Lücke geschlossen wird.
| Durchlauf | Was Sie sehen | Was es lehrt |
|---|---|---|
| 1 – Basislinie | file:///etc/passwd → 403 blocked scheme | Der Filter scheint vernünftig |
| 2 – Exploit | Gleiche URL mit Leerzeichen-Präfix → 200 + Inhalt von /etc/passwd und internes Geheimnis | Ein einziges Leerzeichen besiegt den gesamten Filter |
| 3 – Patch | Gleiche Nutzlast gegen Python 3.11.4 → 403 blocked | Gepatchtes urlparse entfernt zuerst Leerzeichen; Filter fängt es korrekt ab |
Vier Dienste in einem isolierten Docker-Bridge-Netzwerk (cve-lab-net):
| Dienst | Python-Version | Rolle | Host-Port |
|---|---|---|---|
vulnerable-api | 3.11.3 | Ziel-API mit naivem URL-Filter | 8000 |
fixed-api | 3.11.4 | Gleicher Code, gepatchter Interpreter | 8000 |
internal-service | 3.12 | Gefälschter interner Metadaten-Endpunkt | none |
attacker | 3.12 | Exploit-Treiber | none |
internal-service hat keine Host-Port-Zuordnung – es ist nur von innerhalb des Docker-Netzwerks erreichbar und simuliert eine echte Vertrauensgrenze.
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 (betroffen)
├── docker-compose.fixed.yml # Python 3.11.4 (gepatcht)
├── vulnerable-api/
│ ├── app.py # Flask-API mit dem naiven Filter
│ ├── requirements.txt
│ └── Dockerfile
├── internal-service/
│ ├── app.py # Gefälschter interner Metadaten-Endpunkt
│ ├── requirements.txt
│ └── Dockerfile
└── attacker/
├── exploit.py # Demo-Treiber (baseline / exploit / verify)
├── requirements.txt
└── Dockerfile
Die anfälligen und gepatchten API-Dienste teilen sich den gleichen Quellcode – nur die Basis-Image-Python-Version unterscheidet sich. Dies ist die wichtigste wissenschaftliche Kontrolleigenschaft des Labors.
Der anfällige API-Filter (vereinfacht):
parsed = urllib.parse.urlparse(url)
if parsed.scheme.lower() in {"file", "gopher", "ftp", "data"}:
return 403 # blockiert
if parsed.hostname in {"localhost", "127.0.0.1", "internal-service"}:
return 403 # blockiert
urllib.request.urlopen(url) # ruft die ursprüngliche, unveränderte Zeichenkette ab
Die Umgehungsnutzlast ist ein einziges führendes Leerzeichen:
file:///etc/passwd
^
Leerzeichen (0x20)
Auf Python ≤ 3.11.3 sieht urlparse ein leeres Schema und keinen Hostnamen → Filter besteht. urlopen entfernt das Leerzeichen → ruft file:///etc/passwd ab.
Auf Python ≥ 3.11.4 entfernt urlparse zuerst das Leerzeichen → erkennt korrekt scheme=file → Filter blockiert mit 403.
CPython Issue #102153 – der Fix entfernt C0-Steuerzeichen und Leerzeichen vom Anfang der URL vor dem Parsen. Nach dem Patch stimmen Parser und Abrufer darin überein, was die URL ist, sodass der Filter auf diese Weise nicht umgangen werden kann.
Das korrekte defensive Muster unabhängig von der Python-Version:
# Parsen → aus Teilen neu aufbauen → die neu aufgebaute URL an nachgelagerte Stellen übergeben.
# Sowohl der Filter als auch der Abrufer arbeiten dann mit derselben Zeichenkette.
parsed = urllib.parse.urlparse(url)
safe_url = parsed.geturl() # aus Komponenten neu aufgebaut
urllib.request.urlopen(safe_url)
${jndi:...}-Umgehungen von log4j an.internal-service für den Host frei.vulnerable-api/app.py ist absichtlich zu Lehrzwecken defekt – kopieren Sie ihn nicht in ein echtes System.