
استغلال إثبات المفهوم وتقرير فني لثغرة CVE-2023-6553، وهي ثغرة تضمين ملفات PHP غير مصادق عليها تتيح تنفيذ تعليمات برمجية عن بُعد في إضافة Backup Migration لووردبريس بإصدار <=1.3.7.
الإضافة: Backup Migration (backup-backup) ≤ 1.3.7
CVSS: 9.8 (حرجة)
CWE: CWE-98 — تحكم غير سليم في اسم الملف لجملة Include/Require
متطلب المصادقة: لا يوجد
الأثر: تنفيذ تعليمات برمجية عن بُعد
إضافة Backup Migration هي إضافة ووردبريس شائعة إلى حد ما (أكثر من ~90,000 تثبيت نشط) تساعد المستخدمين على إنشاء نسخ احتياطية. أثناء عملية النسخ الاحتياطي، يوجد في الإضافة ملف باسم backup-heart.php يعمل في الخلفية - يستقبل معلومات الإعداد عبر ترويسات HTTP لمعرفة الدليل الذي يجب نسخه احتياطيًا، وأين يقع ملف الإعداد، وما إلى ذلك.
تكمن المشكلة في أن هذا الملف يثق تمامًا في ترويسات HTTP المرسلة من العميل، ويأخذ قيمة الترويسة مباشرة إلى مسار الملف، ثم يستخدم require_once() لتحميل الملف من ذلك المسار. لا يحتاج المهاجم سوى إرسال ترويسة Content-Dir تشير إلى دليل يحتوي على كود PHP خبيث → يقوم الخادم تلقائيًا بتضمينه وتنفيذه.
من الجدير بالذكر أن ملف backup-heart.php لا يتطلب مصادقة — فهو يتحقق فقط من أن طريقة الطلب هي POST، دون التحقق من أي nonce أو صلاحيات مستخدم. يمكن لأي شخص على الإنترنت إرسال طلب إليه.
⇒ هذه ثغرة بدون نقرة (zero-click).
| الخاصية | القيمة |
|---|---|
| معرف CVE | CVE-2023-6553 |
| درجة CVSS | 9.8 (حرجة) |
| الإضافة | backup-backup (Backup Migration) ≤ 1.3.7 |
| المصادقة | غير مطلوبة |
| تفاعل المستخدم | لا شيء (بدون نقرة) |
| الإصلاح | الإصدار 1.3.8 |
توجد في PHP دوال مثل include() و require() و require_once() تُستخدم لتضمين ملفات PHP أخرى في البرنامج الجاري. عندما يأتي مسار الملف المُمرَّر إلى هذه الدوال من مدخلات المستخدم دون تحقق، يمكن للمهاجم إجبار الخادم على تضمين أي ملف يريده:
allow_url_include=On (عادةً ما يكون معطلاً افتراضيًا).تندرج هذه الثغرة تحت LFI — يتحكم المهاجم في المسار المُمرَّر إلى require_once() الذي يشير إلى ملف PHP تمكن المهاجم من كتابته على الخادم.
يعتقد العديد من المطورين أن ترويسات HTTP هي بيانات وصفية "داخلية" لا يعرفها سوى الخادم والعميل. في الواقع، يتحكم المهاجمون في 100% من محتوى الترويسات — يمكنهم تعيين أي اسم وقيمة لأي ترويسة. الثقة بالترويسات تشبه تمامًا الثقة بمدخلات النماذج — يجب التحقق منها.
define() وثوابت PHPتُنشئ define('NAME', $value) ثابتًا يُستخدم في جميع أنحاء التطبيق. وبمجرد تعريفه عبر define، لا يمكن تغيير قيمته. إذا كانت $value قادمة من مهاجم، فكل مكان يستخدم هذا الثابت يتأثر.
بدأت باستخدام grep للبحث عن جميع جمل require وinclude في أنحاء الإضافة:
grep -rn "require\|include" includes/

أعادت نتائج البحث العديد من استدعاءات require/include. وبمراجعتها، كانت معظمها استدعاءات include_once في banner/misc.php وbanner/views/index.php — وهي تخص كود عرض واجهة الإدارة بمسارات ثابتة، مما يجعلها غير قابلة للاستغلال.
غير أن سطرين في backup-heart.php لفتا انتباهي:
includes/backup-heart.php:64: define('BMI_INCLUDES', BMI_ROOT_DIR . 'includes');
includes/backup-heart.php:118: require_once BMI_INCLUDES . '/bypasser.php';
يستخدم السطر 118 require_once مع الثابت BMI_INCLUDES — ولو كان هذا الثابت بقيمة ثابتة لكان آمنًا. ولكن بالنظر إلى السطر 64، رأيت أن BMI_INCLUDES مبني من ثابت آخر هو BMI_ROOT_DIR. لذا علينا تتبع المصدر أكثر: أين تُسند قيمة BMI_ROOT_DIR؟
استخدمت grep مرة أخرى لتتبعه:
grep -n "BMI_ROOT_DIR" includes/backup-heart.php
النتائج:

