Skip to content

Add WordPress pre-auth RCE exploit (CVE-2026-63030 + CVE-2026-60137) - #21760

Open
M4xSec wants to merge 1 commit into
rapid7:masterfrom
M4xSec:add-wp-batch-rce-cve-2026-63030
Open

Add WordPress pre-auth RCE exploit (CVE-2026-63030 + CVE-2026-60137)#21760
M4xSec wants to merge 1 commit into
rapid7:masterfrom
M4xSec:add-wp-batch-rce-cve-2026-63030

Conversation

@M4xSec

@M4xSec M4xSec commented Aug 8, 2026

Copy link
Copy Markdown

Summary

This adds a new exploit module for two chained WordPress Core vulnerabilities:

  • CVE-2026-63030 — REST API Batch Route Confusion (auth bypass)
  • CVE-2026-60137 — Blind SQL Injection via author__not_in parameter

Chaining these achieves unauthenticated Remote Code Execution on WordPress 6.8.0–6.8.5, 6.9.0–6.9.4, and 7.0.0–7.0.1. Fixed in 6.8.6, 6.9.5, and 7.0.2.

Attack Chain

  1. Route confusion via nested batch requests with invalid primer path bypasses authentication
  2. Time-based blind SQLi extracts table prefix, admin ID, and credentials
  3. UNION-based row forgery seeds oEmbed caches (read-only SQLi → DB writes)
  4. Changeset elevation + re-entrant parse_request() forges an administrator account
  5. Authenticated plugin upload deploys a self-cleaning webshell
  6. Command execution via the deployed shell, then automatic cleanup

Module Features

  • check() method with full SQLi confirmation (timing differential + DB version extraction)
  • Two shell modes: stealth (POST body + custom header auth) and classic (REST API route)
  • Two targets: PHP in-memory and Unix command
  • Cloudflare WAF bypass via colon primer + JSON unicode escaping of SQL keywords
  • Automatic webshell cleanup after payload execution
  • Fallback to admin hash extraction when changeset elevation fails on certain WP versions

Files

  • modules/exploits/multi/http/wp_wordpress_batch_rce.rb — exploit module
  • documentation/modules/exploit/multi/http/wp_wordpress_batch_rce.md — module documentation with setup instructions and sample scenarios

Verification

Tested against WordPress 6.9.0 on Docker (MariaDB 11.4) with and without ModSecurity WAF:

[+] 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...
[+] Table prefix: wp_
[+] Admin ID: 1
[*] [3/6] Seeding oEmbed caches (SQLi -> DB writes)...
[+] 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...
[+] Login verified — w2s_a1b2c3 is admin
[+] Webshell deployed and activated
[*] [6/6] Executing payload...
[+] RCE CONFIRMED — whoami: www-data
[+] Payload output: uid=33(www-data) gid=33(www-data) groups=33(www-data)
[+] Webshell removed

Test Environment

Docker lab available at: https://github.com/M4xSec/wp2shell-lab

git clone https://github.com/M4xSec/wp2shell-lab.git
cd wp2shell-lab/lab
docker compose up -d --build
# WordPress 6.9.0 on http://localhost:8888

Exploit module for WordPress REST API Batch Route Confusion chained
with Blind SQLi to achieve unauthenticated RCE on WP 6.8.0-6.8.5,
6.9.0-6.9.4, 7.0.0-7.0.1. Includes Cloudflare WAF bypass and
self-cleaning webshell with stealth mode.
@h00die

h00die commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

is this different than #21686 ? Doesn't look to be, so just wanted to be sure.

@M4xSec

M4xSec commented Aug 8, 2026

Copy link
Copy Markdown
Author

Hey @h00die, thanks for the heads up on #21686. I saw Crypto-Cat's module got merged nice work on their end.

My module covers the same CVE chain but has several differences that I think add value on top of what's already merged:

WAF Bypass: built-in Cloudflare/ModSecurity bypass via JSON unicode escaping of SQL keywords, colon primer path, /wp-json/ permalink routing, and Origin/Referer header spoofing. This matters because a large number of WordPress sites sit behind Cloudflare and the existing module won't work against them without this.

Stealth Shell: two shell modes: a stealth webshell that authenticates via a custom HTTP header and receives commands in POST body (never logged in access logs), and a classic REST API route shell. The merged module only has one approach.

Separate Scanner Module: I also submitted #21761 as a standalone non-destructive auxiliary scanner that does version fingerprinting + optional blind SQLi confirmation without any exploitation. Useful for bulk assessment without touching anything.

Additional check() hardening: my check() extracts the full DB version string as proof-of-vulnerability, not just a timing confirmation.

@h00die

h00die commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

@Nayeraneru Note the test env here has a vuln docker for wordpress. haven't tried it yet, but that may get you through some of the setup issues. Many plugins are just wget+unzip after that (if they dont have post-install setup).

@Nayeraneru

Copy link
Copy Markdown
Contributor

@Nayeraneru Note the test env here has a vuln docker for wordpress. haven't tried it yet, but that may get you through some of the setup issues. Many plugins are just wget+unzip after that (if they dont have post-install setup).

