wp2shell: Pre-Auth RCE in WordPress Core — and the Patch Trap
wp2shell (CVE-2026-63030 + CVE-2026-60137) is an unauthenticated RCE in WordPress Core's REST batch API. Why 'we're patched' isn't the same as 'we're safe' — and how to actually validate exposure.
On July 17, 2026 the WordPress security team shipped a coordinated release — 6.8.6, 6.9.5, and 7.0.2 — to close wp2shell, an unauthenticated remote code execution that works against a stock install with no plugins, no configuration, and no login. A day later the CVE IDs were public (CVE-2026-63030 and CVE-2026-60137) and, per The Hacker News, a working proof-of-concept had landed on GitHub. WordPress powers on the order of 40% of the web, and the vulnerable code is in Core — not a third-party plugin — so this is not a vendor niche. It is most of the internet's publishing layer, reachable pre-auth, in one request.
The interesting part is not the RCE. It is the gap between "we ran the update" and "we are actually not exploitable," which for wp2shell is wider than most teams assume.
wp2shell: two bugs that only matter chained
Neither half of wp2shell is remarkable on its own. Chained, they turn an anonymous HTTP request into code execution.
The first is CVE-2026-60137, a SQL injection (CWE-89) in the author__not_in parameter of WP_Query. The parameter is supposed to receive an array of author IDs. Hand it a string instead of an array and the type check that expects an array is skipped, and the value flows into the query unsanitised. On its own this is a real but conditional bug — some plugin or theme has to pass untrusted input into that parameter. WPScan rated it a medium 5.9; CISA-ADP scored the same flaw 9.1 critical, because the two assessments are answering different questions (the isolated component versus what it enables).
The second is CVE-2026-63030, a route-confusion flaw (CWE-436, Interpretation Conflict) in the REST API batch endpoint. WordPress ships /wp-json/batch/v1 in Core, on by default, reachable without authentication. It exists so a client can bundle several REST calls into one request. Internally the controller tracks those sub-requests in parallel arrays — one for the requests, one for the validation results. Per the Searchlight Cyber / Assetnote research by Adam Kues, and the mechanism reconstructed by The Hacker News, an error in one sub-request knocks those arrays out of step by one. From that point on, each request is validated against the previous request's context — so a sub-request runs under a handler and an authorization decision that belong to a different call.
That desync is the whole game. It lets an unauthenticated caller reach a privileged handler and drive it straight into the author__not_in SQL injection, which the researchers then escalate to full RCE. WPScan's CNA record rates the chain 9.8 critical (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H); CISA-ADP scored the batch endpoint component alone at 7.5. The endpoint also answers on the always-available /?rest_route=/batch/v1 form, so it does not need pretty permalinks to be reachable — a detail worth holding onto, because it defeats the "we don't expose the REST API" assumption a lot of hardening guides still lean on.
Picture the batch controller as a coat check. Request A hands over a valid ticket; request B hands over nothing. One error shifts the rack by one hook. Now B's coat comes back on A's ticket. Nobody forged a ticket. The clerk simply handed the wrong coat to the wrong person — and B walks out wearing admin.
Why signature scanners miss the wp2shell class
CWE-436 is not a payload bug. There is no single malicious string a WAF can match, because the defect lives in the coupling of two features — batch routing and a parameter-type assumption — not in either one alone. This is the same structural family as HTTP request smuggling: a state desync between two components that each behave correctly in isolation.
That has direct consequences for how you find it, and how you fail to:
- Regex/signature WAF rules under-detect it. The exploit traffic can look like ordinary, well-formed batch requests. Cloudflare shipped managed rules at disclosure, but a rule that blocks the batch route is a blunt instrument, not detection of the flaw itself.
- Version-diffing scanners produce false confidence. A scanner that reads the version string and checks it against a range will call a site "patched" the moment the number changes — while telling you nothing about whether the batch route was ever reachable, or whether a shell was already dropped before the update.
- The SQL injection is conditional. The
author__not_insink needs untrusted input routed into it; whether that path is live depends on the request context the route confusion manufactures, not on a static code inspection.
You only establish exploitability by exercising the chain against the actual target. That is the difference between an estimate and evidence, and it is the theme wp2shell keeps returning to.
Why a WordPress version number lies
Here is the trap. WordPress's headline defense is automatic background updates for minor releases, and the coordinated fix went out on the minor-update tracks. So the reflex is: auto-update is on, the number rolled forward, we're done.
Two sites on the exact same vulnerable version can have completely different real exploitability, because the chain has preconditions the version string never captures. Rapid7's analysis notes the RCE step requires the absence of a persistent object cache — a site fronted by Redis or Memcached is far harder to push from SQLi to code execution than one without. Whether the batch route is reachable, whether a WAF covers the /?rest_route= form, whether auto-updates were actually disabled by a managed host's staged rollout or pinned by an agency workflow — all of it moves the needle, and none of it is in the banner.
| Two sites, same "vulnerable" version | Site A | Site B |
|---|---|---|
| Batch route reachable pre-auth | Yes | Yes |
/?rest_route=/batch/v1 fallback exposed |
Yes | Blocked at proxy |
| Persistent object cache (Redis/Memcached) | None | Redis |
| WAF covers the batch route specifically | No | Yes |
| Auto-update actually applied | Pinned by agency workflow | Applied |
| Real exposure | Exploitable to RCE | Not practically exploitable |
Now multiply that by scale. WordPress at the enterprise and agency level is rarely one site. It is hundreds or thousands — client sites on shared hosts, marketing microsites, campaign landing pages, and the forgotten install a contractor stood up three years ago and never decommissioned. A version-banner CMDB cannot answer "is every one of these actually closed," because the honest answer depends on per-site context the inventory does not hold. That is an exposure-validation problem, and it is exactly where a patch-and-pray posture breaks down.
Remediation
The fix is simple to state and, at fleet scale, hard to prove complete. Work it as a full report, not a version bump.
1. Am I affected?
Check the running version on every install — not the one you think is deployed:
# WP-CLI, per site
wp core version
# or from the filesystem
grep "wp_version =" wp-includes/version.php
Vulnerable ranges:
- 6.8.0 – 6.8.5 — the SQL injection (CVE-2026-60137) component only.
- 6.9.0 – 6.9.4 and 7.0.0 – 7.0.1 — the full pre-auth RCE chain (CVE-2026-63030 + CVE-2026-60137).
Confirm whether the batch route answers unauthenticated — test both forms, because the permalink-independent one is the one people forget:
curl -s -o /dev/null -w "%{http_code}\n" -X POST \
"https://TARGET/?rest_route=/batch/v1" \
-H "Content-Type: application/json" --data '{"requests":[]}'
A 200/207 here means the endpoint is live and reachable. Also record whether a persistent object cache is configured (wp cache type / check wp-config.php for a Redis/Memcached drop-in) — its absence is the RCE precondition.
2. Patch — exact fixed versions
Update to the fixed release for your track (verbatim from the WordPress security release):
- 7.0.x → 7.0.2
- 6.9.x → 6.9.5
- 6.8.x → 6.8.6
- 7.1 beta → 7.1 Beta 2
Do not leave sites on 7.0.x/6.9.x expecting a persistent object cache to save you — it raises the bar on the RCE step, it does not close the SQL injection.
3. Can't patch this hour? Compensating controls
- Block POST to the batch route at the edge — deny both
/wp-json/batch/v1and/?rest_route=/batch/v1. A single-form block leaves the fallback open, which is the most common mistake here. - Front the site with a persistent object cache (Redis/Memcached). This makes the SQLi-to-RCE step substantially harder; treat it as a speed bump, not a fix.
- Restrict the REST API to authenticated contexts where the site's functionality allows it.
4. Hunt for compromise
No vendor IOC list has been published, so hunt on behaviour, mapped to MITRE ATT&CK:
- T1190 — Exploit Public-Facing Application. POST requests to
/wp-json/batch/v1or/?rest_route=/batch/v1, especially bursts with malformed sub-request arrays or an error immediately followed by a 200/207. - T1505.003 — Web Shell. New or modified
.phpfiles underwp-content/uploads/,wp-content/plugins/,wp-content/themes/, or the webroot. Runwp core verify-checksumsand diff plugin/theme files against known-good copies. - T1059 — Command and Scripting Interpreter. The web server process spawning unexpected children (
sh,curl,wget,php -r). - T1136.001 / T1078 — Create / Valid Accounts. New administrator users, or unexpected writes to the
wp_optionstable (particularlyautoloadentries) andwp_users. - T1041 / T1071.001 — Exfiltration / C2 over web. Outbound sessions from a web host that historically only serves — traffic to never-seen ASNs or unexplained POST volume.
5. Eradicate and verify
Patching does not evict a shell that was planted before you patched. After the update:
- Remove any web shells and unauthorised files; restore Core, plugins, and themes from known-good sources.
- Rotate every secret reachable from the database and config:
wp-config.phpauth keys and salts (wp config shuffle-salts), the database credentials, and any API keys stored inwp_options. - Reset all administrator passwords and invalidate existing sessions.
- Review
wp-cron, must-use plugins (wp-content/mu-plugins/), and scheduled tasks for persistence. - Re-verify the site is clean after patching — because, again, the update closes the door, it does not clear the room.
Validate exposure, don't trust the banner
Everything above converges on one operational question: across your whole WordPress estate, which sites are actually exploitable right now — not which version strings changed. That is not a question a scanner answers by reading a number, and it is precisely what Zero Hunt is built to answer by evidence.
Zero Hunt's 10-agent AI swarm generates the exploit chain per target with a local LLM — not a pulled ExploitDB payload, which for a state-desync class like CWE-436 does not exist as a stable signature. The Web and Exploit agents reconstruct the batch route-confusion into the author__not_in sink against your install, inside an ephemeral gVisor-hardened Docker container, and return a verdict that is the real thing: reachable and exploitable, yes or no — accounting for the object-cache precondition and the WAF coverage that a banner check ignores. Every finding is backtested in AI Gym against the corpus before it runs, and ECDSA-signed at write time, so the answer for each site is an artefact an auditor can verify, not an assertion. Because campaigns are change-triggered, a new WordPress asset appearing on the perimeter — the microsite marketing stood up on Friday — triggers a full validation within the hour, which is the only honest defense for an estate that grows faster than its inventory.
When exploitation does start, the AI Traffic Analysis model is the second net: a deep-learning engine with four inference heads, trained on billions of PCAP sequences, running locally on the appliance GPU at 2.7+ Gbit/s. It sees the anomalous batch-endpoint request pattern and the web shell's outbound C2 on the wire while it happens — not in the next morning's SIEM digest, by which point the shell is already three hops deep. For a vulnerability where the version number lies and the payload has no signature, the wire and the validated exploit are the two places the truth still lives.
If you want to see how continuous, evidence-based validation maps to your WordPress footprint, get in touch.