# XSS2Shell: WordPress Preauth XSS to RCE Chain (CVE-2026-64638)

Pre-auth username field survives strip_tags but KSES re-parses it into live DOM, triggering user-profile.js auto-click and JSONP SOME to click admin approve, yielding Application Password and RCE.

- Payout: Research
- Severity: critical
- Program: WordPress Core
- Credited: PWNAI Research
- Disclosed: 2026-08-06
- CVE: CVE-2026-64638
- Write-up quality: 9/10
- Original: https://pwn.ai/blog/xss2shell
- Library page: https://bugbountylibrary.com/r/blog%3Apwn-ai-blog-xss2shell
- Tags: domain:web impact:ato impact:rce kind:research platform:research sev:critical tech:php technique:chained technique:parser-differential vuln:access-control vuln:rce vuln:xss

**Target:** wp-login.php username field, REST JSONP endpoint, authorize-application.php
**Root cause:** wp_strip_all_tags() (PHP strip_tags) does not treat '< area' as a tag while wp_kses_post() does, so attacker HTML survives sanitization and becomes live DOM; user-profile.js auto-clicks injected .wp-generate-pw inside .reset-pass-submit, and undefined===undefined guard passes on login page.
**Steps:**
1. Submit login with username `< area id=ajaxurl href=/?rest_route=/&_method=GET&_jsonp=alert>< div id=color-picker class=reset-pass-submit>< button class="wp-generate-pw color-option">X` and any password
2. strip_tags leaves `< area` intact; wp_kses_post re-parses into live <area>/<div>/<button> elements
3. user-profile.js ready handler auto-clicks .wp-generate-pw; delegated #color-picker handler fires; user_id and checkuser_id both undefined so guard passes
4. $.post(ajaxurl) resolves window.ajaxurl to injected <area> href; jQuery sends same-origin POST to REST JSONP
5. REST wraps response in callback; jQuery globalEval executes it in WordPress origin
6. For RCE: attacker page opens child window, navigates opener to authorize-application.php, child submits payload with callback `window.opener.approve.click`
7. Approve click creates Application Password redirected to attacker success_url
8. Use Application Password via Basic auth to publish page with unfiltered_html script
9. Script fetches upload-plugin nonce and POSTs malicious ZIP; PHP in extracted dir executes directly
**How found:** Traced username through sanitize_user/wp_strip_all_tags and noticed strip_tags requires '<' immediately followed by letter; hypothesized KSES tokenizer disagrees on whitespace; read kses.php allowlist, then audited user-profile.js enqueued on login page for auto-triggering handlers and found undefined===undefined guard and ajaxurl DOM clobbering.
**Exploitation:** Attacker lures admin to page that opens child window and navigates opener to authorize-application.php; child submits crafted login triggering JSONP SOME callback window.opener.approve.click, auto-approving an Application Password exfiltrated to attacker callback; attacker uses it to publish a page with script (unfiltered_html), which uploads a plugin ZIP and executes PHP shell.
**Escalation:** Preauth XSS -> JSONP SOME cross-window click -> Application Password theft -> authenticated REST publish with unfiltered_html -> plugin upload -> PHP RCE.
**Insight:** When two sanitizers run in sequence, diff their tokenizers on edge cases like whitespace after '<'; also grep scripts enqueued on a page for auto-triggered handlers (.trigger('click')) and guards comparing two absent DOM values that both yield undefined.
