
wp2shell (CVE-2026-63030 & CVE-2026-60137) - catena RCE completa
Proof-of-concept indipendente per l'iniezione SQL blind basata su confusione di route del batch REST non autenticato di WordPress, associata all'avviso wp2shell di Searchlight Cyber.
Questo repository non è il checker ufficiale di Searchlight Cyber. check conferma il percorso SQLi, read dimostra la lettura del database e shell apre una shell di comando basata su plugin, utilizzando credenziali di amministratore fornite o sfruttando prima il ponte SQLi-to-admin.
L'avviso di Searchlight Cyber elenca questi intervalli di esposizione RCE wp2shell:
| Intervallo di versione | Stato |
|---|---|
| <= 6.8.5 | Non interessato |
| 6.9.0 – 6.9.4 | Interessato |
| 7.0.0 – 7.0.1 | Interessato |
L'endpoint batch REST (/batch/v1) non è autenticato ed esegue diverse sotto-richieste in una singola chiamata, affidandosi al fatto che ogni sotto-richiesta sia convalidata e controllata per i permessi individualmente.
serve_batch_request_v1() costruisce due array paralleli — $matches (l'handler corrispondente per ogni sotto-richiesta) e $validation (il risultato della convalida per ogni sotto-richiesta) — poi indicizza entrambi con lo stesso offset durante l'invio. Una sotto-richiesta il cui percorso fallisce wp_parse_url() viene aggiunta a $validation ma non a $matches, quindi gli array si sfalsano e una sotto-richiesta viene inviata sotto l'handler di una sotto-richiesta differente. Questa è la confusione di route.
Il PoC annida la primitiva due volte:
POST /wp/v2/posts che contiene un corpo requests viene inviata sotto l'handler batch stesso. Essendo stata convalidata come richiesta di posts, la sua lista requests non viene mai controllata rispetto allo schema batch, quindi le sue sotto-richieste possono usare GET — la whitelist dei metodi viene bypassata.GET /wp/v2/posts/999999 trasporta parametri di query della collezione posts come author_exclude, orderby e per_page. L'ID 999999 non deve esistere; è solo un ID improbabile per far corrispondere la route item, il cui schema non convalida quei parametri riservati alla collezione. Il desincronismo invia quindi la stessa richiesta sotto get_items() di posts, dove author_exclude viene mappato alla variabile di query WP_Query author__not_in, che la build vulnerabile interpola in SQL come stringa.Il risultato è un'iniezione SQL blind basata su booleano e tempo raggiungibile pre-autenticazione. Questo PoC include anche la primitiva UNION di post fittizi utilizzata nella catena SQLi-to-admin.
Il percorso RCE implementato qui è:
wp_posts UNION fittizie per rendere contenuto controllato dall'attaccante attraverso una collezione di post. Il ponte di rendering usa la route item /wp/v2/posts/999999 — la stessa route che la lettura SQLi usa per raggiungere get_items().POST /wp/v2/users, creando un amministratore generato.I passaggi 1–5 sono pre-autenticazione; il passaggio di esecuzione comandi è l'upload autenticato di un plugin da amministratore.
Python 3.8+ e libreria standard. Nessuna dipendenza di terze parti.
Eseguirlo dalla directory del repository:
./wp2shell.py <comando> <url> [opzioni]
Oppure pip install . per ottenere un comando wp2shell nel tuo PATH.
Stampa prima i marcatori passivi di WordPress e gli indizi di versione pubblici, poi invia una sonda batch marker benigna. Un'implementazione batch vulnerabile restituisce HTTP 207 con il pattern di marker di confusione di route parse_path_failed, block_cannot_read e rest_batch_not_allowed.
La sonda marker si basa sul fix core di WordPress. La richiesta malformata /// crea parse_path_failed; una richiesta /wp/v2/posts funge da spacer consentito in batch; la route /wp/v2/block-renderer/... non è consentita in batch ma restituisce block_cannot_read se il suo handler viene raggiunto anonimamente; /batch/v1 restituisce rest_batch_not_allowed. Nelle build vulnerabili l'errore di parsing sposta gli array degli handler batch fuori sincrono, quindi la richiesta spacer viene inviata sotto l'handler block-renderer. Le build corrette mantengono gli array allineati, quindi questo esatto pattern di tutti e tre non dovrebbe apparire per la sonda creata.
Per impostazione predefinita, check si ferma qui e non invia un payload SQLi. Usa --confirm-sqli se vuoi anche una conferma SQLi attiva. La conferma prova prima la primitiva di lettura UNION e cade su sonde di temporizzazione accoppiate se la riflessione UNION non è disponibile.
I segnali sono indipendenti: un indizio di versione è solo un indizio, il pattern marker mostra la confusione di route, e --confirm-sqli mostra che un payload ha raggiunto il database. Un WAF può bloccare il payload, quindi una conferma fallita non prova l'assenza del bug.
./wp2shell.py check http://target
./wp2shell.py check target.txt # scansiona ogni URL nel file
./wp2shell.py read http://target # fingerprint del server
./wp2shell.py read http://target --preset users # login utente e hash password
./wp2shell.py read http://target --query "SELECT @@version"
Per impostazione predefinita l'estrazione è --technique auto, che prova i metodi disponibili in questo ordine:
WP_Post fittizia tramite UNION e legge il suo titolo dalla risposta REST come ||HEX(valore)||. Il payload usa la stessa route sorgente /wp/v2/posts/999999 con orderby=none e per_page=500 in modo che la riga fittizia sopravviva come post renderizzato. Una richiesta per valore.EXTRACTVALUE/UPDATEXML perde circa 15 byte per richiesta, quando il target riflette errori MySQL (ad es. WP_DEBUG_DISPLAY attivo).X-WP-Total della collezione posts come segnale vero/falso e non necessita di un valore riflesso.Forza uno con --technique union|error|blind. Questi percorsi di lettura non scrivono righe nel database.
Con --user e --password, shell effettua il login con credenziali di amministratore fornite e usa il comportamento di upload di plugin di WordPress.
Senza credenziali, shell esegue prima il ponte SQLi-to-admin pre-autenticazione, effettua il login come amministratore generato, poi carica la shell plugin.
./wp2shell.py shell http://target --user admin --password '<recuperata>' --cmd id
./wp2shell.py shell http://target --user admin --password '<recuperata>' -i # shell interattiva
./wp2shell.py shell http://target --cmd id # ponte pre-auth
./wp2shell.py shell http://target -i # interattivo pre-auth