
PoC de RCE sin autenticación para el núcleo de WordPress — encadena CVE-2026-63030 (route-confusion desync en REST /batch/v1) con CVE-2026-60137 (SQLi en author__not_in) para obtener un shell sin autenticación. Solo pruebas autorizadas.
PoC de RCE no autenticado en el núcleo de WordPress que encadena CVE-2026-63030 + CVE-2026-60137. Solo pruebas autorizadas.
wp2shell.py es una prueba de concepto de un solo archivo que usa únicamente la biblioteca estándar y encadena
dos vulnerabilidades del núcleo de WordPress para lograr ejecución remota de código no autenticada:
| CVE | Componente | Fallo |
|---|---|---|
| CVE-2026-63030 | Manejador REST /batch/v1 | desync por confusión de rutas: el manejador de lotes pierde la sincronía entre su tabla de manejadores y su tabla de validación cuando una ruta de sub-solicitud no se puede analizar, de modo que una sub-solicitud se despacha a través de un manejador para el que nunca fue validada. |
| CVE-2026-60137 | WP_Query | inyección SQL escalar: el valor de author__not_in se inserta directamente en el SQL, por lo que una cadena que llegue hasta él es inyectable. |
Ninguna de las dos es RCE por sí sola. El desync es el medio de entrega (introduce una cadena del atacante en una consulta a la que nunca debería llegar); la inyección es la primitiva. Juntas le dan a un atacante sin autenticación un oráculo completo de lectura/escritura SQL y, a partir de ahí, una shell funcional.
⚠️ Solo pruebas autorizadas. Esto explota instalaciones de WordPress en producción. Úsalo solo contra sistemas que poseas o para los que tengas permiso explícito por escrito.
Vulnerables: 6.8.0–6.8.5, 6.9.0–6.9.4, 7.0.0–7.0.1 Corregidas: 6.8.6, 6.9.5, 7.0.2 (y posteriores) Reportadas por Adam Kues (Assetnote / Searchlight Cyber); la SQLi también acreditada a TF1T, dtro, haongo.
Solo 6.9.0–7.0.1 son capaces de RCE. En la línea 6.8.x el desync aún se dispara, pero la
rama que desalinearía el manejador choca con un fatal WP_Error::get_method() en su lugar —
así que 6.8.x es una denegación de servicio / caída, no una shell. La herramienta lo detecta
y te indica qué primitiva está disponible.
El exploit se construye en capas; cada una es un puente hacia la siguiente.
Desync (CVE-2026-63030). Una solicitud por lotes anida un segundo lote. Una sub-solicitud
lleva una ruta deliberadamente malformada (http://:, que wp_parse_url() rechaza).
En 6.9.0+ el fallo de análisis hace que las tablas de manejadores y de validación se desvíen una entrada,
de modo que la siguiente sub-solicitud se ejecuta bajo un manejador contra el que nunca fue validada: la
colección pública de posts.
Inyección (CVE-2026-60137). Esa llamada mal despachada llega a WP_Query con un
author__not_in controlado por el atacante. Un UNION ALL SELECT ahí no lee filas —
las fabrica, lo que nos permite forjar posts arbitrarios en el resultado de la consulta.
Puente SQL → escritura. Los posts fabricados llevan marcado [embed]. WordPress
los resuelve fielmente y los almacena en caché como filas reales de oembed_cache post-meta — convirtiendo
una inyección de solo lectura en una primitiva de escritura arbitraria.
Escalada de privilegios. Usando ese puente de escritura forjamos (a) un changeset
del personalizador con autoría del administrador real y (b) un post request autorreferencial. Juntos
fuerzan a WordPress a re-ejecutar su propio pipeline de solicitudes como ese administrador — una ventana
lo bastante larga para que una llamada encolada a cree un nuevo administrador.
Para el lado de lectura (--dump, resolución de prefijo, IDs en caché) la herramienta selecciona automáticamente el
oráculo más rápido que funcione y se detiene en el primer acierto:
union — UNION en banda: el post_title de la fila forjada lleva |||HEX(value)|||,
que se refleja textualmente en el cuerpo de la respuesta de /wp/v2/posts. Lee un valor completo
(de cualquier longitud) en una sola solicitud. Si UNION se refleja, los oráculos booleanos nunca se prueban.
bool — oráculo booleano X-WP-Total: el encabezado X-WP-Total de la sub-respuesta confundida
es count>0 para verdadero / 0 para falso. Un bit por solicitud (bisección).
time — SLEEP() basado en tiempo: el recurso de respaldo cuando el cuerpo se elimina y la
respuesta es un 500 (p. ej. un fatal posterior al despacho de un mu-plugin de caché). Funciona incluso en
objetivos que fallan en cada llamada REST.
--dump usa GROUP_CONCAT para extraer todo el conjunto de credenciales de una sola vez cuando UNION está
disponible.
Ninguna. Solo biblioteca estándar de Python 3.7+ — sin pip install.
chmod +x wp2shell.py
# 1. Is it vulnerable? (no injection, no writes, no account created)
./wp2shell.py http://target --check
# 2. Read-only credential dump — FIRST administrator + option secrets
./wp2shell.py http://target --dump
# …every administrator
./wp2shell.py http://target --dump all
# 3. Full RCE — create a temp admin, run a command, clean up
./wp2shell.py http://target --exec "id; uname -a"
./wp2shell.py http://target --user-list # full user table (logins/emails/roles)
./wp2shell.py http://target --content # every post type at status=any (private/draft)
./wp2shell.py http://target --content full # …including each item's raw body
./wp2shell.py -f targets.txt --check --target-threads 8
$wp$2y$… / $P$… / $2y$…) van directamente a hashcat
(-m 3200 para phpass, o el modo bcrypt de WP). WordPress 6.8+ usa bcrypt $wp$….--dump por defecto solo al primer administrador. Usa --dump all para la
lista completa de admins.--oracle time --dump para recuperar hashes.La ruta --exec está diseñada para no dejar nada atrás: el administrador temporal,
su usermeta, las filas oembed_cache fabricadas y la webshell subida se eliminan antes de
que la herramienta termine. No hay flag --keep — un operador nunca debe dejar una cuenta
de administrador activa creada por un atacante en el sistema de un cliente.
wp2shell.py — el exploit (un solo archivo, solo stdlib).El descubrimiento de las vulnerabilidades, su divulgación y el informe original de "wp2shell" pertenecen al equipo de investigación Searchlight Cyber / Assetnote:
/batch/v1WP_Query author__not_inEste repositorio es una implementación independiente de prueba de concepto de la cadena de explotación que ellos divulgaron; no reivindica el descubrimiento de las vulnerabilidades subyacentes. Si alguna atribución es incompleta o incorrecta, abre un issue y se corregirá.
Esta es una prueba de concepto de investigación de seguridad para pruebas de penetración autorizadas y validación defensiva. Ejecutarla contra sistemas que no posees o para los que no tienes permiso por escrito es ilegal. El autor no acepta ninguna responsabilidad por el mal uso.
POST /wp/v2/usersShell. Inicia sesión como el nuevo administrador, sube un plugin, ejecuta el comando. La cuenta, su meta, las filas oembed y la webshell se limpian después — no queda ninguna huella persistente.
| Flag | Propósito |
|---|
url / -f FILE | objetivo único, o un archivo de objetivos (una URL por línea) |
--check | solo verificación de vulnerabilidad — sin inyección, escrituras o cuentas |
--dump [all] | volcar el hash + secretos del primer admin; all = todos los admins |
--exec CMD | crear admin temporal → ejecutar CMD → limpiar |
--user-list | lectura en banda elevada de la tabla de usuarios completa (sin login, evita 2FA) |
--content [full] | lectura elevada de todos los tipos de post en status=any; full añade los cuerpos |
--oracle {auto,union,bool,time} | forzar un oráculo de extracción (por defecto auto) |
--prefix PFX | forzar el prefijo de tabla en lugar de resolverlo automáticamente |
--impersonate UID | suplantar este id de usuario en lugar del primer admin |
--threads N | sondas de extracción concurrentes dentro de un objetivo (por defecto 5) |
--target-threads N | escanear N objetivos de -f concurrentemente (por defecto 1) |
--proxy URL | enrutar a través de un proxy (p. ej. Burp http://127.0.0.1:8080) |
--timeout SEC | tiempo de espera por solicitud (por defecto 30) |
--verbose / -v | volcar sub-respuestas de lote crudas (depuración) |
| Flag | Propósito |
|---|
--form | enviar el lote como multipart/form-data (URL limpia, sin ?rest_route=) |
--permalink [PREFIX] | enrutar vía /<PREFIX>/batch/v1 (por defecto wp-json); mantiene el payload fuera de la URL y fuera de $_POST. Consejo: --permalink index.php/wp-json llega a REST vía PATH_INFO, evadiendo reglas ancladas en /wp-json |
--dummy N | anteponer N bytes de basura en el cuerpo para desbordar el búfer de inspección de un WAF |
--donor {widgets,categories,tags} | cambiar la ruta donante de la confusión si una da 500 (por defecto widgets) |
--per-page N | forjar per_page (-1 o un positivo grande devuelven todas las filas; usa un positivo grande si el objetivo da 500 con -1) |
--users-route PATH | variante de mayúsculas/consulta para la llamada de creación de usuario (/wp/v2/Users, …?_=1) |
--primer PATH | ruta de desync malformada alternativa si un WAF identifica la http://: por defecto |
--user-agent UA | fijar el UA (por defecto: un UA de navegador realista aleatorio por ejecución) |
--embed-base URL | sobrescribir la base de la URL oembed (evitar un loopback de auto-embed) |