Thanks for the pointer @h00die ! I was trying to check the repo in the PR description out to see the entries in the compose file and watch the boot sequence live docker compose up --build
BUT this happens

nayera@Nero:~/git$ git clone https://github.com/M4xSec/wp2shell-lab.git
Cloning into 'wp2shell-lab'...
Username for 'https://github.com': Nayeraneru
Password for 'https://Nayeraneru@github.com':
remote: Repository not found.
fatal: repository 'https://github.com/M4xSec/wp2shell-lab.git/' not found

@h00die

h00die commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

@Nayeraneru its public now

@Nayeraneru

Copy link
Copy Markdown
Contributor

tried it @h00die
dug into wp2shell-lab a bit, turns out it's 2 containers (WordPress + MariaDB on an internal Docker network), fully self-provisions itself via a WP-CLI retry loop, no install wizard needed but pretty different from my current single-container WP setup though as my schema only handles one image: per definition, no concept of a second coordinated container. doesn't feel like a YAML tweak, more like actual schema surgery if we want multi-container support. figured I'd flag it as an open question with real evidence behind it before going off and building anything solo

also not really a swap-in for my existing WP module either way, since this PR's chain (unauth REST API + blind SQLi) is a totally different vuln class from wp_admin_shell_upload (which needs admin creds already). would be a new reference module either way, not a replacement

one thing I will borrow now regardless: their setup.sh retries the install until the DB's actually ready, vs. my Provisioner which fires once which is cheap, low-risk improvement to make either way

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new Metasploit exploit module and accompanying documentation for a chained WordPress Core pre-auth attack path (REST batch route confusion + blind SQLi) that culminates in plugin-based code execution, targeting specific vulnerable WordPress version ranges.

Changes:

  • Introduces exploit/multi/http/wp_wordpress_batch_rce implementing version fingerprinting, SQLi timing oracle/exfiltration, DB row forgery, admin creation, and plugin-upload shell execution/cleanup.
  • Adds end-user module documentation with setup steps, options, and example scenarios (including a WAF-bypass mode).

Impact Analysis: isolated change; no meaningful downstream impact identified from diff.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 6 comments.

File Description
modules/exploits/multi/http/wp_wordpress_batch_rce.rb New exploit module implementing the described WordPress pre-auth RCE chain.
documentation/modules/exploit/multi/http/wp_wordpress_batch_rce.md New documentation describing vulnerable versions, options, and usage scenarios.
Suppressed comments (2)

modules/exploits/multi/http/wp_wordpress_batch_rce.rb:728

  • Suggestion: Problem: JSON parsing uses JSON.parse(res.body) rescue {}, which silently hides malformed/empty JSON and can lead to false negatives/marker mismatches. Impact: makes failures harder to debug and inconsistent with response helpers. Fix: use res.get_json_document and validate a Hash before accessing keys.
      if res && res.code == 200
        result = JSON.parse(res.body) rescue {}
        if result['marker'] == marker
          print_good("RCE CONFIRMED — whoami: #{result['output'].strip}")
        end

modules/exploits/multi/http/wp_wordpress_batch_rce.rb:751

  • Suggestion: Problem: second JSON parse site also uses JSON.parse(res.body) rescue {} and swallows parse errors. Impact: can mask server-side failures and make payload output handling unreliable. Fix: use res.get_json_document and validate a Hash.
      if res && res.code == 200
        result = JSON.parse(res.body) rescue {}
        if result['marker'] == marker && !result['output'].to_s.empty?
          print_good("Payload output: #{result['output'].strip}")
        end

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +1 to +4
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
Comment on lines +304 to +308
return CheckCode::Vulnerable("SQLi confirmed but version extraction empty")
end
print_good("DB version: #{db_ver}")

CheckCode::Vulnerable("WordPress #{ver} | DB: #{db_ver} | SQLi confirmed")
Comment on lines +768 to +780
def target_host
datastore['VHOST'].to_s.empty? ? datastore['RHOST'] : datastore['VHOST']
end

def full_uri(path = '')
proto = datastore['SSL'] ? 'https' : 'http'
port_str = ''
if (datastore['SSL'] && datastore['RPORT'] != 443) ||
(!datastore['SSL'] && datastore['RPORT'] != 80)
port_str = ":#{datastore['RPORT']}"
end
"#{proto}://#{target_host}#{port_str}#{normalize_uri(base_uri, path)}"
end
unless res && res.code == 200
fail_with(Failure::UnexpectedReply, "Could not fetch posts list")
end
posts = JSON.parse(res.body) rescue []
Comment on lines +74 to +76
`/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 +311 to +315
def wp_hash_check(password, stored_hash)
if stored_hash =~ /\A\$wp\$2[aby]\$/
bcrypt_hash = stored_hash.sub(/\A\$wp\$/, '$').sub(/\A\$2y\$/, '$2a$')
begin
gem 'bcrypt'
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Todo

Development

Successfully merging this pull request may close these issues.

4 participants