Failles critiques wp2shell + xss2shellTester mon site
WordPressXSSRCEVulnérabilitéCVE

xss2shell (CVE-2026-64638) : la petite sœur de wp2shell exposant votre WordPress

xss2shell (CVE-2026-64638) touche toutes les versions de WordPress avant la 7.0.3 et peut mener à la prise de contrôle complète d’un site. Ce que la faille change pour votre organisation, comment vérifier votre exposition et comment vous protéger.

par Thibaud Robin19 min de lecture
xss2shell (CVE-2026-64638) : la petite sœur de wp2shell exposant votre WordPress

Pourquoi xss2shell mérite votre attention immédiate

Trois semaines à peine après wp2shell, le cœur de WordPress encaisse une seconde faille de premier plan. Elle porte un nom, xss2shell, un identifiant, CVE-2026-64638, et un point de départ d'une banalité déconcertante : la page de connexion, celle que la quasi-totalité des sites laisse ouverte sur Internet. Une seule tentative de connexion ratée suffit à injecter du JavaScript dans le navigateur d'un administrateur. De là, la chaîne remonte jusqu'à l'exécution de code PHP sur le serveur.

Le correctif est disponible depuis le 6 août 2026 avec WordPress 7.0.3. L'exploit complet, celui qui va jusqu'au shell, n'a pas encore été publié sous forme prête à l'emploi. Mais le mécanisme est documenté dans le détail, un template de détection circule déjà, et la surface concernée dépasse 40 % du web. La fenêtre est courte. Cet article décortique la chaîne marche par marche, vous donne un PoC prêt à l'emploi pour tester votre exposition (à la main puis avec Nuclei), et détaille la remédiation.

XSS aujourd'hui, shell demain : les deux visages de la faille

Le nom dit toute la trajectoire : une XSS (cross-site scripting) qui finit en shell. Une nuance mérite d'être posée d'emblée, car c'est elle qui sépare la panique de la remédiation méthodique.

La première marche, la XSS réfléchie non authentifiée, est solidement établie et large. Elle touche une installation WordPress par défaut, sans le moindre plugin, dès qu'un attaquant atteint l'écran de connexion. La seconde partie, le passage de cette XSS à l'exécution de code, est bien réelle mais conditionnelle : elle suppose qu'un administrateur consulte au mauvais moment une page piégée, et que quelques réglages restés par défaut soient en place. Les deux ne pèsent pas le même poids en probabilité, et un plan de remédiation sérieux en tient compte.

La faille a été découverte et reproduite par l'équipe de recherche pwn.ai, qui reste la source primaire à recouper. Le score CVSS 3.1 retenu est de 7,5 (élevé), et son vecteur dit la nuance : AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:H. L'attaque se mène à distance et sans le moindre privilège (PR:N), mais avec une complexité élevée (AC:H) et une interaction utilisateur requise (UI:R), précisément parce que l'exécution de code réclame qu'un administrateur agisse à son insu. Un point à garder en tête pour la suite : la version seule ne prouve pas l'exploitabilité, c'est la réflexion de la charge qui la confirme.

Au cœur du bug : deux nettoyeurs HTML qui se contredisent

Toute la faille tient dans un désaccord entre deux morceaux de code qui, pris séparément, font correctement leur travail. C'est un grand classique de la sécurité applicative : la confusion de parseurs. Deux composants lisent la même chaîne de caractères et n'en tirent pas la même conclusion.

strip_tags() contre wp_kses_post() : l'espace qui change tout

Quand une connexion échoue sur un nom d'utilisateur inexistant, WordPress réaffiche ce nom dans un message d'erreur, sur la page de connexion. Avant l'affichage, la valeur passe par sanitize_user(), qui en mode non strict s'appuie sur wp_strip_all_tags(), lui-même bâti sur la fonction native de PHP strip_tags().

Or strip_tags() a une particularité peu connue : elle ne reconnaît une balise que si le chevron ouvrant est collé au nom de la balise. Glissez une espace juste après le <, et elle laisse tout passer.

strip_tags('<area id=x>');   // ""              → la balise est retirée
strip_tags('< area id=x>');  // "< area id=x>"  → la chaîne survit intacte

La chaîne survivante n'est pas encore dangereuse. Elle le devient à l'étape suivante. WordPress rend le message d'erreur en le passant par wp_kses_post(), son nettoyeur HTML de référence. Et celui-là, contrairement à strip_tags(), considère < area comme une balise <area> parfaitement valide.

Du HTML injecté au JavaScript : le DOM clobbering

