**仅限教育用途。**此实验室旨在展示一个真实漏洞,并提供一个安全隔离的环境。切勿对不属于自己的系统运行此实验。切勿在任何生产系统中重复使用存在有意性缺陷的过滤器代码。
一个自包含的 Docker 实验室,演示 CVE-2023-24329 — Python urllib.parse.urlparse() 中的解析器差异,该差异允许在 Python < 3.11.4 上绕过 URL scheme 和主机过滤器。
该实验室展示了一个 API,它明确阻止 file:// URL 和内部主机名,但被诱骗读取自身容器中的 /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 通过在进行解析之前去除前导空白/控制字符来修复此问题,从而消除了这一差异。
四个服务运行在一个隔离的 Docker 桥接网络 (cve-lab-net) 上:
internal-service 没有主机端口映射 — 它只能从 Docker 网络内部访问,模拟了一个真实的信任边界。
git clone <仓库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 # 演示驱动程序(基线/利用/验证)
├── requirements.txt
└── Dockerfile
易受攻击和已修补的 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) # 获取原始未修改的字符串
绕过 payload 是一个 单一前导空格:
file:///etc/passwd
^
空格 (0x20)
在 Python ≤ 3.11.3 上,urlparse 看到空 scheme 和无主机名 → 过滤器放行。urlopen 去除空格 → 获取 file:///etc/passwd。
在 Python ≥ 3.11.4 上,urlparse 先去除空格 → 正确识别 scheme=file → 过滤器以 403 拦截。
CPython 问题 #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 — 可自由使用、分享和改编用于教育目的,需注明出处。
| 回合 | 你能看到的 | 它教导什么 |
|---|
| 1 — 基线 | file:///etc/passwd → 403 blocked scheme | 过滤器看似合理 |
| 2 — 利用 | 相同 URL 加一个空格前缀 → 200 + /etc/passwd 内容和内部密钥 | 一个空格就使整个过滤器失效 |
| 3 — 补丁 | 相同 payload 在 Python 3.11.4 上 → 403 blocked | 已修补的 urlparse 先去除空格;过滤器正确拦截 |
| 服务 | Python 版本 | 角色 | 主机端口 |
|---|
vulnerable-api | 3.11.3 | 带有简陋 URL 过滤器的目标 API | 8000 |
fixed-api | 3.11.4 | 相同代码,已修补的解释器 | 8000 |
internal-service | 3.12 | 虚假的内部元数据端点 | 无 |
attacker | 3.12 | 利用驱动程序 | 无 |