Critical CVE Response

wp2shell Turns a Stock WordPress Install Into Unauthenticated RCE (CVE-2026-63030, CVE-2026-60137)

Dark cyberpunk illustration of a data switchyard at night: cyan light-streams on rails, one misaligned junction diverting a stream onto the wrong track that glows orange and forces open a sealed gate.

Ask a small-business owner why their WordPress site got hacked and most will say a plugin did it. That answer has been right often enough to become a habit. The wp2shell chain breaks the habit. Two flaws in WordPress core itself, no plugin and no theme involved, let an unauthenticated attacker run code on a stock 6.9 or 7.0 install. The project shipped fixes on July 17, working exploits reached GitHub the same day, and CISA added both CVEs to its Known Exploited Vulnerabilities catalog on July 21.

WordPress runs a large share of the public web, and the small-business slice of that is enormous: the marketing site, the storefront, the booking page, the membership portal. Most of those sites were built once and left to auto-update in the background. This is one of the rare weeks where "left to auto-update" is the good outcome and where knowing your version actually matters.

The belief that keeps failing

The working assumption for years has been that WordPress core is solid and the risk lives in the plugin and theme ecosystem. There is real history behind it. The overwhelming majority of mass-exploited WordPress bugs have been third-party code: a forms plugin, a page builder, an abandoned theme. Core has a mature security team, a tight review process, and a strong track record. Betting on the plugins was a reasonable bet.

wp2shell is the counterexample that should update the bet. The chain lives entirely in files that ship with WordPress. A default install with zero plugins and the stock theme is exploitable. The correct mental model going forward is simpler: any code reachable before login is attack surface, and core ships a lot of it. The REST API is on by default and answers anonymous requests. That is the surface the attackers walked through. The plugin habit is not wrong so much as incomplete. Plugins remain the more common way in, but "core is fine" is an assumption worth checking rather than trusting, and this week it costs a minute to check.

Who this hits, and who can close the tab

The full unauthenticated-RCE chain needs both bugs, so the sites at real risk of takeover run WordPress 6.9.0 through 6.9.4, or 7.0.0 through 7.0.1. Those branches carry both the route-confusion flaw and the SQL injection. The 6.8.x branch (before 6.8.6) carries only the injection, which is serious on its own but does not complete the chain to code execution by itself. WordPress fixed all of it in 6.8.6, 6.9.5, and 7.0.2.

If you run WordPress and cannot state your version from memory, treat yourself as exposed until you check. That is most owners, and it is the honest hard part of this advisory. If you are on a managed host that force-pushed the update, or WordPress.org's forced automatic security update already landed, you are likely covered but should still confirm rather than assume. If you do not run WordPress anywhere, this one is not yours, and you can stop here. Telling you when to stop reading is the point.

How wp2shell actually works

The interesting part of this chain is that neither bug is enough alone. Read together they explain why a validated, sanitized API still handed an attacker a shell.

CVE-2026-63030 is a logic flaw in the REST API batch processor at /wp-json/batch/v1 (also reachable as ?rest_route=/batch/v1). The batch endpoint validates all sub-requests in one loop, then executes them in a second loop. When wp_parse_url() fails on a sub-request path, the error is recorded in the validation results but not in the parallel matches array. From that point the two arrays are out of step, and every following sub-request executes under the wrong route handler. Adam Kues of Searchlight Cyber, who disclosed the flaw, rated it 9.8 on CVSS, per Tenable's breakdown of the two CVEs. The desynchronization is what lets a request that should have been rejected reach a handler that trusts it.

CVE-2026-60137 is a SQL injection in the author__not_in parameter of the posts query. When that parameter arrives as a scalar string rather than the expected array, it is interpolated straight into raw SQL. On its own the guardrails around the posts endpoint blunt it, which is why its standalone CVSS is a modest 5.9. Fed through the batch route confusion, the input lands where the validation cannot stop it, and the result is a pre-authentication UNION-based injection. From there the published exploits read enough of the database to forge a session, create an administrator account, log in through the front door, and upload a malicious plugin for full server-side code execution. Rapid7's analysis walks the same path end to end, and NetSPI's write-up reaches the same conclusion.

The lesson for defenders is broader than one CMS. Two findings that a scanner rates medium and critical, neither exploitable alone, combined into a pre-auth takeover. Chained bugs are how modern web compromise happens, and severity scores on the individual links understate the danger of the link set.

