
PoC en Python para CVE-2026-94504, un XSS almacenado no autenticado en Ninja Forms <= 3.15.3. Identifica versiones vulnerables e implanta un payload mediante nf_ajax_submit.
POC de XSS almacenado no autenticado en Ninja Forms
Ninja Forms ≤ 3.15.3 — XSS almacenado no autenticado. Se guarda un textarea público sin RTE, luego se aplica html_entity_decode() y se escribe en el editor de envíos heredado sin esc_textarea(). 3.15.4 lo escapa. No se requiere iniciar sesión para plantar el payload. Se ejecuta cuando un editor abre ese envío.
Solo para pruebas autorizadas. No ejecutes esto contra sistemas que no sean tuyos o para los que no tengas permiso de prueba.
Requiere: Python 3, solo biblioteca estándar.
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 | Qué hace |
|---|
-u / --url | URL base de WordPress (obligatorio). http:// o https://. Un nombre de host sin esquema se prueba primero como HTTPS, luego HTTP. Las redirecciones http→https conservan el POST. Los certificados TLS incorrectos se reintentan sin verificación; --insecure omite la verificación desde el principio |
--test | Solo fingerprinting. Lee readme.txt, encuentra un textarea público sin RTE. No envía nada |
--plant | nf_ajax_submit no autenticado. Rellena los campos obligatorios, coloca el payload en el textarea e imprime la URL de edición. No abre un navegador |
--page | Página que incrusta el formulario. Por defecto rastrea /, /contact/ y la lista REST de páginas/entradas |
--field | Id del campo textarea. Por defecto es el primer textarea sin RTE |
--payload | Valor del textarea. Por defecto cierra el <textarea> del admin con un img onerror. En cmd.exe no lo envuelvas en comillas simples (< es una redirección). Omite --payload, o usa comillas dobles y comillas simples dentro de alert: --payload "</textarea>" |
--name / --email | Rellenos para otros campos obligatorios (por defecto Ada Lovelace / [email protected]) |
--insecure | Omite la verificación TLS |
--timeout | Segundos de timeout HTTP (por defecto 30) |
--test lee /wp-content/plugins/ninja-forms/readme.txt (Stable tag) y una página pública cuyo JSON nfForms tenga "type":"textarea" con el editor de texto enriquecido desactivado. ≤ 3.15.3 más ese campo es VULNERABLE. ≥ 3.15.4 es NOT_VULNERABLE. Ningún formulario en el sitio es NEED_FORM.
--plant envía /wp-admin/admin-ajax.php action=nf_ajax_submit con el ajaxNonce de la página. Sin cuenta de WordPress. El éxito imprime PAYLOAD, SUB y EDIT /wp-admin/post.php?post=ID&action=edit. El script se ejecuta cuando ese envío se abre en wp-admin. La página pública del formulario no.
Los párrafos de texto enriquecido (textarea_rte) son una ruta diferente y se omiten. El campo "Message" habitual de Contact Me es el objetivo habitual.
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. Parcheado en 3.15.4. Crédito: Hippolyte Quéré (Hippie); el changelog de 3.15.4 también nombra a Venkateswara Reddy Challa por la corrección de escape del admin.

