A screen door on a submarine — the vendor locked the hatch but left a porthole that leads straight to the engine room
CVE-2026-60137 is a SQL injection in WordPress core's WP_Query class. The author__not_in parameter — meant to accept an array of integer user IDs — skips its absint() sanitisation guard when it receives a scalar string instead of an array. The REST API's author_exclude query parameter maps directly to author__not_in, so a crafted value passes raw SQL straight into the NOT IN (...) clause of the generated query. Affected versions are 6.8.0–6.8.5, 6.9.0–6.9.4, and 7.0.0–7.0.1; patches landed in 6.8.6, 6.9.5, and 7.0.2 on July 17 2026. On WordPress 6.8.x the flaw is reachable only when a plugin or theme forwards untrusted input to the parameter. On 6.9.x and 7.0.x a companion bug, CVE-2026-63030 (REST API batch-route confusion), removes that prerequisite entirely — the pair, dubbed wp2shell, yields unauthenticated remote code execution against default installations with a single POST to /wp-json/batch/v1.
The vendor rated this CVE a MEDIUM 5.9 (AC:H) because, in isolation, the SQLi needs a conduit. That framing is technically honest but operationally misleading to the point of negligence. CVE-2026-63030 ships in the same codebase, the same versions, disclosed the same day — every WordPress 6.9+ or 7.0.x instance carries both bugs by default. Attackers never exploit this CVE in isolation; the wp2shell chain is the observed attack. Within 48 hours of disclosure, 45 million exploit attempts from ~150,000 unique sources were logged, GreyNoise tracked a Chinese-speaking threat actor who compromised 49 organisations across 29 countries, and a western government lost 18,000+ sensitive records including law-enforcement PII. CISA added it to KEV on July 21 with a remediation deadline of August 4. Treating this as a 5.9 would be malpractice — the real-world severity is chain-aware CRITICAL.
6 steps from start to impact.
Target identification
/wp-login.php response headers, /wp-includes/js/ asset paths, or the REST API root at /wp-json/. Version detection is trivial via the <meta name="generator"> tag or the ?rest_route=/ response. Tools like WPScan, Nuclei templates, and custom Shodan/FOFA dorks automate this at scale.- Target runs WordPress 6.9.x or 7.0.x (for full chain) or 6.8.x with a plugin exposing author__not_in
- WordPress auto-updates were force-pushed; sites with auto-update enabled patched within hours
- Cloudflare deployed WAF rules across all plans blocking the batch-endpoint payload pattern
wordpress-version-detect, Shodan query http.component:wordpressREST API batch-route confusion (CVE-2026-63030)
/wp-json/batch/v1 containing a nested batch envelope. WordPress's route dispatcher resolves the outer batch request without authentication, then processes inner sub-requests in an elevated context. This bypasses the authentication check that would normally gate the author_exclude parameter on the posts endpoint. No credentials, no cookies, no nonce required.- WordPress ≥ 6.9.0 (batch endpoint introduced in 6.9)
- REST API reachable (default — enabled out of the box)
- Sites behind a WAF that blocks nested batch payloads or SQL metacharacters in query parameters
- Sites that disabled the REST API entirely via plugin (rare, <5%)
/wp-json/batch/v1 or /?rest_route=/batch/v1 with JSON body containing nested requests arrays; Cloudflare managed rule 100700Blind SQL injection via author__not_in
author_exclude value containing a UNION SELECT payload. Because WP_Query skips the absint() cast when the value arrives as a scalar string rather than an array, the raw SQL lands in the NOT IN (...) clause. The attacker uses UNION-based extraction with hex-encoded markers (e.g. 0x4f4b = 'OK') between || delimiters to exfiltrate data. The observed payload appends orderby=none&per_page=500 to maximise result visibility. Weaponised tools: wp2shell PoC by gichirimaku (564 stars), AbdullahMaqbool22 blind-SQLi harness, multiple Nuclei templates.- Step 2 succeeded (unauthenticated context reached the posts endpoint)
- WordPress database is MySQL/MariaDB (universal for WP)
- Persistent object cache (Redis/Memcached) may interfere with exploitation path per Tenable's FAQ
- Parameterised query plugins or DB firewalls (extremely rare in WP ecosystem)
wp_posts; IDS signatures for SQL metacharacters in REST API parameters; F5 Labs published payload signatures matching 735-byte exploitCredential extraction and hash cracking
wp_users to extract user_login, user_pass (phpass bcrypt hashes), and user_email for administrator accounts. WordPress's default phpass hashing uses bcrypt with cost 10 — offline cracking with hashcat mode 400 on a modern GPU rig yields common passwords in minutes. The GreyNoise-tracked actor also extracted wp-config.php database credentials via the same SQLi channel.- At least one admin account exists (always true)
- Admin password is crackable or DB credentials are reusable
- Strong admin passwords resist offline cracking
- MFA on wp-admin (via plugin) blocks credential reuse even if hash is cracked
Admin access and webshell deployment
/wp-admin/ and uploads a malicious plugin containing a webshell. GreyNoise documented three webshell variants: a minimal one-liner, a medium PHP backdoor, and a sophisticated 150KB 'CMSmap' plugin with file management, database access, port scanning, and privilege escalation. The actor created backdoor accounts ('kapibala', 'kapibala2') with backdated timestamps to evade detection. Alternatively, the RCE can be achieved directly through the SQLi by writing to a file via SELECT ... INTO OUTFILE if MySQL file privileges permit.- Admin credentials obtained or direct file-write via SQLi
- Hardened file permissions on wp-content/plugins may block plugin upload
- Read-only filesystem on containerised deployments
- Security plugins (Wordfence, Sucuri) may alert on plugin installation
Post-exploitation and lateral movement
- Webshell deployed and operational
- Backend infrastructure reachable from web server
- Network segmentation isolating web tier from internal resources
- EDR on the web server host
- Container sandboxing limiting filesystem and network access
/wp-json/batch/v1 and /?rest_route=/batch/v1. This breaks the CVE-2026-63030 chain partner, eliminating the unauthenticated path to the SQLi. Deploy within the noisgate CRITICAL mitigation SLA of 3 days. Cloudflare's managed rule 100700 already does this — enable it if you're on Cloudflare. For self-hosted WAFs, match on URI path containing batch/v1 with a POST method.wp core version across your fleet via WP-CLI or your configuration management tool. For hosts where auto-update failed (custom deployments, version-pinned Composer installs, Docker images with baked-in WP), apply manually. Target the noisgate CRITICAL remediation SLA of 90 days, but given active exploitation, prioritise within days not weeks.rest_authentication_errors filter returning WP_Error for non-logged-in users. This blocks the entire unauthenticated attack surface including both chain CVEs. Deploy within the 3-day mitigation window. Note: this breaks public-facing features that depend on the REST API (Gutenberg editor for logged-out previews, headless front-ends, some plugins).- WordPress security plugins alone (Wordfence free tier, Sucuri free) — the free tiers of these plugins did not have WAF rules for wp2shell at disclosure; premium/paid tiers received rules faster but still required the plugin to be installed and active before the attack. They cannot protect against the SQLi at the database layer.
- Changing the REST API URL prefix (
/wp-json/→ custom) — security through obscurity; the?rest_route=fallback path is always active and bypasses any custom prefix. Attackers use both paths. - PHP version upgrade — this is a WordPress application-layer flaw, not a PHP runtime vulnerability. Running PHP 8.4 vs 8.1 makes no difference.
- Database user privilege restriction — while removing FILE privilege from the WordPress MySQL user blocks
INTO OUTFILE, it does not prevent UNION-based data extraction of user credentials and site data, which is the primary attack path.
The supporting signals.
| In-the-wild status | Actively exploited at scale. 45M exploit attempts from ~150K unique IPs in the first week (CyberSecurityNews). GreyNoise tracked a Chinese-speaking actor (UTC+8) who compromised 49 orgs across 29 countries. A government org lost 18,000+ records including law-enforcement PII. Wiz reported 60% of WP orgs had a vulnerable instance at disclosure; 25% were internet-exposed. |
|---|---|
| CISA KEV | Listed 2026-07-21, remediation due 2026-08-04 per BOD 26-04. Added alongside CVE-2026-63030 (the chain partner). |
| EPSS | 0.05323 (5.3%) — top ~5th percentile. High for a 'MEDIUM' CVE, reflecting the active exploitation signal and massive attack surface. |
| PoC availability | 20+ public repositories on GitHub. Top repos: gichirimaku/wp2shell-PoC (564★, full RCE chain), AbdullahMaqbool22/CVE-2026-60137-WordPress-Core-SQL-Injection-PoC (blind SQLi harness), Bhanunamikaze/WP2Shell-CVE-2026-63030-POC (safe detector). Multiple Nuclei templates available. Many PoCs reportedly AI-generated within hours of disclosure. |
| CVSS vector | Vendor: CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N = 5.9. The AC:H reflects the standalone requirement for a plugin conduit. Chained with CVE-2026-63030: effective AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H = 9.8 (pre-auth RCE on default installs). |
| Affected versions | WordPress 6.8.0–6.8.5 (SQLi only, requires plugin/theme conduit), 6.9.0–6.9.4 (full wp2shell RCE chain), 7.0.0–7.0.1 (full wp2shell RCE chain), 7.1 beta (pre-beta2). |
| Fixed versions | 6.8.6, 6.9.5, 7.0.2, 7.1 beta2. WordPress.org force-pushed auto-updates for supported installations. Distro backports: Debian wordpress package updated; Ubuntu USN issued. |
| Scanning & exposure | Shodan indexes ~80M+ WordPress instances globally. Wiz: 25% of WP orgs had internet-exposed vulnerable instances at disclosure (dropping to ~10% within 24h due to auto-updates). Cloudflare deployed managed WAF rule 100700 across all plans. F5 Labs published 735-byte payload signature. |
| Disclosure timeline | 2026-07-17: Patches released + CVEs published. 2026-07-19: First public PoCs on GitHub (AI-assisted, <48h). 2026-07-20: Horizon3 detection tooling released; mass exploitation begins. 2026-07-21: CISA KEV listing. Researchers noted 'no security researcher could have found and completed this exploit chain in 10 hours without AI.' |
| Discoverers | TF1T, dtro, haongo (CVE-2026-60137). Adam Kues / Searchlight Cyber (CVE-2026-63030, the chain partner). Kues published research on disclosure day while withholding full technical details; AI-assisted PoC development filled the gap within hours. |
Sources.
- GitHub Advisory (GHSA-fpp7-x2x2-2mjf)
- Horizon3 — CVE-2026-60137 & CVE-2026-63030 Technical Analysis
- Tenable — wp2shell FAQ
- F5 Labs — wp2shell Captured Exploit Payload
- GreyNoise — Kapibala Actor Campaign
- Wiz — wp2shell Exploitation in the Wild
- CISA KEV Entry
- CyberSecurityNews — 45M Exploit Attempts Analysis
Why this verdict
- Chain collapses AC:H to AC:L. The vendor's MEDIUM rating assumes the SQLi needs a plugin to deliver untrusted input. On WordPress 6.9+ and 7.0.x, CVE-2026-63030 — same product, same versions, same disclosure day — provides that delivery mechanism on *default installations* with zero authentication. Every unpatched 6.9+/7.0.x instance carries both bugs. Scoring this CVE in isolation is like rating a detonator as 'low explosive yield' while ignoring the bomb it ships with.
- Active mass exploitation destroys any theoretical friction. 45 million exploit attempts in week one, 150K unique source IPs, 49 confirmed compromises across 29 countries, a government data breach of 18,000+ records. This is not a lab curiosity — the exploit is commoditised, AI-generated PoCs proliferated within 48 hours, and the chain is a single HTTP POST.
- WordPress is internet-facing by definition with a colossal installed base. WordPress powers roughly 40% of the web. The affected component is core, not a plugin. There is no 'internal-only deployment' friction to apply — if WordPress is running, it is reachable. Wiz documented 25% of WP organisations exposing a vulnerable server to the internet at disclosure.
- Role multiplier: WordPress in high-value roles. (a) *Low-value*: personal blogs — chain succeeds, blast radius limited to that site's data. (b) *Typical*: small business marketing sites — chain succeeds, credential theft + customer data exposure. (c) *High-value*: government portals (confirmed compromised), e-commerce platforms (PCI cardholder data in wp_postmeta/WooCommerce tables), healthcare patient portals (PHI), corporate intranets on WordPress — chain succeeds with blast radius extending to regulated data exfiltration, lateral movement into backend databases, and supply-chain pivot via compromised update mechanisms. The GreyNoise-tracked actor demonstrated credential spraying from compromised WP hosts into internal SQL databases. WordPress in government/e-commerce roles represents well over 10% of the installed base → verdict floor is CRITICAL.
- KEV listing with 14-day remediation deadline confirms federal-level urgency. CISA added both chain CVEs on July 21 with an August 4 due date — the fastest remediation window CISA assigns, signalling assessed criticality far above the vendor's MEDIUM.
Why not higher?
A 9.4 rather than 10.0 because WordPress.org force-pushed auto-updates, which materially reduced the vulnerable population within 24 hours (Wiz: from 25% to ~10% internet-exposed). Sites with functioning auto-update received the patch without operator action. The persistent-object-cache requirement noted by Tenable introduces a minor exploitation constraint on some configurations. These factors prevent a perfect 10 but do not meaningfully lower the severity for the remaining unpatched population.
Why not lower?
Downgrading below CRITICAL would require ignoring confirmed mass exploitation, KEV listing, the chain context that eliminates the AC:H prerequisite, and the blast radius against high-value deployment roles (government, e-commerce, healthcare). The 6.8.x branch alone — where a plugin is genuinely required — might warrant HIGH, but assessing the CVE across all affected versions (including 6.9+/7.0+ where the chain is default-exploitable), CRITICAL is the floor. The GreyNoise campaign with 49 compromised organisations is dispositive.
Crowdsourced verification payload.
Run this script on each WordPress host (or from a management server with SSH/WP-CLI access) as any user who can read the WordPress installation directory. Example: bash check_cve_2026_60137.sh /var/www/html where the argument is the WordPress root. No root privileges required — only read access to wp-includes/version.php.
#!/usr/bin/env bash
# check_cve_2026_60137.sh — Detect CVE-2026-60137 (WordPress author__not_in SQLi)
# Usage: bash check_cve_2026_60137.sh /path/to/wordpress
# Exit codes: 0 = PATCHED, 1 = VULNERABLE, 2 = UNKNOWN
set -euo pipefail
WP_ROOT="${1:-/var/www/html}"
VERSION_FILE="${WP_ROOT}/wp-includes/version.php"
if [[ ! -f "$VERSION_FILE" ]]; then
echo "UNKNOWN — Cannot find ${VERSION_FILE}. Is this a WordPress installation?"
exit 2
fi
# Extract $wp_version from version.php
WP_VERSION=$(grep -oP "\\\$wp_version\s*=\s*'\K[^']+" "$VERSION_FILE" 2>/dev/null || true)
if [[ -z "$WP_VERSION" ]]; then
echo "UNKNOWN — Could not parse WordPress version from ${VERSION_FILE}"
exit 2
fi
echo "Detected WordPress version: ${WP_VERSION}"
# Compare versions using sort -V
version_gte() {
# Returns 0 if $1 >= $2
printf '%s\n%s' "$2" "$1" | sort -V -C
}
version_lt() {
! version_gte "$1" "$2"
}
# Vulnerable ranges:
# 6.8.0 <= v < 6.8.6
# 6.9.0 <= v < 6.9.5
# 7.0.0 <= v < 7.0.2
VULNERABLE=false
CHAIN_RCE=false
if version_gte "$WP_VERSION" "6.8.0" && version_lt "$WP_VERSION" "6.8.6"; then
VULNERABLE=true
echo "STATUS: Version ${WP_VERSION} is in vulnerable range 6.8.0–6.8.5 (SQLi only, requires plugin conduit)"
elif version_gte "$WP_VERSION" "6.9.0" && version_lt "$WP_VERSION" "6.9.5"; then
VULNERABLE=true
CHAIN_RCE=true
echo "STATUS: Version ${WP_VERSION} is in vulnerable range 6.9.0–6.9.4 (wp2shell RCE chain exploitable)"
elif version_gte "$WP_VERSION" "7.0.0" && version_lt "$WP_VERSION" "7.0.2"; then
VULNERABLE=true
CHAIN_RCE=true
echo "STATUS: Version ${WP_VERSION} is in vulnerable range 7.0.0–7.0.1 (wp2shell RCE chain exploitable)"
fi
if [[ "$VULNERABLE" == "true" ]]; then
if [[ "$CHAIN_RCE" == "true" ]]; then
echo "VULNERABLE — CVE-2026-60137 + CVE-2026-63030 (wp2shell pre-auth RCE). PATCH IMMEDIATELY."
else
echo "VULNERABLE — CVE-2026-60137 (SQLi, requires plugin/theme conduit). Patch to 6.8.6."
fi
exit 1
else
echo "PATCHED — WordPress ${WP_VERSION} is not in a vulnerable range for CVE-2026-60137."
exit 0
fi