
PoC Python pour CVE-2026-94504, une XSS stockée non authentifiée dans Ninja Forms <= 3.15.3. Il identifie les versions vulnérables et injecte une charge utile via nf_ajax_submit.
POC de XSS stocké non authentifié dans Ninja Forms
Ninja Forms ≤ 3.15.3 — XSS stocké non authentifié. Un textarea public non-RTE est enregistré, puis passé par html_entity_decode() et écrit dans l'éditeur de soumissions hérité sans esc_textarea(). 3.15.4 l'échappe. Aucune connexion n'est requise pour implanter la charge utile. Elle s'exécute lorsqu'un éditeur ouvre cette soumission.
Pour des tests autorisés uniquement. Ne lancez pas ceci contre des systèmes que vous ne possédez pas ou pour lesquels vous n'avez pas d'autorisation de test.
Nécessite : Python 3, bibliothèque standard uniquement.
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 | Ce qu'il fait |
|---|
-u / --url | URL de base WordPress (requis). http:// ou https://. Un nom d'hôte sans schéma est essayé en HTTPS d'abord, puis en HTTP. Les redirections http→https conservent le POST. Les certificats TLS invalides sont réessayés sans vérification ; --insecure ignore la vérification dès le départ |
--test | Empreinte uniquement. Lit readme.txt, trouve un textarea public non-RTE. Ne soumet pas |
--plant | nf_ajax_submit non authentifié. Remplit les champs requis, place la charge utile dans le textarea et affiche l'URL d'édition. N'ouvre pas de navigateur |
--page | Page qui intègre le formulaire. Par défaut, explore /, /contact/ et la liste REST des pages/articles |
--field | Id du champ textarea. Par défaut, le premier textarea non-RTE |
--payload | Valeur du textarea. Par défaut, ferme le <textarea> d'administration avec un img onerror. Dans cmd.exe, ne l'entourez pas de guillemets simples (< est une redirection). Omettez --payload, ou utilisez des guillemets doubles et des guillemets simples à l'intérieur de alert : --payload "</textarea>" |
--name / --email | Valeurs de remplissage pour les autres champs requis (par défaut Ada Lovelace / [email protected]) |
--insecure | Ignore la vérification TLS |
--timeout | Délai d'expiration HTTP en secondes (par défaut 30) |
--test lit /wp-content/plugins/ninja-forms/readme.txt (Stable tag) et une page publique dont le JSON nfForms contient "type":"textarea" avec l'éditeur de texte enrichi désactivé. ≤ 3.15.3 plus ce champ donne VULNERABLE. ≥ 3.15.4 donne NOT_VULNERABLE. Aucun formulaire sur le site donne NEED_FORM.
--plant envoie /wp-admin/admin-ajax.php action=nf_ajax_submit avec l'ajaxNonce de la page. Aucun compte WordPress. En cas de succès, affiche PAYLOAD, SUB et EDIT /wp-admin/post.php?post=ID&action=edit. Le script s'exécute lorsque cette soumission est ouverte dans wp-admin. La page publique du formulaire, non.
Les paragraphes en texte enrichi (textarea_rte) empruntent un chemin différent et sont ignorés. Le champ « Message » habituel de Contact Me est la cible typique.
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. Corrigé dans 3.15.4. Crédit : Hippolyte Quéré (Hippie) ; le changelog 3.15.4 cite également Venkateswara Reddy Challa pour la correction de l'échappement dans l'admin.

