
Solo a scopo didattico. Questo laboratorio esiste per dimostrare una vulnerabilità reale in un ambiente sicuro e isolato. Non eseguirlo mai contro sistemi che non possiedi. Non riutilizzare mai il codice di filtro volutamente difettoso in alcun sistema di produzione.
Un laboratorio Docker autonomo che dimostra CVE-2023-24329 — una differenza di parsing in urllib.parse.urlparse() di Python che consente di aggirare i filtri su schema URL e hostname su Python < 3.11.4.
Il laboratorio mostra un'API che blocca esplicitamente gli URL file:// e gli hostname interni, che viene ingannata per leggere /etc/passwd dal proprio container e per raggiungere un servizio interno privato — poi dimostra che lo stesso exploit fallisce su Python aggiornato.
L'implementazione di urlparse() di Python e i relativi fetcher HTTP/file non concordano su come gestire gli URL con spazi bianchi iniziali. Nelle versioni interessate:
from urllib.parse import urlparse
urlparse(" file:///etc/passwd").scheme # → "" (vuoto — il filtro passa)
urlparse(" file:///etc/passwd").hostname # → None (vuoto — il filtro passa)
Ma urllib.request.urlopen(" file:///etc/passwd") rimuove lo spazio e recupera comunque file:///etc/passwd.
Quel divario tra ciò che vede il parser e ciò che fa il fetcher — quella è la vulnerabilità.
Python 3.11.4 ha corretto il problema rimuovendo spazi bianchi e caratteri di controllo iniziali prima del parsing, chiudendo il divario.
Quattro servizi su una rete Docker bridge isolata (cve-lab-net):
internal-service non ha mapping di porte verso l'host — è raggiungibile solo dall'interno della rete Docker, simulando un vero confine di fiducia.
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 (vulnerabile)
├── docker-compose.fixed.yml # Python 3.11.4 (aggiornato)
├── vulnerable-api/
│ ├── app.py # API Flask con il filtro ingenuo
│ ├── requirements.txt
│ └── Dockerfile
├── internal-service/
│ ├── app.py # Endpoint fittizio di metadati interni
│ ├── requirements.txt
│ └── Dockerfile
└── attacker/
├── exploit.py # Driver demo (baseline / exploit / verify)
├── requirements.txt
└── Dockerfile
I servizi API vulnerabile e aggiornato condividono lo stesso codice sorgente — solo la versione di Python nell'immagine base differisce. Questa è la proprietà fondamentale di controllo scientifico del laboratorio.
Il filtro dell'API vulnerabile (semplificato):
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
Il payload di bypass è un singolo spazio iniziale:
file:///etc/passwd
^
spazio (0x20)
Su Python ≤ 3.11.3, urlparse vede uno schema vuoto e nessun hostname → il filtro passa. urlopen rimuove lo spazio → recupera file:///etc/passwd.
Su Python ≥ 3.11.4, urlparse rimuove prima lo spazio → vede correttamente scheme=file → il filtro blocca con 403.
Issue CPython issue #102153 — la correzione rimuove i caratteri di controllo C0 e gli spazi dall'inizio dell'URL prima del parsing. Dopo la patch, sia il parser sia il fetcher concordano su cosa sia l'URL, quindi il filtro non può essere aggirato in questo modo.
Il pattern difensivo corretto indipendentemente dalla versione di Python:
# Parse → ricostruisci dalle componenti → passa l'URL ricostruito a valle.
# Sia il filtro sia il fetcher operano quindi sulla stessa stringa.
parsed = urllib.parse.urlparse(url)
safe_url = parsed.geturl() # ricostruito dalle componenti
urllib.request.urlopen(safe_url)
${jndi:...}.internal-service verso l'host.vulnerable-api/app.py è volutamente difettoso per scopi didattici — non copiarlo in alcun sistema reale.MIT — libero di usare, condividere e adattare per scopi educativi con attribuzione.
| Prova | Cosa vedi | Cosa insegna |
|---|
| 1 — Baseline | file:///etc/passwd → 403 blocked scheme | Il filtro sembra ragionevole |
| 2 — Exploit | Stesso URL con uno spazio iniziale → 200 + contenuto di /etc/passwd e segreto interno | Un singolo spazio sconfigge l'intero filtro |
| 3 — Patch | Stesso payload contro Python 3.11.4 → 403 blocked | urlparse aggiornato rimuove gli spazi prima; il filtro lo intercetta correttamente |
| Servizio | Versione Python | Ruolo | Porta host |
|---|
vulnerable-api | 3.11.3 | API di destinazione con filtro URL ingenuo | 8000 |
fixed-api | 3.11.4 | Stesso codice, interprete aggiornato | 8000 |
internal-service | 3.12 | Endpoint fittizio di metadati interni | nessuna |
attacker | 3.12 | Driver dell'exploit | nessuna |