
Exploit de preuve de concept pour CVE-2026-64638 : XSS réfléchi dans la connexion WordPress, chaîné à du DOM clobbering pour parvenir à la prise de contrôle d'un compte administrateur et à l'exécution de code à distance.
Logiciel : WordPress Core ≤ 7.0.2 (toutes les versions antérieures à 7.0.3)
CVSS : 8.9 (Élevé)
CWE : CWE-79 — Mauvaise neutralisation des entrées lors de la génération de pages web
Authentification requise : Aucune (pré-auth)
Interaction utilisateur : Active (l'admin doit cliquer sur 1 lien)
Impact : XSS → prise de contrôle du compte → exécution de code à distance
WordPress est le système de gestion de contenu le plus populaire au monde, représentant plus de 40 % de tous les sites web sur Internet. Chaque site WordPress possède une page de connexion à /wp-login.php — c'est un point d'accès public que n'importe qui peut consulter sans authentification.
Lorsqu'un utilisateur saisit un nom d'utilisateur incorrect, WordPress affiche un message d'erreur contenant exactement le nom d'utilisateur qu'il vient de taper : « Le nom d'utilisateur X n'est pas enregistré sur ce site. » Le problème réside dans le fait que la valeur du nom d'utilisateur est placée directement dans la réponse HTML sans passer par aucune fonction d'échappement — un attaquant n'a qu'à saisir du HTML/JavaScript à la place d'un vrai nom d'utilisateur, et le code sera exécuté dans le navigateur.
Il s'agit d'une faille de type XSS réfléchi — la charge utile est contenue dans la requête et renvoyée à l'identique par le serveur dans le HTML. Ce qui la rend dangereuse, c'est que la faille se trouve sur la page de connexion — un endroit fréquemment consulté par les admins, où les cookies de session admin peuvent être volés.
L'équipe de recherche a également découvert que ce XSS peut être chaîné avec une vulnérabilité de DOM clobbering dans l'emoji-loader de WordPress, permettant de charger du JavaScript depuis un serveur externe. À partir de là, un attaquant peut créer un nouveau compte admin → installer une extension contenant un webshell → exécuter du code PHP sur le serveur. Cette chaîne d'exploitation est appelée XSS2Shell.
| Attribut | Valeur |
|---|---|
| ID CVE | CVE-2026-64638 |
| Score CVSS | 8.9 (Élevé) |
| Logiciel | WordPress Core ≤ 7.0.2 |
| Authentification | Aucune requise (pré-auth) |
| Interaction utilisateur | Nécessite 1 clic (l'admin clique sur le lien) |
| Complexité d'attaque | Élevée |
| Correctif | WordPress 7.0.3 (08/06/2026) |
| Rapporteur | équipe pwn.ai via HackerOne |
| Rapport HackerOne | #3877102 |
WordPress charge le support des émojis sur chaque page (y compris la page de connexion) via le fichier emoji-loader.js. Ce script lit la configuration à partir d'un élément possédant id="wp-emoji-settings" :
// Before patch (vulnerable)
const settings = JSON.parse(
document.getElementById('wp-emoji-settings').textContent
);
document.getElementById() renvoie le premier élément du DOM dont l'identifiant correspond. Si un attaquant injecte une <div id="wp-emoji-settings"> avant la balise script d'origine, getElementById lira le contenu de l'attaquant au lieu de la configuration réelle. Cette technique est appelée DOM clobbering — le fait d'écraser le comportement de JavaScript en injectant des éléments HTML.
La configuration des émojis contient une URL permettant de charger un fichier JavaScript (concatemoji). L'attaquant contrôle cette URL → charge un fichier JS depuis un serveur externe → exécute du code arbitraire dans le contexte du navigateur.
Une fois l'exécution de JavaScript dans le contexte admin obtenue, l'attaquant dispose de tous les privilèges admin de WordPress :
/wp-admin/user-new.php avec la session admin/wp-admin/plugin-install.phpN'importe laquelle de ces 3 méthodes permet d'exécuter du code PHP sur le serveur — autrement dit un RCE.
À partir du commit de correctif 0d6d42e sur wordpress-develop, j'ai identifié 3 emplacements dans le fichier wp-includes/user.php où le nom d'utilisateur/l'e-mail est placé directement dans le message d'erreur :
# View diff between vulnerable and patched versions
git diff 7.0.2..7.0.3 -- src/wp-includes/user.php
Emplacement 1 — ligne 189 (le nom d'utilisateur n'existe pas) :
Avant :
// BEFORE (vulnerable):
sprintf(
__( 'The username <strong>%s</strong> is not registered...' ),
$username // ← no escaping
)

Après :
// fixed:
sprintf(
__( 'The username <strong>%s</strong> is not registered...' ),
esc_html( $username ) // ← escaped
)
Emplacement 2 — ligne 216 (mot de passe incorrect) :
Avant :

// BEFORE:
'<strong>' . $username . '</strong>' // ← no escaping
Après :
// AFTER:
'<strong>' . esc_html( $username ) . '</strong>'
Emplacement 3 — ligne 299 (mot de passe incorrect pour l'e-mail) :
Avant :

// BEFORE:
'<strong>' . $email . '</strong>' // ← no escaping
Après :
// AFTER:
'<strong>' . esc_html( $email ) . '</strong>'
Flux de données de la requête POST au message d'erreur :
$_POST['log'] ← user input from login form
↓
wp_signon() [user.php:51]
$credentials['user_login'] = wp_unslash($_POST['log']) ← only removes backslashes
↓
wp_authenticate($username, $password) [pluggable.php:689]
$username = sanitize_user($username) ← strips HTML tags, but has a bypass
↓
wp_authenticate_username_password() [user.php:153]
get_user_by('login', $username) ← user not found
↓
sprintf('The username <strong>%s</strong>...', $username) ← XSS!
↓
WP_Error → login_header() → wp_admin_notice()
wp_kses_post(...) ← filters HTML but allows <div>, <a>, through
↓
HTML response → browser render → JavaScript execute
WordPress dispose de 2 couches de filtrage avant que le nom d'utilisateur n'atteigne le HTML :
Couche 1 : sanitize_user() — appelle strip_tags() pour supprimer les balises HTML. Cependant, strip_tags() de PHP a des limitations connues : un format de balise non standard peut contourner le filtre.
Couche 2 : wp_kses_post() — laisse passer un sous-ensemble sûr de HTML, notamment <div>, <a>, `` avec certains attributs (mais supprime les gestionnaires d'événements comme onerror, onload). Point crucial : wp_kses_post autorise <div id="wp-emoji-settings"> — précisément l'élément nécessaire pour le DOM clobbering.
L'équipe pwn.ai a trouvé un moyen de contourner les deux couches pour injecter une charge utile exploitable. Les détails techniques précis n'ont pas été divulgués publiquement.
Pour confirmer visuellement que le nom d'utilisateur va directement dans le HTML sans échappement, j'ai utilisé Xdebug + VS Code pour placer des points d'arrêt aux endroits clés de la chaîne d'exécution.