Check whether you are exposed

Start by pinning your exact version and confirming the batch endpoint answers anonymous callers. Run this from any host that can reach the site:

# 1. Read the core version straight from the REST API (or the readme fallback)
curl -s "https://YOURSITE/wp-json/" | grep -o '"description":"[^"]*"' | head
curl -s "https://YOURSITE/readme.html" | grep -i -o "Version [0-9.]*" | head -1

# 2. Is the vulnerable batch route reachable without auth?
#    401/403 = access is restricted. 200/207/400 = it answers anonymous callers.
curl -s -o /dev/null -w "%{http_code}\n" \
  -X POST "https://YOURSITE/wp-json/batch/v1/" \
  -H "Content-Type: application/json" --data '{"requests":[]}'

# 3. Same route via the query-string form some WAF rules forget to cover
curl -s -o /dev/null -w "%{http_code}\n" \
  "https://YOURSITE/?rest_route=/batch/v1/"

If the version is below 6.8.6, 6.9.5, or 7.0.2 and the batch route answers, you have the exposed condition. Upgrading core is the fix. On most installs that is one click or a single wp core update, and it is the first thing to do this week.

Contain it before you finish patching

If you cannot update immediately because a client change-window or a fragile theme blocks it, put a barrier in front of the endpoint. Block both forms of the batch route at the web server or WAF, and prefer blocking anonymous REST access outright if nothing on the site needs it. Searchlight Cyber and the major WAF vendors published rules that cover both paths; Cloudflare pushed a managed rule to all plans. A local nginx block is a few lines:

# nginx: refuse the batch endpoint in both its path and query-string forms
location ~* ^/wp-json/batch/v1 { return 403; }
if ($args ~* "rest_route=/batch/v1") { return 403; }

# Apache (.htaccess) equivalent
# RewriteEngine On
# RewriteRule ^wp-json/batch/v1 - [F,L]
# RewriteCond %{QUERY_STRING} rest_route=/batch/v1 [NC]
# RewriteRule ^ - [F,L]

Treat the block as a splint, not a repair. It closes the one route the current exploits use. Update core as soon as your change window allows, then remove the rule if it interferes with legitimate batch callers.

Hunt for a site that is already owned

Exploitation has run in the wild since around July 20, days after disclosure, so patching is only half the job. A site on a vulnerable version last week may already carry an attacker's admin account. Look for the tracks the chain leaves:

# Anomalous POSTs to the batch endpoint in the access logs
grep -E "POST .*(wp-json/batch/v1|rest_route=/batch/v1)" \
  /var/log/nginx/access.log* | less

# Administrator accounts you did not create (WP-CLI)
wp user list --role=administrator --fields=ID,user_login,user_email,user_registered

# PHP files written since the disclosure window (possible dropped webshell)
find /var/www -name "*.php" -newermt "2026-07-15" -printf "%TY-%Tm-%Td %p\n" | sort

# Plugins present on disk, cross-check against what you intended to install
wp plugin list --fields=name,status,version

A batch-endpoint POST followed minutes later by a new administrator registration is the signature to act on. If you find one, treat the site as compromised: rotate every credential the site holds (database, admin, any API keys in wp-config.php), pull the site to a clean host or a known-good backup taken before July 17, remove unrecognized admin users and files, and only then bring it back online patched. A plugin upload means the attacker had code execution, so cleaning the visible account is not enough on its own.

Confirm your version, then block the batch route

The move for this week is small and specific. Confirm the WordPress version on every site you own or manage, update anything below 6.8.6, 6.9.5, or 7.0.2, and turn core auto-updates on so the next core bug patches itself while you sleep. If a site cannot update today, block /wp-json/batch/v1 in both its forms until it can, and read back the access logs for the batch-then-admin pattern before you assume you were fast enough. The version check takes a minute per site and settles the question that matters: whether you are patched, or whether you are hunting.

Small teams often run more WordPress than they remember: an old campaign microsite, a staging box that got a public DNS record, a client site nobody has logged into in a year. Those forgotten installs are exactly where an unpatched 6.9 is sitting right now. If you want a second set of eyes on your public web footprint, this is the kind of external attack-surface review Red Hound runs for small businesses and the MSPs that serve them.

Not sure what is exposed on your public web footprint?

We map the sites, subdomains, and forgotten installs an attacker can reach from the internet, then tell you plainly which ones need action this week. Book a session and we will review your external attack surface with you.