
PoC in Python per CVE-2026-94504, un XSS memorizzato non autenticato in Ninja Forms <= 3.15.3. Rileva le versioni vulnerabili e inserisce un payload tramite nf_ajax_submit.
POC di XSS memorizzato non autenticato in Ninja Forms
Ninja Forms ≤ 3.15.3 — XSS memorizzato non autenticato. Una textarea pubblica non-RTE viene salvata, poi sottoposta a html_entity_decode() e scritta nell'editor delle submission legacy senza esc_textarea(). 3.15.4 la esegue l'escaping. Non è richiesto alcun login per inserire il payload. Viene eseguito quando un editor apre quella submission.
Solo per test autorizzati. Non eseguire questo strumento contro sistemi di cui non sei proprietario o per cui non hai il permesso di eseguire test.
Richiede: Python 3, solo libreria standard.
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 | Cosa fa |
|---|
-u / --url | URL base di WordPress (obbligatorio). http:// o https://. Un hostname senza schema viene provato prima come HTTPS, poi come HTTP. I redirect http→https mantengono il POST. I certificati TLS non validi vengono ritentati senza verifica; --insecure salta la verifica fin dall'inizio |
--test | Solo fingerprinting. Legge readme.txt, trova una textarea pubblica non-RTE. Non invia nulla |
--plant | nf_ajax_submit non autenticato. Compila i campi obbligatori, inserisce il payload nella textarea e stampa l'URL di modifica. Non apre un browser |
--page | Pagina che incorpora il form. Per impostazione predefinita esegue la scansione di /, /contact/ e della lista REST di pagine/post |
--field | Id del campo textarea. Il valore predefinito è la prima textarea non-RTE |
--payload | Valore della textarea. Il valore predefinito chiude la <textarea> di amministrazione con un img onerror. In cmd.exe non racchiudere questo valore tra apici singoli (< è un redirect). Ometti --payload, oppure usa doppi apici e apici singoli all'interno di alert: --payload "</textarea>" |
--name / --email | Valori di riempimento per gli altri campi obbligatori (predefiniti Ada Lovelace / [email protected]) |
--insecure | Salta la verifica TLS |
--timeout | Timeout HTTP in secondi (predefinito 30) |
--test legge /wp-content/plugins/ninja-forms/readme.txt (Stable tag) e una pagina pubblica il cui JSON nfForms abbia "type":"textarea" con l'editor rich-text disattivato. ≤ 3.15.3 più quel campo equivale a VULNERABLE. ≥ 3.15.4 equivale a NOT_VULNERABLE. Nessun form sul sito equivale a NEED_FORM.
--plant invia /wp-admin/admin-ajax.php con action=nf_ajax_submit e l'ajaxNonce della pagina. Nessun account WordPress. In caso di successo stampa PAYLOAD, SUB e EDIT /wp-admin/post.php?post=ID&action=edit. Lo script viene eseguito quando quella submission viene aperta in wp-admin. La pagina pubblica del form no.
I paragrafi rich-text (textarea_rte) seguono un percorso diverso e vengono saltati. Il campo "Message" del solito Contact Me è il target abituale.
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. Corretto in 3.15.4. Crediti: Hippolyte Quéré (Hippie); il changelog di 3.15.4 cita anche Venkateswara Reddy Challa per la correzione dell'escaping nell'admin.

