Skip to content
KitploitKITPLOIT
ToolsBlog
Submit
ToolsBlog
Submit

Hacking, PenTest, and Cybersecurity Tools for Your Security Arsenal!

Kitploit is a directory of hacking, cybersecurity, and pentesting tools. Discover the latest project updates to find vulnerabilities, analyze systems, automate testing, and strengthen your security.

··Feeds·Contact·Privacy·© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
Tools/GitHubGitHub/jithinodattu/cve-2023-24329-lab
Vulnerability AnalysisExploitationWeb SecurityCTFPenetration TestingLearning & EducationLabs & Practice
GitHubjithinodattu/cve-2023-24329-lab

CVE-2023-24329-lab

View Repository
4 months agoNot yet reviewed

Most Popular

View all →

Discover the most used tools by our community.

Explore all tools

Browse our collection of tools

View all tools →
Share

CVE-2023-24329 — Parser Differential Lab

Educational use only. This lab exists to demonstrate a real vulnerability in a safe, isolated environment. Never run this against systems you do not own. Never reuse the intentionally broken filter code in any production system.

A self-contained Docker lab demonstrating CVE-2023-24329 — a parser differential in Python's urllib.parse.urlparse() that allows bypass of URL scheme and host filters on Python < 3.11.4.

The lab shows an API that explicitly blocks file:// URLs and internal hostnames being tricked into reading /etc/passwd from its own container and hitting a private internal service — then proves the same exploit fails on patched Python.


The Vulnerability

Python's urlparse() and the underlying HTTP/file fetchers disagree on how to handle URLs with leading whitespace. On affected versions:

root@kitploit:~
from urllib.parse import urlparse

urlparse(" file:///etc/passwd").scheme   # → ""   (empty — filter passes)
urlparse(" file:///etc/passwd").hostname # → None (empty — filter passes)

But urllib.request.urlopen(" file:///etc/passwd") strips the space and fetches file:///etc/passwd anyway.

That gap between what the parser sees and what the fetcher does — that is the vulnerability.

Python 3.11.4 fixed this by stripping leading whitespace/control characters before parsing, closing the gap.


Demo — Three Beats


Architecture

Four services on an isolated Docker bridge network (cve-lab-net):

internal-service has no host port mapping — it is reachable only from inside the Docker network, simulating a real trust boundary.


Prerequisites

  • Docker Desktop (or Docker Engine + Compose plugin)
  • ~500 MB disk space for images

Quick Start

root@kitploit:~
git clone <repo-url>
cd CVE-2023-24329-lab

Beat 1 — Baseline: filter should hold

root@kitploit:~
docker compose -f docker-compose.vulnerable.yml up --build -d
docker compose -f docker-compose.vulnerable.yml exec attacker python exploit.py baseline

Beat 1 — baseline filter holds

Beat 2 — Exploit: bypass the filter

root@kitploit:~
docker compose -f docker-compose.vulnerable.yml exec attacker python exploit.py exploit

Beat 2 — bypass successful, /etc/passwd and internal secret exposed

Beat 3 — Patch: same payload, patched Python

root@kitploit:~
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

Beat 3 — patch holds on Python 3.11.4

Tear down

root@kitploit:~
docker compose -f docker-compose.fixed.yml down

Directory Layout

root@kitploit:~
CVE-2023-24329-lab/
├── docker-compose.vulnerable.yml   # Python 3.11.3 (affected)
├── docker-compose.fixed.yml        # Python 3.11.4 (patched)
├── vulnerable-api/
│   ├── app.py                      # Flask API with the naive filter
│   ├── requirements.txt
│   └── Dockerfile
├── internal-service/
│   ├── app.py                      # Fake internal metadata endpoint
│   ├── requirements.txt
│   └── Dockerfile
└── attacker/
    ├── exploit.py                  # Demo driver (baseline / exploit / verify)
    ├── requirements.txt
    └── Dockerfile

The vulnerable and fixed API services share the same source code — only the base image Python version differs. This is the key scientific-control property of the lab.


How the Bypass Works

The vulnerable API filter (simplified):

root@kitploit:~
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

The bypass payload is a single leading space:

root@kitploit:~
 file:///etc/passwd
^
space (0x20)

On Python ≤ 3.11.3, urlparse sees an empty scheme and no hostname → filter passes. urlopen strips the space → fetches file:///etc/passwd.

On Python ≥ 3.11.4, urlparse strips the space first → correctly sees scheme=file → filter blocks with 403.


The Fix (What Patched Python Does)

CPython issue #102153 — the fix strips C0 control characters and spaces from the start of the URL before parsing. After the patch both the parser and the fetcher agree on what the URL is, so the filter cannot be bypassed this way.

The correct defensive pattern regardless of Python version:

root@kitploit:~
# 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)

Key Teaching Points

  1. Parser differentials are a vulnerability class, not a one-off bug. The same idea drives HTTP request smuggling, SAML confusion attacks, and log4j's ${jndi:...} bypasses.
  2. Blocklists fail when the parser lies to you. Enumerating bad inputs is a losing game.
  3. Validate the reconstructed URL, not the raw input string.
  4. One minor version, one tiny patch, massive consequence. The CPython fix is a handful of lines.

References

  • NVD: CVE-2023-24329
  • CPython issue: github.com/python/cpython/issues/102153
  • Original disclosure: Yebo Cao — search "CVE-2023-24329 Yebo Cao"
  • Broader class: PortSwigger — SSRF filter bypass techniques
  • Broader class: James Kettle — HTTP Desync Attacks

Guardrails

  • Never expose internal-service ports to the host.
  • Never run this lab on a machine connected to a production network.
  • The filter code in vulnerable-api/app.py is deliberately broken for teaching purposes — do not copy it into any real system.

License

MIT — free to use, share, and adapt for educational purposes with attribution.

Download Tool
BeatWhat you seeWhat it teaches
1 — Baselinefile:///etc/passwd → 403 blocked schemeThe filter looks reasonable
2 — ExploitSame URL with a space prefix → 200 + contents of /etc/passwd and internal secretOne space defeats the entire filter
3 — PatchSame payload against Python 3.11.4 → 403 blockedPatched urlparse strips whitespace first; filter catches it correctly
ServicePython versionRoleHost port
vulnerable-api3.11.3Target API with naive URL filter8000
fixed-api3.11.4Same code, patched interpreter8000
internal-service3.12Fake internal metadata endpointnone
attacker3.12Exploit drivernone