
CVE-2020-13671 - Analyse de vulnérabilité et PoC de la RCE Drupal via téléversement de fichiers
Logiciel : Drupal Core 8.7.5 (Affecté : 7.x < 7.78, 8.x < 8.8.11, 8.9.x < 8.9.9, 9.0.x < 9.0.8)
CVSS : 8.8 (Élevé)
CWE : CWE-434 — Téléversement sans restriction de fichiers de type dangereux
CISA KEV : Oui — Vulnérabilité connue exploitée
Avis : SA-CORE-2020-012
Drupal est un système de gestion de contenu (CMS) open source écrit en PHP, similaire à WordPress ou Joomla, mais plutôt orienté vers la construction de systèmes plus complexes — sites d'entreprise, plateformes multilingues. Drupal repose sur une architecture modulaire, permettant d'étendre les fonctionnalités en activant/désactivant les modules disponibles ou en installant des modules supplémentaires issus de la communauté.
L'une des fonctions de base de tout CMS est de permettre aux utilisateurs de téléverser des fichiers — avatars de profil, documents joints, pièces jointes dans les articles. Drupal enregistre ces fichiers dans le répertoire sites/default/files/ et les sert directement via le serveur web (Apache ou Nginx).
⇒ Cela crée une surface d'attaque évidente : si un attaquant parvient à téléverser un fichier PHP dans ce répertoire, le serveur web l'exécutera lorsqu'il sera accédé via une requête entrante.
Pour empêcher cela, Drupal met en place plusieurs couches de défense : validation des extensions de fichiers, renommage des fichiers dangereux, placement d'un .htaccess pour bloquer l'exécution de scripts dans le répertoire de téléversement. Mais dans la version 8.7.5, les attaquants exploitent exactement l'angle mort de ces couches.
Cette vulnérabilité nécessite un compte disposant des permissions de téléversement de fichiers. Par défaut dans Drupal 8.7.5, les utilisateurs réguliers (Authentifiés) n'ont que les permissions de voir le contenu et de poster des commentaires — aucune permission pour créer des articles ou téléverser des fichiers.
| Compte | Exploitable ? | Explication |
|---|---|---|
| Admin | Oui | Permissions de téléversement complètes |
| Éditeur / Créateur de contenu | Oui | Si la permission « Créer du contenu » est accordée avec téléversement par l'administrateur |
| Utilisateur authentifié (par défaut) | Non | Par défaut, aucune permission pour créer du contenu ou téléverser |
| Anonyme (non connecté) | Non | Aucune permission de téléversement |
Cependant, dans la pratique, de nombreux sites Drupal accordent les permissions de création de contenu aux utilisateurs réguliers (forums, blogs communautaires, sites d'actualité permettant la soumission d'articles). Dans ces cas, un attaquant n'a besoin que de créer un compte pour l'exploiter.
Déroulement de l'attaque :
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
Dans le fichier core/modules/file/file.module, il existe une seule expression régulière qui détermine quels fichiers sont considérés comme exécutables :
define('FILE_INSECURE_EXTENSION_REGEX', '/\.(phar|php|pl|py|cgi|asp|js)(\.|$)/i');

Cette expression régulière liste 7 extensions : phar, php, pl, py, cgi, asp, js. Tout fichier dont l'extension correspond à cette liste sera automatiquement complété avec .txt par Drupal — neutralisant ainsi la capacité d'exécution.
Cependant, le moteur PHP ne traite pas uniquement les fichiers .php. Selon la configuration du serveur web, il reconnaît et exécute également d'autres extensions :
| Extension | Signification | Incluse dans la regex ? |
|---|---|---|
.php | PHP standard | Oui |
.phtml | Modèle alternatif PHP | Non |
.php5 | Gestionnaire PHP 5 | Non |
.pht | Modèle PHP | Non |
.phps | Source PHP | Non |
.shtml | Inclusions côté serveur | Non |
5 variantes d'extensions PHP sont totalement absentes de l'expression régulière. Cela signifie qu'un fichier nommé shell.phtml envoyé à Drupal → l'expression régulière ne correspond pas → pas de renommage → enregistré sous son nom d'origine dans le répertoire de téléversement → le serveur web voit .phtml → l'exécute comme PHP → l'attaquant obtient une RCE. C'est la cause racine : Drupal a utilisé une liste noire pour bloquer les extensions dangereuses, mais cette liste était incomplète.
Lorsqu'un utilisateur téléverse un fichier, Drupal le fait passer à travers 3 fonctions de validation avant de l'enregistrer. Voici une analyse de la raison pour laquelle ces 3 fonctions échouent avec .phtml.
file_munge_filename() (core/includes/file.inc)Objectif : détecter les extensions dangereuses situées au milieu du nom de fichier et ajouter _ pour les neutraliser. Voici comment la fonction fonctionne :

$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;
Par exemple avec shell.phtml :
explode divise en ["shell", "phtml"]shift prend "shell", laissant ["phtml"]pop prend "phtml", laissant []foreach ne s'exécute pas"shell.phtml" intactCependant, si le fichier n'a qu'une seule extension, cette fonction n'intervient pas. Ainsi, elle n'est conçue que pour gérer les fichiers à extensions multiples.
FILE_INSECURE_EXTENSION_REGEX (file.module:1015)C'est la principale couche de défense. Code à la ligne 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 le nom de fichier correspond à l'expression régulière → changer le MIME en text/plain et ajouter .txt à la fin.
Avec shell.phtml :
preg_match('/\.(phar|php|pl|py|cgi|asp|js)(\.|$)/i', 'shell.phtml') renvoie 0.phtml est dangereux, elle passe à travers..htaccess dans le répertoire de téléversementDrupal place un fichier .htaccess dans 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>