
Exploit de prova de conceito e documentação técnica para CVE-2023-6553, uma vulnerabilidade de inclusão de arquivo PHP não autenticada que permite execução remota de código no plugin WordPress Backup Migration <=1.3.7.
Plugin: Backup Migration (backup-backup) ≤ 1.3.7
CVSS: 9.8 (Crítico)
CWE: CWE-98 — Controle Incorreto do Nome de Arquivo para Instrução Include/Require
Requisito de Autenticação: Nenhum
Impacto: Execução Remota de Código
Backup Migration é um plugin WordPress bastante popular (~90.000+ instalações ativas) que ajuda os usuários a criar cópias de backup. Durante o processo de backup, o plugin possui um arquivo chamado backup-heart.php rodando em segundo plano — ele recebe informações de configuração via cabeçalhos HTTP para saber qual diretório precisa ser copiado, onde o arquivo de configuração está localizado, etc.
O problema está no fato de que este arquivo confia totalmente nos cabeçalhos HTTP enviados pelo cliente, coloca o valor do cabeçalho diretamente no caminho do arquivo e, em seguida, usa require_once() para carregar o arquivo desse caminho. Um atacante só precisa enviar o cabeçalho Content-Dir apontando para um diretório contendo código PHP malicioso → o servidor automaticamente inclui e executa o código.
Vale notar que o arquivo backup-heart.php não exige autenticação — ele apenas verifica se o método da requisição é POST, sem validar nenhum nonce ou privilégios de usuário. Qualquer pessoa na internet pode enviar uma requisição para ele.
⇒ Esta é uma vulnerabilidade zero-clique.
| Atributo | Valor |
|---|---|
| ID CVE | CVE-2023-6553 |
| Pontuação CVSS | 9.8 (Crítico) |
| Plugin | backup-backup (Backup Migration) ≤ 1.3.7 |
| Autenticação | Não necessária |
| Interação do Usuário | Nenhuma (zero-clique) |
| Corrigido em | Versão 1.3.8 |
O PHP possui funções como include(), require(), require_once() usadas para incluir outros arquivos PHP no programa em execução. Quando o caminho de arquivo passado para essas funções vem de entrada do usuário sem validação, um atacante pode forçar o servidor a incluir qualquer arquivo que desejar:
allow_url_include=On (geralmente desabilitado por padrão).Este CVE se enquadra em LFI — o atacante controla o caminho passado para require_once(), apontando para um arquivo PHP que o atacante conseguiu gravar no servidor.
Muitos desenvolvedores pensam que cabeçalhos HTTP são metadados "internos" conhecidos apenas pelo servidor e pelo cliente. Na realidade, os atacantes controlam 100% do conteúdo do cabeçalho — eles podem definir qualquer nome e valor de cabeçalho. Confiar em cabeçalhos é como confiar em entradas de formulário — isso deve ser validado.
define() e Constantes do PHPdefine('NAME', $value) cria uma constante usada em toda a aplicação. Uma vez definida pela função define, o valor não pode ser alterado. Se $value vier de um atacante, todos os lugares que usam essa constante serão afetados.
Comecei usando grep para buscar por todas as instruções require e include no plugin:
grep -rn "require\|include" includes/

Os resultados da busca retornaram muitas chamadas require/include. Analisando-as, a maioria eram chamadas include_once em banner/misc.php e banner/views/index.php — essas pertencem ao código de renderização da interface administrativa com caminhos fixos, tornando-as inexploráveis.
No entanto, 2 linhas em backup-heart.php chamaram minha atenção:
includes/backup-heart.php:64: define('BMI_INCLUDES', BMI_ROOT_DIR . 'includes');
includes/backup-heart.php:118: require_once BMI_INCLUDES . '/bypasser.php';
A linha 118 usa require_once com a constante BMI_INCLUDES — se essa constante fosse fixa, estaria segura. Mas olhando para a linha 64, vi que BMI_INCLUDES é construída a partir de outra constante, BMI_ROOT_DIR. Portanto, precisamos rastrear mais: onde BMI_ROOT_DIR recebe seu valor?
Usei grep novamente para rastreá-la:
grep -n "BMI_ROOT_DIR" includes/backup-heart.php
Resultados:

Line 62: define('BMI_ROOT_DIR', $fields['content-dir']);
Line 64: define('BMI_INCLUDES', BMI_ROOT_DIR . 'includes');
A linha 62 mostra que BMI_ROOT_DIR recebe seu valor de $fields['content-dir']. Esta é uma variável, não um valor fixo — precisamos abrir o arquivo e ver o que $fields contém.
Abri backup-heart.php no VS Code na linha 62:
// Line 62
define('BMI_ROOT_DIR', $fields['content-dir']);
// Line 64
define('BMI_INCLUDES', BMI_ROOT_DIR. 'includes');

É claramente visível que $fields['content-dir'] vai diretamente para define(). Agora precisamos determinar onde a variável $fields é atribuída:
// Lines 7-9: Only checks POST method
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
exit;
}
// Lines 30-33: Reads ALL HTTP headers from the request
if (isFunctionEnabled('getallheaders')) {
$fields= getallheaders();
}
// Lines 42-46: Lowercases header names
foreach ($fieldsas $key=> $value) {
$buffer= $value;
unset($fields[$key]);
$fields[strtolower($key)] = $value;
}


Neste ponto, a causa raiz fica extremamente clara: $fields contém todos os cabeçalhos HTTP obtidos via getallheaders() — completamente controlados pelo cliente. Não há wp_verify_nonce(), nenhum current_user_can(), nenhuma verificação de caminho válido — ele simplesmente verifica o método POST e lê os cabeçalhos diretamente.
Attacker sends POST request with header Content-Dir: /path/to/attacker/
↓
getallheaders() reads raw headers → $fields['content-dir'] = "/path/to/attacker/"
↓
define('BMI_ROOT_DIR', "/path/to/attacker/") ← no validation
↓
define('BMI_INCLUDES', "/path/to/attacker/includes")
↓
require_once "/path/to/attacker/includes/bypasser.php" ← executes PHP
↓
Attacker code runs with www-data privileges → RCE
Resumo da causa raiz: Cabeçalho HTTP → define() → require_once(), com zero etapas de validação no meio.
Usei Xdebug + VS Code para confirmar visualmente o fluxo de ataque. Defini 2 breakpoints nas linhas 62 e 118 em backup-heart.php e, em seguida, enviei a requisição de exploit usando curl.
Breakpoint 1 — Linha 62:
O depurador pausou exatamente em define('BMI_ROOT_DIR', $fields['content-dir']). Expandindo a variável $fields no painel Variáveis, apareceu uma matriz de 22 elementos — contendo todos os cabeçalhos HTTP enviados pelo cliente. Especificamente:
content-dir = "/tmp/bmi/" — este é exatamente o valor enviado via cabeçalho, que é diretamente atribuído à constante BMI_ROOT_DIR.content-abs = "/var/www/html/", content-configdir = "/tmp/bmi/", content-backups = "/tmp/bmi/back..." — todos controlados pelo atacante.