
كاشف غير إتلافي + مختبر Docker لأداة wp2shell (التباس مسار REST /batch/v1 CVE-2026-63030 + حقن SQL عبر author__not_in CVE-2026-60137) في نواة ووردبريس 6.9.0-6.9.4 / 7.0.0-7.0.1
مختبر مستقل بذاته، كاشف غير مدمر، وإثبات مفهوم كامل لـ RCE قبل المصادقة لأداة wp2shell — سلسلة الثغرات قبل المصادقة في نواة ووردبريس:
| CVE | المكوّن | التصنيف | CVSS |
|---|---|---|---|
| CVE-2026-60137 | WP_Query::author__not_in | حقن SQL (CWE-89) | 9.1 |
| CVE-2026-63030 | التباس مسار REST /batch/v1 | تعارض تفسير (CWE-436) → يتسلسل إلى RCE | 7.5 |
المتأثر: نواة ووردبريس 6.9.0–6.9.4 و7.0.0–7.0.1 (مصب حقن SQL وحده يؤثر أيضًا على 6.8.0–6.8.5). أُصلح في 6.8.6 / 6.9.5 / 7.0.2. أُبلغ عنها Adam Kues (Assetnote / Searchlight Cyber)؛ ونُسب حقن SQL أيضًا إلى TF1T وdtro وhaongo. سلسلة RCE على التثبيت القياسي الافتراضي (oEmbed → changeset → re-entry) من إعداد Mustafa Can İPEKÇİ (nukedx).
حدّث أولًا. أصدرت ووردبريس تحديثات تلقائية إجبارية لهذا الغرض. هذا المستودع موجود لمساعدتك على التحقق من أن بيئتك الخاصة مُحدّثة وفهم الثغرة — وليس لمهاجمة أي شخص. انظر SECURITY.md.
البدائية الصحيحة دائمًا هي حقن SQL دون مصادقة، بدون إضافات، في النواة الافتراضية يتيح
قراءة كاملة لقاعدة البيانات (تجزئات كلمات مرور المسؤول، وكل ما في wp_options/wp_users). هذا وحده
يستحق تصنيف 9.1 والترقية الفورية.
RCE حقيقية وتعمل على ووردبريس القياسي الافتراضي — لا حاجة لصلاحية FILE، ولا لذاكرة تخزين مؤقتة
كائنية دائمة، ولا لإضافات، ولا لسوء إعداد. تستخدم السلسلة حقن SQL للقراءة فقط كـبدائية
لتزوير الصفوف (UNION ALL SELECT يحقن صفوف wp_posts مزيفة)، ثم تستغل خط أنابيب عرض المحتوى
في ووردبريس نفسه لتحويل تلك الصفوف المزورة إلى كتابات حقيقية في قاعدة البيانات عبر تخزين oEmbed
المؤقت. ومن هناك، يعمل رفع الـ changeset وإعادة الدخول إلى parse_request في سياق المسؤول،
مما ينشئ حساب مسؤول جديد — كل ذلك من طلب HTTP واحد دون مصادقة.
POST /?rest_route=/batch/v1)1. Route confusion — double-nested batch desyncs $matches/$validation so a GET
/wp/v2/widgets runs under posts::get_items() (public), reaching
WP_Query's author__not_in with attacker-controlled input.
2. Row forgery — author__not_in is string-concatenated into SQL;
"1) AND 1=0 UNION ALL SELECT <23 cols> -- -" injects fake
WP_Post rows. per_page=-1 bypasses split_the_query (WP_Query
treats -1 as "no limit" → empty $limits → split=false →
full SELECT wp_posts.* → UNION columns match).
3. oEmbed write — forged posts carry [embed]<self-url>[/embed]; rendering via
context=view makes WordPress cache real oembed_cache posts in
the DB (turns read-only SQLi into writes with predictable IDs).
4. Elevation+re-entry — a forged customize_changeset (user_id = real admin) plus a
forged post_type=request row with parent loops drives an
in-process re-entrant parse_request in admin context.
5. Admin creation — POST /wp/v2/users in the same batch passes
current_user_can('create_users') → new administrator.
6. RCE — login → plugin webshell upload → command execution → cleanup.
الجدة تكمن في التركيب، لا في أي خطأ منفرد — فالأدوات الفردية سلوكيات مشروعة في ووردبريس.
تسميتها (وفقًا لشرح Adam Kues) تجعل مخطط الصفوف المزورة في exploit() قابلاً للقراءة
وتحدد ما يجب إعادة تدقيقه بعد تصحيح نقاط الدخول:
wp_update_post() وتفضّل post_type/post_status في الذاكرة، مما يسمح
بإعادة تصنيف صف oembed_cache إلى post/customize_changeset حقيقي.post_content — يتجول فلتر wp_insert_post_parent في سلسلة الأصل؛
وعند اكتشاف دورة، يستدعي wp_update_post() ثانيًا يصلح الأصل دون استبدال post_content.
هذا هو الرابط الحرج: فهو يسمح لـpost_content الخبيث في الـ changeset المزوّر بالبقاء. (لهذا
تستخدم الصفوف المزورة حلقات أصل ذاتية/متبادلة.)parse_request — نشر مشاركة يشغّل do_action("{$status}_{$type}")؛
صف مزوّر بـpost_status=parse / post_type=request يطلق parse_request، ويعيد تشغيل خط أنابيب
الدفعة بينما ما تزال هوية المسؤول المفترضة للـ changeset (wp_set_current_user) قائمة.تبقى هذه الأدوات حاضرة في ووردبريس المُصحّح — أُغلقت فقط نقطتا الدخول (عدم مزامنة الدفعة +
تجاوز author__not_in القياسي). أي بدائية جديدة تزوّر كاش المشاركات في الذاكرة أو تكتب
صف oembed_cache ستعيد تمكين الذيل نفسه للاستيلاء على المسؤول.
تم تأكيد بدائيات الكتابة الأربع في وقت العرض فعليًا ضد 7.0.1 — كل واحدة زُوّرت كمشاركة دون مصادقة، وعُرضت عبر التباس الدفعة، وتُحقق من كتابة قاعدة البيانات الناتجة عبر حقن SQL أعمى:
| البدائية | ترميز التشغيل | المصب | المعرّف المتوقع | تم التحقق |
|---|---|---|---|---|
| oembed | [embed]<url>[/embed] | صف wp_posts (oembed_cache) | post_name = md5(url+attrs) | تم إنشاء معرّف المشاركة |
| rss | wp:rss {feedURL} | transient للموقع في wp_options | _site_transient_feed_<md5(url)> | option_id، 5192 بايت مخزنة |
| navigation | wp:navigation | صف wp_posts (wp_navigation) | post_name = 'navigation' (ثابت) | تم إنشاء المعرّف، slug: navigation |
| calendar | wp:calendar | wp_options | wp_calendar_block_has_published_posts | option_id، القيمة '1' |
ماذا يثبت كل نتيجة:
wp_posts جديد برابط slug متوقع من المهاجم
(md5(url+serialize(attrs))) ومعرّف تزايد تلقائي حقيقي. هذه الوحيدة التي تمنحك
صفوف مشاركات متعددة، عند الطلب، باسم المهاجم — ولهذا تستخدمها سلسلة RCE لدعم
مخطط الـ changeset/request المزوّر._site_transient_feed_<md5(url)> قابل للتنبؤ
تمامًا، والبايتات المخزنة هي جسم التغذية الذي يقدمه عنوان URL الخاص بالمهاجم — أي أن المهاجم
يتحكم في المفتاح والقيمة معًا. إنها كتابة في wp_options (بدون معرّف مشاركة)، لذا فهي
بدائية تسميم خيارات وليست بديلًا جاهزًا لدعم الـ changeset.wp_navigation منشور) برابط slug ثابت، لذا يمكنه دعم
كائن مزوّر واحد على الأكثر، على عكس صفوف oEmbed المتعددة.update_option يُطلق دون مصادقة، لكن اسم الخيار
وقيمته '1'/'0' ثابتان/مشتقان من قاعدة البيانات، لذا فهو إثبات "حدوث كتابة" دون
تحكم للمهاجم في المفتاح أو القيمة.FILE العامة + أن
يكون secure_file_priv مخدمًا عبر الويب + أن يكون دليل الإخراج قابلًا للقراءة من مستخدم الويب.
على الاستضافات العادية/المُدارة لا ينطبق أي من ذلك.call_user_func('wp_insert_user', user_data_array) — يتطلب
gc_enabled()=false + HMAC صالحًا (wp_hash للبايتات المتسلسلة تمامًا، ويحتاج أسرار wp-config.php).المتطلبات: Docker + Docker Compose v2، Python 3.8+ (المكتبة القياسية فقط)، make، curl.
make up # WordPress 6.9.4 (vulnerable) + MySQL 8.0, auto-installed on :8093
make check # -> [VULNERABLE] http://localhost:8093 (WordPress 6.9.4 ...)
make proof # -> also reads @@version and current_user() as read-only evidence
make exploit # -> full pre-auth RCE: creates admin, deploys webshell, runs "id"
make patched # rebuild on the fixed image and re-check -> [not vulnerable]
make down # tear down (removes volumes)
غيّر المنفذ باستخدام WP_PORT=8100 make up.