# Click2Shell: Preauth WordPress Core Theme Preview Injection to  RCE Chain

Crafted theme-install URL injects CSS selector syntax into jQuery, auto-clicking Install for an attacker-chosen catalog theme whose inactive PHP then loads an unprotected AJAX plugin installer, yielding RCE.

- Payout: $300
- Severity: critical
- Program: WordPress Core
- Credited: PWNAI Research
- Disclosed: 2026-09-18
- Write-up quality: 9/10
- Original: https://pwn.ai/blog/click2shell
- Library page: https://bugbountylibrary.com/r/blog%3Apwn-ai-blog-click2shell
- Tags: domain:web impact:privesc impact:rce kind:research platform:research sev:critical tech:php tech:wordpress technique:chained technique:parser-differential vuln:access-control vuln:auth-bypass vuln:rce

**Target:** wp-admin/theme-install.php theme preview and Customizer AJAX
**Root cause:** The theme slug is canonicalized by the WordPress.org Themes API but the raw value is concatenated unescaped into a jQuery selector, letting injected quotes/combinators reach the Install control; the installed theme's PHP then loads pre-activation via Customizer and exposes a nonce/capability-less AJAX plugin installer.
**Steps:**
1. Visit `/wp-admin/theme-install.php?theme=twentytwenty%22%5D%3E%2A%3E%2A%3E%2A%2F%2A` as an authenticated Administrator
2. API canonicalizes value to `twentytwenty` and returns genuine catalog record
3. Browser builds selector `div[data-slug="twentytwenty"]>*>*>*/*"]` and `.trigger('click')` fires the real Install button
4. Theme ZIP downloads to `wp-content/themes/twentytwenty/`, remains inactive
5. Request `/wp-admin/admin-ajax.php?wp_customize=on&customize_theme=mobile-repair-zone` to load inactive theme PHP
6. POST `action=mobile_repair_zone_install_and_activate_plugin` with `plugin_details[plugin_url]` pointing at attacker ZIP
7. Theme fetches, unpacks and includes attacker plugin PHP -> `system("id")`
**How found:** After XSS2Shell, noticed a theme-preview URL value is interpreted twice: once by the WordPress.org Themes API (canonicalized to a slug) and again by admin JS that keeps raw punctuation in a jQuery selector. Hypothesized the parser differential could make the selector walk into the Install control, then audited catalog themes for code reachable before activation and found the nonce-less AJAX installer.
**Exploitation:** Attacker hosts a page that opens the crafted theme-install URL in a popup against an already-authenticated Administrator; WordPress's own JS spends the install nonce and capability to install the catalog theme, then a delayed form POST to admin-ajax with wp_customize=on loads the inactive theme and its unprotected installer fetches an attacker-hosted base64 plugin ZIP, executing PHP as the web server user.
**Escalation:** Chained a forced-install UI primitive with a separate pre-activation flaw in a catalog theme (and 40+ others) to convert a non-RCE install into full server-side PHP execution, then plugin code can read wp-config.php and take over the host.
**Insight:** Look for values consumed by two parsers with different canonicalization (server API vs client selector/URL parser); a slug that the API normalizes but the DOM keeps raw can turn a read-only preview into a privileged action using the victim's own nonce. Also audit inactive plugin/theme code paths (Customizer preview, admin-ajax) since installed-but-inactive code still executes.