Line 62: define('BMI_ROOT_DIR', $fields['content-dir']);
Line 64: define('BMI_INCLUDES', BMI_ROOT_DIR . 'includes');
يُظهر السطر 62 أن BMI_ROOT_DIR يأخذ قيمته من $fields['content-dir']. هذا متغير وليس قيمة ثابتة — نحتاج إلى فتح الملف ومعرفة ما يحتويه $fields.
فتحت backup-heart.php في VS Code عند السطر 62:
// Line 62
define('BMI_ROOT_DIR', $fields['content-dir']);
// Line 64
define('BMI_INCLUDES', BMI_ROOT_DIR. 'includes');

من الواضح أن $fields['content-dir'] يدخل مباشرة إلى define(). الآن نحتاج إلى تحديد أين يُسند المتغير $fields:
// Lines 7-9: Only checks POST method
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
exit;
}
// Lines 30-33: Reads ALL HTTP headers from the request
if (isFunctionEnabled('getallheaders')) {
$fields= getallheaders();
}
// Lines 42-46: Lowercases header names
foreach ($fieldsas $key=> $value) {
$buffer= $value;
unset($fields[$key]);
$fields[strtolower($key)] = $value;
}


عند هذه النقطة، يصبح السبب الجذري واضحًا تمامًا: $fields يحتوي على جميع ترويسات HTTP المستخرجة عبر getallheaders() — وهي خاضعة بالكامل للعميل. لا يوجد wp_verify_nonce() ولا current_user_can() ولا فحص للمسار الصحيح — بل يتحقق فقط من طريقة POST ويقرأ الترويسات مباشرة.
Attacker sends POST request with header Content-Dir: /path/to/attacker/
↓
getallheaders() reads raw headers → $fields['content-dir'] = "/path/to/attacker/"
↓
define('BMI_ROOT_DIR', "/path/to/attacker/") ← no validation
↓
define('BMI_INCLUDES', "/path/to/attacker/includes")
↓
require_once "/path/to/attacker/includes/bypasser.php" ← executes PHP
↓
Attacker code runs with www-data privileges → RCE
ملخص السبب الجذري: ترويسة HTTP ← define() ← require_once()، دون أي خطوة تحقق بينها.
استخدمت Xdebug + VS Code لتأكيد مسار الهجوم بصريًا. وضعت نقطتي توقف عند السطرين 62 و118 في ملف backup-heart.php، ثم أرسلت طلب الاستغلال باستخدام curl.
نقطة التوقف 1 — السطر 62:
توقف المُنقّح (debugger) مباشرة عند define('BMI_ROOT_DIR', $fields['content-dir']). عند توسيع المتغير $fields في لوحة Variables ظهرت مصفوفة من 22 عنصرًا — تحتوي على جميع ترويسات HTTP التي أرسلها العميل. وتحديدًا:
content-dir = "/tmp/bmi/" — هذه هي القيمة المُرسلة عبر الترويسة تمامًا، والتي تُسند مباشرة إلى الثابت BMI_ROOT_DIR.content-abs = "/var/www/html/", content-configdir = "/tmp/bmi/", content-backups = "/tmp/bmi/back..." — جميعها خاضعة للمهاجم.لا توجد أي خطوات تحقق أو تصفية تُطبق على content-dir قبل تمريره إلى define().

نقطة التوقف 2 — السطر 118:
عند الضغط على F5، توقف المُنقّح عند require_once BMI_INCLUDES . '/bypasser.php'. بالنظر إلى الحالة:
$fields يحتفظ بـ content-dir = "/tmp/bmi/" — مما يثبت أن القيمة لم تتغير بين السطرين 62 و118.{main} backup-heart.php 118:1 — أي أن الكود نُفذ مباشرة من أعلى الملف حتى هذه النقطة، متجاوزًا أي برمجيات وسيطة أو فحوصات مصادقة./tmp/bmi/includes/bypasser.php — وهو ملف يتحكم المهاجم في محتواه.