wp2shell: RCE pre-auth nel core di WordPress — e la trappola della patch
wp2shell (CVE-2026-63030 + CVE-2026-60137) è una RCE non autenticata nell'API batch REST del core di WordPress. Perché 'abbiamo aggiornato' non significa 'siamo al sicuro', e come validare davvero l'esposizione.
Il 17 luglio 2026 il team di sicurezza di WordPress ha rilasciato un aggiornamento coordinato — 6.8.6, 6.9.5 e 7.0.2 — per chiudere wp2shell, una remote code execution non autenticata che funziona contro un'installazione stock, senza plugin, senza configurazioni particolari, senza login. Il giorno dopo gli identificativi CVE erano pubblici (CVE-2026-63030 e CVE-2026-60137) e, secondo The Hacker News, su GitHub era comparso un proof-of-concept funzionante. WordPress muove qualcosa come il 40% del web, e il codice vulnerabile è nel core — non in un plugin di terze parti — quindi non parliamo di una nicchia di vendor. Parliamo di gran parte del livello di pubblicazione di internet, raggiungibile pre-auth, in una sola richiesta.
La parte interessante non è la RCE. È la distanza tra "abbiamo lanciato l'aggiornamento" e "non siamo realmente sfruttabili" — che con wp2shell è più ampia di quanto la maggior parte dei team immagini.
wp2shell: due bug che contano solo insieme
Nessuna delle due metà di wp2shell è notevole da sola. In catena trasformano una richiesta HTTP anonima in esecuzione di codice.
La prima è CVE-2026-60137, una SQL injection (CWE-89) nel parametro author__not_in di WP_Query. Il parametro dovrebbe ricevere un array di ID autore. Passandogli una stringa al posto di un array, il controllo di tipo che si aspetta un array viene saltato e il valore finisce nella query senza sanitizzazione. Da sola è una falla reale ma condizionata — serve che un plugin o un tema inoltri input non fidato in quel parametro. WPScan l'ha classificata media, 5.9; CISA-ADP ha assegnato allo stesso difetto un 9.1 critico, perché le due valutazioni rispondono a domande diverse (il componente isolato contro ciò che abilita).
La seconda è CVE-2026-63030, una falla di route confusion (CWE-436, Interpretation Conflict) nell'endpoint batch dell'API REST. WordPress distribuisce /wp-json/batch/v1 nel core, attivo di default, raggiungibile senza autenticazione. Esiste per permettere a un client di raggruppare più chiamate REST in una sola richiesta. Internamente il controller traccia quelle sotto-richieste in array paralleli — uno per le richieste, uno per gli esiti di validazione. Secondo la ricerca di Searchlight Cyber / Assetnote firmata da Adam Kues, e il meccanismo ricostruito da The Hacker News, un errore in una sotto-richiesta sfasa quegli array di una posizione. Da quel momento ogni richiesta viene validata rispetto al contesto della richiesta precedente — così una sotto-richiesta gira sotto un handler e una decisione di autorizzazione che appartengono a un'altra chiamata.
Quel disallineamento è tutto il gioco. Consente a un chiamante non autenticato di raggiungere un handler privilegiato e spingerlo dritto nella SQL injection di author__not_in, che i ricercatori scalano poi a RCE completa. Il record CNA di WPScan valuta la catena 9.8 critico (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H); CISA-ADP ha valutato il solo componente dell'endpoint batch 7.5. L'endpoint risponde anche nella forma sempre disponibile /?rest_route=/batch/v1, quindi non servono i permalink "belli" per raggiungerlo — un dettaglio da tenere a mente, perché smonta l'assunzione "tanto non esponiamo l'API REST" su cui molte guide di hardening ancora si appoggiano.
Immagina il controller batch come un guardaroba. La richiesta A consegna un tagliando valido; la richiesta B non consegna nulla. Un errore sposta la rastrelliera di un gancio. Ora il cappotto di B torna sul tagliando di A. Nessuno ha falsificato un tagliando. Il guardarobiere ha semplicemente dato il cappotto sbagliato alla persona sbagliata — e B esce vestito da amministratore.
Perché gli scanner a firma mancano la classe wp2shell
CWE-436 non è un bug da payload. Non esiste una singola stringa malevola che un WAF possa intercettare, perché il difetto vive nell'accoppiamento di due funzionalità — il routing batch e un'assunzione sul tipo di un parametro — non in una delle due presa singolarmente. È la stessa famiglia strutturale dell'HTTP request smuggling: un disallineamento di stato tra due componenti che, isolati, si comportano correttamente.
Questo ha conseguenze dirette su come lo trovi, e su come non lo trovi:
- Le regole WAF a regex/firma lo rilevano male. Il traffico di exploit può assomigliare a normali richieste batch ben formate. Cloudflare ha distribuito regole gestite al momento della disclosure, ma una regola che blocca la rotta batch è uno strumento grezzo, non il rilevamento del difetto.
- Gli scanner che confrontano le versioni producono falsa sicurezza. Uno scanner che legge la stringa di versione e la confronta con un intervallo dichiara "patchato" il sito nell'istante in cui il numero cambia — senza dirti nulla su se la rotta batch fosse mai raggiungibile, o se una shell fosse già stata piazzata prima dell'aggiornamento.
- La SQL injection è condizionata. Il sink
author__not_inrichiede che vi arrivi input non fidato; se quel percorso è attivo dipende dal contesto di richiesta che la route confusion fabbrica, non da un'ispezione statica del codice.
L'esposizione la stabilisci solo eseguendo la catena contro il bersaglio reale. È la differenza tra una stima e una prova, ed è il tema a cui wp2shell continua a tornare.
Perché il numero di versione di WordPress mente
Ecco la trappola. La difesa di punta di WordPress sono gli aggiornamenti automatici in background per le release minori, e la correzione coordinata è uscita proprio sui canali minori. Quindi il riflesso è: l'auto-update è attivo, il numero è avanzato, abbiamo finito.
Due siti sulla stessa identica versione vulnerabile possono avere una sfruttabilità reale completamente diversa, perché la catena ha precondizioni che la stringa di versione non cattura. L'analisi di Rapid7 nota che lo step di RCE richiede l'assenza di una cache oggetti persistente — un sito con davanti Redis o Memcached è molto più difficile da spingere dalla SQLi all'esecuzione di codice rispetto a uno senza. Se la rotta batch è raggiungibile, se un WAF copre la forma /?rest_route=, se gli auto-update sono stati di fatto disabilitati dal rollout scaglionato di un hosting gestito o bloccati dal workflow di un'agenzia — tutto sposta l'ago, e nulla di questo è nel banner.
| Due siti, stessa versione "vulnerabile" | Sito A | Sito B |
|---|---|---|
| Rotta batch raggiungibile pre-auth | Sì | Sì |
Fallback /?rest_route=/batch/v1 esposto |
Sì | Bloccato al proxy |
| Cache oggetti persistente (Redis/Memcached) | Assente | Redis |
| WAF copre specificamente la rotta batch | No | Sì |
| Auto-update effettivamente applicato | Bloccato dal workflow d'agenzia | Applicato |
| Esposizione reale | Sfruttabile fino a RCE | Non sfruttabile in pratica |
Ora moltiplica per la scala. WordPress a livello enterprise e d'agenzia è raramente un sito solo. Sono centinaia o migliaia — siti clienti su hosting condivisi, microsite di marketing, landing page di campagna e l'installazione dimenticata che un consulente ha messo su tre anni fa e mai dismesso. Una CMDB basata sul banner di versione non può rispondere a "sono chiusi davvero tutti", perché la risposta onesta dipende da un contesto per-sito che l'inventario non possiede. È un problema di validazione dell'esposizione, ed è esattamente il punto in cui la postura patch-and-pray si rompe.
Remediation
La correzione è semplice da enunciare e, su scala di flotta, difficile da dimostrare completa. Trattala come un report completo, non come un salto di versione.
1. Sono interessato?
Verifica la versione in esecuzione su ogni installazione — non quella che pensi sia deployata:
# WP-CLI, per sito
wp core version
# oppure dal filesystem
grep "wp_version =" wp-includes/version.php
Intervalli vulnerabili:
- 6.8.0 – 6.8.5 — solo il componente SQL injection (CVE-2026-60137).
- 6.9.0 – 6.9.4 e 7.0.0 – 7.0.1 — l'intera catena RCE pre-auth (CVE-2026-63030 + CVE-2026-60137).
Verifica se la rotta batch risponde senza autenticazione — testa entrambe le forme, perché quella indipendente dai permalink è quella che si dimentica:
curl -s -o /dev/null -w "%{http_code}\n" -X POST \
"https://TARGET/?rest_route=/batch/v1" \
-H "Content-Type: application/json" --data '{"requests":[]}'
Un 200/207 qui significa che l'endpoint è vivo e raggiungibile. Registra anche se è configurata una cache oggetti persistente (wp cache type / controlla in wp-config.php la presenza di un drop-in Redis/Memcached) — la sua assenza è la precondizione della RCE.
2. Patch — versioni corrette esatte
Aggiorna alla release corretta per il tuo canale (verbatim dal rilascio di sicurezza WordPress):
- 7.0.x → 7.0.2
- 6.9.x → 6.9.5
- 6.8.x → 6.8.6
- 7.1 beta → 7.1 Beta 2
Non lasciare i siti su 7.0.x/6.9.x aspettandoti che una cache oggetti persistente ti salvi — alza l'asticella sullo step RCE, non chiude la SQL injection.
3. Non puoi patchare subito? Controlli compensativi
- Blocca il POST alla rotta batch al bordo — nega sia
/wp-json/batch/v1sia/?rest_route=/batch/v1. Bloccare una sola forma lascia aperto il fallback, ed è l'errore più comune qui. - Metti davanti al sito una cache oggetti persistente (Redis/Memcached). Rende lo step da SQLi a RCE sensibilmente più difficile; trattalo come un dosso, non come una correzione.
- Restringi l'API REST ai contesti autenticati, dove la funzionalità del sito lo consente.
4. Caccia alla compromissione
Nessuna lista di IOC è stata pubblicata dal vendor, quindi caccia sul comportamento, mappato a MITRE ATT&CK:
- T1190 — Exploit Public-Facing Application. Richieste POST verso
/wp-json/batch/v1o/?rest_route=/batch/v1, in particolare raffiche con array di sotto-richieste malformati o un errore seguito subito da un 200/207. - T1505.003 — Web Shell. File
.phpnuovi o modificati sottowp-content/uploads/,wp-content/plugins/,wp-content/themes/o nel webroot. Eseguiwp core verify-checksumse confronta i file di plugin/tema con copie note-buone. - T1059 — Command and Scripting Interpreter. Il processo del web server che genera figli inattesi (
sh,curl,wget,php -r). - T1136.001 / T1078 — Create / Valid Accounts. Nuovi utenti amministratori, o scritture inattese nella tabella
wp_options(in particolare vociautoload) ewp_users. - T1041 / T1071.001 — Esfiltrazione / C2 sul web. Sessioni in uscita da un host web che storicamente solo serve — traffico verso ASN mai visti o volume POST inspiegabile.
5. Bonifica e verifica
La patch non sfratta una shell piazzata prima della patch. Dopo l'aggiornamento:
- Rimuovi eventuali web shell e file non autorizzati; ripristina core, plugin e temi da fonti note-buone.
- Ruota ogni segreto raggiungibile dal database e dalla configurazione: le chiavi di autenticazione e i salt in
wp-config.php(wp config shuffle-salts), le credenziali del database e qualsiasi chiave API salvata inwp_options. - Reimposta tutte le password amministrative e invalida le sessioni esistenti.
- Rivedi
wp-cron, i must-use plugin (wp-content/mu-plugins/) e i task pianificati alla ricerca di persistenza. - Ri-verifica che il sito sia pulito dopo aver patchato — perché, di nuovo, l'aggiornamento chiude la porta, non ripulisce la stanza.
Valida l'esposizione, non fidarti del banner
Tutto quanto sopra converge su un'unica domanda operativa: su tutto il tuo parco WordPress, quali siti sono realmente sfruttabili adesso — non quali stringhe di versione sono cambiate. Non è una domanda a cui uno scanner risponde leggendo un numero, ed è esattamente ciò che Zero Hunt è costruito per rispondere per prove.
Lo swarm di 10 agenti AI di Zero Hunt genera la catena di exploit per bersaglio con un LLM locale — non un payload prelevato da ExploitDB, che per una classe di state-desync come CWE-436 non esiste come firma stabile. Gli agenti Web ed Exploit ricostruiscono la route confusion batch fino al sink author__not_in contro la tua installazione, dentro un container Docker effimero irrobustito con gVisor, e restituiscono un verdetto che è la cosa reale: raggiungibile e sfruttabile, sì o no — tenendo conto della precondizione sulla cache oggetti e della copertura WAF che un controllo del banner ignora. Ogni finding è backtestato in AI Gym contro il corpus prima di girare, e firmato ECDSA al momento della scrittura, così la risposta per ciascun sito è un artefatto che un auditor può verificare, non un'affermazione. Poiché le campagne sono attivate dal cambiamento, un nuovo asset WordPress che compare sul perimetro — il microsite che il marketing ha messo online venerdì — innesca una validazione completa entro un'ora, l'unica difesa onesta per un parco che cresce più in fretta del suo inventario.
Quando lo sfruttamento inizia davvero, il modello di AI Traffic Analysis è la seconda rete: un motore di deep learning con quattro teste di inferenza, addestrato su miliardi di sequenze PCAP, in esecuzione locale sulla GPU dell'appliance a 2.7+ Gbit/s. Vede il pattern anomalo di richieste all'endpoint batch e il C2 in uscita della web shell sul filo mentre accade — non nel digest SIEM del mattino dopo, quando la shell è già a tre salti di distanza. Per una vulnerabilità in cui il numero di versione mente e il payload non ha firma, il filo e l'exploit validato sono i due posti in cui la verità vive ancora.
Se vuoi vedere come una validazione continua e basata su prove si mappa sul tuo parco WordPress, mettiti in contatto.