
PoC em Python para CVE-2026-94504, um XSS armazenado não autenticado no Ninja Forms <= 3.15.3. Identifica versões vulneráveis e injeta um payload via nf_ajax_submit.
POC de XSS armazenado não autenticado no Ninja Forms
Ninja Forms ≤ 3.15.3 — XSS armazenado não autenticado. Uma textarea pública sem RTE é salva, então passa por html_entity_decode() e é gravada no editor de envios legado sem esc_textarea(). A versão 3.15.4 faz o escape. Nenhum login é necessário para plantar o payload. Ele é executado quando um editor abre aquele envio.
Apenas para testes autorizados. Não execute isto contra sistemas que você não possui ou não tem permissão para testar.
Requer: Python 3, apenas biblioteca padrão.
python3 cve-2026-94504-ninja-forms-stored-xss.py -u http://TARGET --test
python3 cve-2026-94504-ninja-forms-stored-xss.py -u http://TARGET --plant
python3 cve-2026-94504-ninja-forms-stored-xss.py -u https://TARGET --test --insecure
python3 cve-2026-94504-ninja-forms-stored-xss.py -u TARGET --test
| Flag | O que faz |
|---|
-u / --url | URL base do WordPress (obrigatório). http:// ou https://. Um hostname sem esquema é tentado como HTTPS primeiro, depois HTTP. Redirecionamentos http→https mantêm o POST. Certificados TLS inválidos são tentados novamente sem verificação; --insecure pula a verificação desde o início |
--test | Apenas fingerprint. Lê readme.txt, encontra uma textarea pública sem RTE. Não envia |
--plant | nf_ajax_submit não autenticado. Preenche os campos obrigatórios, coloca o payload na textarea e imprime a URL de edição. Não abre um navegador |
--page | Página que incorpora o formulário. Por padrão, rastreia /, /contact/ e a lista de páginas/posts da REST |
--field | Id do campo textarea. O padrão é a primeira textarea sem RTE |
--payload | Valor da textarea. O padrão fecha o <textarea> do admin com um img onerror. No cmd.exe não envolva isto em aspas simples (< é um redirecionamento). Omita --payload, ou use aspas duplas e aspas simples dentro de alert: --payload "</textarea>" |
--name / --email | Preenchedores para outros campos obrigatórios (padrões Ada Lovelace / [email protected]) |
--insecure | Pula a verificação TLS |
--timeout | Timeout HTTP em segundos (padrão 30) |
--test lê /wp-content/plugins/ninja-forms/readme.txt (Stable tag) e uma página pública cujo JSON nfForms tenha "type":"textarea" com o editor de texto rico desativado. ≤ 3.15.3 mais esse campo é VULNERABLE. ≥ 3.15.4 é NOT_VULNERABLE. Nenhum formulário no site é NEED_FORM.
--plant envia POST para /wp-admin/admin-ajax.php com action=nf_ajax_submit e o ajaxNonce da página. Nenhuma conta WordPress. O sucesso imprime PAYLOAD, SUB e EDIT /wp-admin/post.php?post=ID&action=edit. O script é executado quando aquele envio é aberto no wp-admin. A página pública do formulário não.
Parágrafos de texto rico (textarea_rte) são um caminho diferente e são ignorados. O campo "Message" padrão do Contact Me é o alvo usual.
Stdout: VERSION, PAGE, FORM, FIELD, PRESENT, VULNERABLE, NOT_VULNERABLE, NOT_FOUND, NEED_FORM, NEED_NONCE, PLANTED, PAYLOAD, SUB, EDIT.
CVE: CVE-2026-94504. Plugin: ninja-forms. Corrigido na 3.15.4. Crédito: Hippolyte Quéré (Hippie); o changelog da 3.15.4 também nomeia Venkateswara Reddy Challa pela correção de escape no admin.

