
Laboratorio Docker para reproducir CVE-2025-11262, un XSS ciego almacenado sin autenticación en el plugin Link Whisper Free de WordPress. Incluye objetivos vulnerables y parcheados, script PoC y análisis de causa raíz.
Este repositorio contiene un laboratorio Docker local para reproducir CVE-2025-11262, una vulnerabilidad de cross-site scripting almacenada no autenticada que afecta al plugin de WordPress Link Whisper Free.
El laboratorio compara dos versiones del plugin:
| Servicio | Versión del plugin | Propósito | URL |
|---|---|---|---|
vuln | 0.9.0 | Objetivo vulnerable | http://127.0.0.1:8081 |
patched | 0.9.1 | Objetivo parcheado de comparación | http://127.0.0.1:8082 |
La cadena de vulnerabilidad demostrada es:
Petición REST no autenticada
→ se persiste un user_id controlado por el atacante
→ un usuario privilegiado de WordPress abre la página de Suscripción AI de Link Whisper
→ el valor almacenado se renderiza en un contexto JavaScript de administración
→ alert("CVE-2025-11262-LAB") se ejecuta en la versión vulnerable
El atacante no necesita estar autenticado para plantar el payload almacenado. El JavaScript se ejecuta posteriormente cuando un usuario privilegiado de WordPress abre la página de administración afectada.
Este laboratorio está diseñado únicamente para investigación local controlada, comprensión a nivel de código fuente y demostración en portafolio.
| Afirmación | Evidencia | Cómo verificar en este laboratorio |
|---|---|---|
Link Whisper Free 0.9.0 es vulnerable. | Los avisos públicos identifican las versiones de Link Whisper Free hasta 0.9.0 inclusive como afectadas. | Ejecute el PoC contra http://127.0.0.1:8081 y abra la URL de administración impresa. |
Link Whisper Free 0.9.1 contiene la corrección. | Los avisos públicos y los datos del registro de cambios identifican 0.9.1 como la versión parcheada. | Ejecute el mismo PoC contra http://127.0.0.1:8082; no debería aparecer ninguna alerta. |
| La plantación del payload no requiere autenticación. | El PoC envía una solicitud POST sin cookies de WordPress, inicio de sesión ni nonce. | Inspeccione poc/poc.py; solo requiere --url. |
| El impacto visible se desencadena en el área de administración de WordPress. | El valor almacenado se renderiza cuando un usuario privilegiado abre la página de Suscripción AI de Link Whisper. | Después de ejecutar el PoC, inicie sesión como administrador y abra la URL de administración impresa. |
El objetivo parcheado puede seguir devolviendo "ok" en la capa HTTP. | Las pruebas locales mostraron que ambos objetivos pueden devolver "ok"; la diferencia significativa es si el payload se persiste y ejecuta. | Compare el comportamiento del navegador en 8081 y 8082. |
Esta es la sección Resumen de la Causa Raíz para reemplazar directamente en el README, es segura para el público, no menciona archivos internos como vuln_detail.txt y se basa en el laboratorio/fuente que estás utilizando ahora.
CVE-2025-11262 es causado por una cadena de inyección de JavaScript almacenada en Link Whisper Free 0.9.0.
El problema no es una única llamada de escape faltante. Es una cadena de múltiples comportamientos inseguros:
Endpoint REST no autenticado
→ validación insuficiente de user_id
→ almacenamiento persistente en wpil_ai_access_user_id
→ renderizado inseguro en un contexto JavaScript de administración
→ XSS almacenado cuando un usuario privilegiado abre la página de Suscripción AI
Link Whisper Free registra un endpoint REST de autenticación AI bajo el espacio de nombres REST del plugin:
const REST_SLUG = 'link-whisper';
const AI_AUTH = 'ai-auth';
El endpoint se registra como una ruta POST:
register_rest_route(self::REST_SLUG, self::AI_AUTH, [
'methods' => 'POST',
'callback' => [
$this,
'ai_auth_handler'
],
'permission_callback' => "__return_true",
'show_in_index' => false
]);
Debido a que el callback de permiso es __return_true, el endpoint es accesible sin autenticación.
En el laboratorio, el endpoint efectivo es:
/wp-json/link-whisper/ai-auth
Esto significa que un atacante no autenticado puede enviar una solicitud al endpoint sin sesión de WordPress, nonce ni cuenta de administrador.
En Link Whisper Free 0.9.0, el manejador lee parámetros controlados por el atacante desde la solicitud REST:
public function ai_auth_handler( WP_REST_Request $request )
{
if(!empty($request->get_param('access_token'))){
$token = $request->get_param('access_token');
$user_id = $request->get_param('user_id');
$uid = (int)$request->get_param('uid');
$uemail = $request->get_param('uemail');
if(!empty($token) && false !== strpos($token, 'ai-')){
update_option('wpil_ai_access_token', Wpil_Toolbox::encrypt($token));
update_option('wpil_ai_access_user_id', $user_id);
update_option('wpil_ai_access_user_email', $uemail);
update_user_meta($uid, 'wpil_ai_access_user_id', $user_id);
update_user_meta($uid, 'wpil_ai_access_user_email', $uemail);
update_option('wpil_ai_access_authorized', true);
}
return 'ok';
}
return new WP_Error(400, 'Bad request', [ 'status' => 404 ]);
}
El comportamiento vulnerable es la condición de validación débil:
if(!empty($token) && false !== strpos($token, 'ai-')){
Esto solo verifica si el token de acceso suministrado contiene la cadena ai-.
No hay una validación estricta de user_id antes de almacenarlo:
update_option('wpil_ai_access_user_id', $user_id);
Como resultado, un JavaScript controlado por el atacante puede persistirse en la tabla de opciones de WordPress.
El valor user_id controlado por el atacante se almacena en la opción de WordPress:
wpil_ai_access_user_id
En este laboratorio, el PoC envía el siguiente payload local:
</script><script>alert("CVE-2025-11262-LAB")</script>
En el servicio vulnerable, el payload se almacena como el valor de wpil_ai_access_user_id.
El atacante no necesita estar autenticado para plantar el payload. El payload se planta a través del endpoint REST no autenticado.
El valor almacenado se recupera posteriormente a través de la lógica de configuración del plugin:
public static function get_linkwhisper_ai_user_id(){
return get_option('wpil_ai_access_user_id', '');
}
El valor se asigna a $ai_id y se renderiza en la página de administración de Suscripción AI.
En Link Whisper Free 0.9.0, el valor se inserta directamente en una cadena JavaScript:
body: JSON.stringify({
ai_id: "<?php echo $ai_id;?>",
subscription_id: "<?php echo ((!empty($sub)) && isset($sub->subscription_id)) ? $sub->subscription_id: null;?>"
})
Debido a que $ai_id no se escapa antes de insertarse en el contexto JavaScript, un payload almacenado puede salir de la cadena prevista y ejecutar JavaScript cuando se abre la página de administración.
Con el payload del laboratorio, la salida renderizada vulnerable se vuelve equivalente a:
body: JSON.stringify({
ai_id: "</script><script>alert("CVE-2025-11262-LAB")</script>",
subscription_id: ""
})
En un navegador, la etiqueta </script> de cierre inyectada termina el bloque de script original, y el bloque <script> inyectado se ejecuta.
El payload es plantado por un atacante no autenticado, pero la ejecución requiere que un usuario privilegiado de WordPress abra la página de administración afectada:
/wp-admin/admin.php?page=link_whisper_ai_subscription
En este laboratorio, la página afectada se abre como administrador de WordPress para activar el diálogo de alerta.