Injecter un <area> ne donne pas directement du JavaScript : wp_kses_post() interdit les attributs événementiels comme onclick, et la balise <script> ne passe pas. L'attaquant procède autrement, par une technique nommée DOM clobbering : au lieu d'exécuter du code, il fabrique des éléments HTML dont les identifiants entrent en collision avec des variables JavaScript que WordPress attend.

L'écran de connexion charge user-profile.js, un script du cœur destiné au flux de réinitialisation de mot de passe. Au chargement de la page, ce script cherche un bouton de génération de mot de passe et le déclenche automatiquement. En imbriquant les bons id dans les balises injectées, l'attaquant fait pointer la variable ajaxurl, que le script croit contenir une URL, vers son élément <area>. Quand le script lit cette variable comme une chaîne, JavaScript appelle la méthode toString() de l'élément, qui renvoie tout simplement son attribut href. L'URL de destination de la requête AJAX est désormais celle que l'attaquant a écrite.

Le rebond par l'API REST : _jsonp et exécution dans l'origine

Reste à faire exécuter du JavaScript arbitraire dans l'origine du site. L'attaquant dirige la requête vers l'API REST de WordPress, en abusant du support JSONP :

/?rest_route=/&_method=GET&_jsonp=<callback>&_envelope=1

L'API REST accepte un paramètre _jsonp tant qu'il correspond à ^[a-zA-Z0-9_.]+$, puis enveloppe sa réponse dans un appel de fonction : /**/<callback>({...}). Servie avec un Content-Type: application/javascript, cette réponse est exécutée par jQuery via globalEval(). Le détail décisif est le point : les caractères . autorisés dans le callback permettent de viser une méthode précise, par exemple window.opener.approve.click. On ne se contente plus d'appeler alert, on actionne un élément choisi de la page.

De la XSS au webshell : la chaîne d'escalade complète

C'est ici que la chaîne devient réellement dangereuse, et ici aussi qu'elle devient conditionnelle. Le passage se fait en abusant, une fois de plus, de fonctionnalités parfaitement légitimes de WordPress.

  • SOME (Same-Origin Method Execution). L'attaquant ouvre la page authorize-application.php dans la session d'un administrateur connecté, puis, grâce au callback JSONP décrit plus haut, déclenche le clic sur le bouton d'autorisation. WordPress crée alors un mot de passe d'application et redirige vers l'URL de retour contrôlée par l'attaquant, qui récupère au passage l'identifiant et le mot de passe en clair.
  • Un compte de service tout neuf. Muni de ce mot de passe d'application, l'attaquant parle à l'API REST en authentification Basic, avec les droits de l'administrateur, sans avoir jamais connu son mot de passe réel ni son cookie de session.
  • Le dépôt du webshell. Il lui reste à extraire un nonce de la page d'upload de plugin, puis à envoyer une archive ZIP piégée via POST /wp-admin/update.php?action=upload-plugin. Le ZIP est décompressé dans wp-content/plugins/, où les fichiers PHP sont directement accessibles et exécutables. Le shell est en place.

Le schéma ci-dessous résume l'enchaînement complet, en sept étapes réparties sur les deux phases décrites plus haut.

Kill chain

De la page de connexion à l'exécution de code

Phase 1XSS réfléchie, sans authentification
  1. 1

    Écran de connexion exposé

    wp-login.php est ouvert sur Internet, sans aucun compte requis.

  2. 2

    Confusion de parseurs

    une balise injectée survit à strip_tags() mais wp_kses_post() la rend comme du HTML valide.

  3. 3

    DOM clobbering

    les identifiants injectés détournent la variable ajaxurl du script user-profile.js.

  4. 4

    Rebond par l'API REST

    le paramètre _jsonp exécute le JavaScript de l'attaquant dans l'origine du site.

Phase 2Escalade vers le shell (conditionnelle)
  1. 5

    Vol d'un mot de passe d'application

    un administrateur connecté ouvre une page piégée, WordPress crée et livre le secret.

  2. 6

    Compte de service administrateur

    l'attaquant parle à l'API REST en Basic auth, avec les pleins droits.

  3. Dépôt du webshell

    un plugin ZIP piégé est décompressé dans wp-content/plugins/ et exécute du code PHP.

La première phase, la XSS non authentifiée, est certaine et touche une installation par défaut. La seconde, l'escalade jusqu'au webshell, reste conditionnelle : elle suppose un administrateur connecté et quelques réglages laissés par défaut.

Suis-je vulnérable ? Versions touchées et correctif

Sur ce point, l'avis officiel est catégorique : la faille affecte toutes les versions de WordPress. Le correctif est sorti avec la 7.0.3 et a été rétroporté jusqu'à la branche 4.7, par courtoisie pour les sites restés sur une version majeure ancienne.

Version WordPressStatutCorrectif
Toute version < 7.0.3Dans la plage historiquement affectée, statut à confirmer selon le dernier correctif de sa branche7.0.3, ou la dernière version corrective de votre canal (rétroportage jusqu'à 4.7)
7.0.3 et au-delàCorrigé avec certitudeDéjà appliqué

Le correctif est d'une sobriété exemplaire : il échappe le nom d'utilisateur avec esc_html() au point d'interpolation, dans wp-includes/user.php, si bien que la valeur est encodée avant même d'atteindre l'un ou l'autre des deux nettoyeurs. Le désaccord entre strip_tags() et wp_kses_post() existe toujours ailleurs dans PHP, mais il ne débouche plus sur cette page.

Proof of concept : testez votre exposition en 2 minutes

La bonne nouvelle, c'est que xss2shell se teste sans exploit destructeur. La première marche, la XSS réfléchie, se confirme en envoyant une charge inoffensive et en regardant si elle ressort échappée ou non. Aucune création de compte, aucune écriture, aucune exécution : on observe simplement le reflet. Trois étapes suffisent, un terminal et curl pour les deux premières, nuclei pour scanner un parc entier.

Étape 1 : confirmer que la cible est bien un WordPress

Inutile de sonder ce qui n'est pas un écran de connexion WordPress. Deux marqueurs présents dans le HTML de wp-login.php le confirment d'un coup d'œil :

curl -s https://VOTRE-SITE/wp-login.php | grep -o 'loginform\|wp-core-ui'

Si la commande affiche loginform et wp-core-ui, vous tenez bien un login WordPress standard. Sinon, la cible n'est pas concernée par ce test, ou l'écran de connexion est déplacé ou protégé en amont.

Étape 2 : le test manuel de réflexion (curl)

Le principe reproduit celui du template public. On poste sur wp-login.php une charge qui contient une balise < area id=ajaxurl> (l'espace après le < est exactement ce qui la fait survivre à strip_tags()), puis on cherche cette balise dans la réponse. Si elle ressort sans être échappée, le site est vulnérable.

Voici, décodé, le seul champ qui compte, log : c'est la charge inoffensive, le reste du corps est un login bidon.

log = < area id=ajaxurl href=/?rest_route=/&_method=GET&_jsonp=alert>
      < div id=color-picker class=reset-pass-submit>
      < button class="wp-generate-pw color-option">X
pwd = x
wp-submit = Log In

En pratique, ces valeurs partent URL-encodées dans une seule requête :

curl -s -X POST https://VOTRE-SITE/wp-login.php \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  --data 'log=%3C%20area%20id%3Dajaxurl%20href%3D%2F%3Frest_route%3D%2F%26_method%3DGET%26_jsonp%3Dalert%3E%3C%20div%20id%3Dcolor-picker%20class%3Dreset-pass-submit%3E%3C%20button%20class%3D%22wp-generate-pw%20color-option%22%3EX&pwd=x&wp-submit=Log+In' \
  | grep -io '<area[^>]*id=["'"'"']*ajaxurl[^>]*>'

Lecture du résultat :

  • La commande affiche une balise du type <area id=ajaxurl href=/?rest_route=/&_method=GET&_jsonp=alert> : la charge est ressortie non échappée, votre site est vulnérable. Mettez à jour sans attendre.
  • La commande n'affiche rien : la charge n'est pas ressortie telle quelle. C'est un bon signe, mais pas une preuve de correctif à elle seule. La même absence peut venir d'un WAF qui nettoie le champ avant WordPress, d'une redirection, d'un écran de connexion personnalisé ou d'un simple aléa réseau. Pour conclure, recoupez avec la version (7.0.3 ou la dernière corrective de votre branche).

C'est tout le test. On ne pousse volontairement pas la chaîne plus loin : reproduire l'escalade complète sur une cible de production revient à l'exploiter pour de bon.

Étape 3 : le scan automatisé avec Nuclei

Pour vérifier un parc entier plutôt qu'une URL, la communauté a publié un template Nuclei (auteur : FLX | Nick Vidovic). Il enchaîne les deux mêmes contrôles : il confirme d'abord que la cible est un écran de connexion WordPress, puis vérifie que la charge ressort non échappée. Si le template est déjà dans votre dépôt local :

nuclei -u https://VOTRE-SITE -id CVE-2026-64638

Sinon, enregistrez le fichier ci-dessous sous CVE-2026-64638.yaml et pointez-le explicitement :

nuclei -u https://VOTRE-SITE -t CVE-2026-64638.yaml
id: CVE-2026-64638
 
info:
  name: WordPress Core < 7.0.3 - Preauth Reflected XSS (XSS2Shell)
  author: FLX | Nick Vidovic (greenhats)
  severity: high
  reference:
    - https://pwn.ai/blog/xss2shell
    - https://nvd.nist.gov/vuln/detail/CVE-2026-64638
  classification:
    cvss-metrics: CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:H
    cvss-score: 7.5
    cwe-id: CWE-79
  tags: cve,cve2026,wordpress,xss,rce
 
flow: http(1) && http(2)
 
http:
  - method: GET
    path:
      - '{{BaseURL}}/wp-login.php'
    matchers:
      - type: word
        part: body
        words:
          - 'loginform'
          - 'wp-core-ui'
        condition: and
        internal: true
 
  - raw:
      - |
        POST /wp-login.php HTTP/1.1
        Host: {{Hostname}}
        Content-Type: application/x-www-form-urlencoded
 
        log=%3C%20area%20id%3Dajaxurl%20href%3D%2F%3Frest_route%3D%2F%26_method%3DGET%26_jsonp%3Dalert%3E%3C%20div%20id%3Dcolor-picker%20class%3Dreset-pass-submit%3E%3C%20button%20class%3D%22wp-generate-pw%20color-option%22%3EX&pwd=x&wp-submit=Log+In
    matchers-condition: and
    matchers:
      - type: regex
        part: body
        regex:
          - '<area[^>]*id=["'']?ajaxurl'
      - type: status
        status:
          - 200

Un template qui remonte la cible marque un site vulnérable. À l'inverse, l'absence de sortie signifie seulement que la réflexion n'a pas été observée : ce n'est pas synonyme de « corrigé ». Un WAF, un timeout, une redirection ou un écran de connexion personnalisé donnent aussi zéro résultat. Seule la version (7.0.3 ou plus) tranche avec certitude.

Que faire maintenant : le plan de remédiation

Par ordre d'efficacité :

  1. Mettez à jour vers WordPress 7.0.3, ou vers le dernier correctif de votre branche si vous êtes resté sur une version majeure antérieure. C'est la seule remédiation qui ferme réellement la faille.
  2. Restreignez l'accès à wp-login.php par IP, par une authentification en amont ou par une règle serveur. Vous coupez le vecteur d'accès initial et, au passage, une bonne partie du bruit des attaques par force brute.
  3. Réduisez la surface d'escalade si le patch ne peut pas être appliqué tout de suite : désactivez les mots de passe d'application si vous ne les utilisez pas, posez DISALLOW_FILE_MODS pour interdire l'installation de plugins depuis l'interface, et vérifiez que PHP ne s'exécute pas depuis les répertoires de plugins inactifs.

Repérer les tentatives dans vos journaux

Vous n'avez pas besoin d'envoyer quoi que ce soit pour repérer une exploitation en cours. Trois signatures parlent d'elles-mêmes dans les logs :

  • un POST /wp-login.php dont le champ log contient un chevron < (encodé %3C), éventuellement suivi d'une espace ou d'un blanc encodé (%09, %0a, %0d) ;
  • une requête vers l'API REST portant un paramètre _jsonp= dont le callback contient un point, signe caractéristique de la phase d'escalade ;
  • un accès à authorize-application.php avec une URL de retour hors de votre domaine, suivi d'un POST /wp-admin/update.php?action=upload-plugin.

Côté indicateurs de compromission, surveillez les mots de passe d'application créés récemment, les nouveaux plugins inconnus et les comptes administrateurs inattendus.

Deux failles en trois semaines : la leçon pour les équipes sécurité

Deux failles WordPress critiques en trois semaines, sur des vecteurs entièrement différents, ça envoie un signal clair. wp2shell passait par l'API REST et une injection SQL. xss2shell part de l'écran de connexion et d'une confusion de parseurs. Le point commun n'est pas technique, il est organisationnel : les deux frappent des installations par défaut, sans plugin en cause, et les deux se propagent en quelques heures une fois publiques.

Le pentest annuel ne voit ni l'une ni l'autre : une faille divulguée le 6 août n'existe pas dans un rapport daté du printemps. Le scanner de version attrape la partie facile, le numéro dans la plage, mais rate le contexte et se fait tromper dès que la version est masquée. Et le périmètre incomplet reste le talon d'Achille de tout le monde : c'est le vieux blog WordPress oublié sur un sous-domaine, celui que personne ne met à jour parce que personne ne se souvient qu'il existe, qui tombe en premier. Pour le trouver, encore faut-il cartographier tout son système d'information et faire l'inventaire de sa surface d'attaque externe.

Détecter xss2shell en continu avec Flawfence

Chez Flawfence, la logique est la même que pour wp2shell, et elle n'a pas eu à être réinventée dans la panique. Le moteur cartographie le périmètre, identifie chaque instance WordPress exposée, y compris le Shadow IT oublié, situe sa version dans les plages vulnérables connues et vérifie l'exposition des vecteurs concernés, sans jamais envoyer de charge destructrice.

Concrètement, face à xss2shell, Flawfence :

  • découvre vos WordPress exposés, sur tous vos domaines et sous-domaines, y compris ceux que vous aviez oubliés ;
  • situe la version sous la 7.0.3 et signale le cas des branches anciennes à recouper avec leur dernier correctif rétroporté ;
  • sonde l'écran de connexion avec le marqueur de réflexion, qui confirme la XSS lorsqu'une version vulnérable le laisse ressortir non échappé ;
  • surveille en continu, pour que l'apparition d'un nouveau sous-domaine vulnérable ou le retour en arrière d'une version déclenche une alerte, sans attendre le prochain audit.

Questions fréquentes sur xss2shell

Qu'est-ce que xss2shell ?

xss2shell est le surnom de CVE-2026-64638, une vulnérabilité du cœur de WordPress. Elle exploite une divergence entre deux fonctions de nettoyage HTML (strip_tags() et wp_kses_post()) pour placer une XSS réfléchie non authentifiée sur l'écran de connexion wp-login.php, puis la chaîne, sous certaines conditions, jusqu'à l'exécution de code PHP sur le serveur.

Quelles versions de WordPress sont vulnérables à xss2shell ?

Selon l'avis officiel, la faille affecte toutes les versions de WordPress antérieures à la 7.0.3. Le correctif est arrivé avec la 7.0.3, le 6 août 2026, et a été rétroporté sur chaque branche maintenue jusqu'à la 4.7. En pratique, tout site sous une version inférieure à la 7.0.3 doit être considéré comme concerné tant qu'il n'a pas pris la dernière version corrective de son canal.

xss2shell est-il activement exploité ?

Au moment où nous écrivons, aucun exploit complet menant au shell n'est public, et aucune exploitation de masse n'a été recensée. Mais le mécanisme est documenté publiquement, un template de détection circule déjà, et la surface est immense. Comme pour wp2shell, la fenêtre entre la divulgation et l'exploitation se compte en heures, pas en semaines. Patchez sans attendre.

xss2shell est-il aussi grave que wp2shell ?

Les deux sont critiques, mais différemment. wp2shell menait à une RCE non authentifiée en une seule requête anonyme, sur une installation par défaut. xss2shell atteint la XSS sans authentification tout aussi facilement, mais l'exécution de code exige en plus qu'un administrateur soit connecté et amené sur une page piégée, et que certains réglages restent par défaut. Le score CVSS 3.1 de xss2shell, 7,5, reflète cette escalade conditionnelle.

Comment tester xss2shell sans risquer de compromettre mon site ?

La XSS réfléchie se confirme avec une charge inoffensive : le marqueur < area id=ajaxurl> de la section Proof of concept ressort non échappé sur une version vulnérable, encodé sur une version corrigée. Il ne crée aucun compte et n'écrit rien. Testez-le en curl ou avec le template Nuclei, uniquement sur vos propres actifs, ou laissez le scanner de Flawfence le faire sous attestation sur l'honneur.

Bloquer wp-login.php avec un WAF suffit-il ?

C'est une mesure de mitigation utile, qui coupe le vecteur d'accès initial, mais elle reste contournable et ne corrige pas la cause. La seule remédiation complète est la mise à jour vers 7.0.3 ou le dernier correctif de votre branche.

En résumé

xss2shell rappelle une vérité inconfortable : les failles les plus dangereuses ne naissent pas toujours d'un code exotique, mais du désaccord entre deux fonctions qui, chacune de son côté, faisaient bien leur travail. Une espace après un chevron, deux nettoyeurs qui divergent, et l'écran de connexion le plus banal du web devient une porte d'entrée.

La bonne nouvelle est la même que pour wp2shell : détecter cette classe de failles ne demande ni exploit destructeur, ni pentest à 20 k€. Il faut un inventaire exhaustif de votre périmètre et une surveillance continue qui teste le comportement réel de vos actifs. C'est exactement ce que Flawfence fait, pour xss2shell comme pour la prochaine faille qui n'a pas encore de nom.

Vérifiez votre exposition à xss2shell dès maintenant, il ne vous faudra que 5 minutes.

Discutons de votre exposition externe

Demandez une démo personnalisée de Flawfence et découvrez votre niveau d’exposition réel.