Top-down forensic evidence board showing WordPress batch requests and route cards shifted out of alignment, with a red path leading through SQL to a patched server package.

wp2shell: How WordPress Route Confusion Became RCE

POST /wp-json/batch/v1 was not unusual by itself. The detail that changed the incident queue was what followed: a new administrator, a short-lived plugin, then a request to a PHP file that had not existed when the previous integrity scan ran.

This is a composite defensive scenario, not a claim about a specific victim. It captures the uncomfortable part of wp2shell: the first request can look like ordinary REST traffic, while the decisive evidence appears later in the database and filesystem. The chain joins CVE-2026-63030, a REST batch-route confusion flaw, with CVE-2026-60137, an SQL injection condition in WP_Query. On affected WordPress 6.9 and 7.0 releases, that combination can move an anonymous request to administrator control and remote code execution.

WordPress shipped fixes on July 17, 2026 and enabled forced automatic updates because of the severity. Four days later, CISA added both CVEs to the Known Exploited Vulnerabilities catalog, citing evidence of active exploitation. At that point, “we have auto-updates” stopped being an answer. The defensible question became: which version was each site running during the exposure window, and what changed before the patch actually landed?

Two bugs, one broken security contract

The first mistake is to describe wp2shell as merely “an SQL injection in WordPress.” That loses the architecture. The SQL sink was not supposed to be reachable with a hostile value through the normal REST posts schema. The batch dispatcher broke that assumption.

A batch endpoint accepts several logical requests inside one HTTP envelope. Internally, the server must keep multiple pieces of state aligned: the original request, the matched route, the handler, validation results, and permission decisions. The researcher’s technical analysis describes a malformed batch member causing those structures to drift out of positional alignment. A later member could then be checked under one route’s expectations but executed through another route’s handler. This is an interpretation conflict: different stages no longer agree about what object they are processing.

That distinction matters beyond WordPress. Schema validation at an API boundary is only useful if dispatch preserves the binding between the validated object and the executed handler. Any middleware that rewrites, batches, retries, unwraps, or recursively dispatches requests can break that binding. It is the same class of design failure examined in advanced threat modeling across trust boundaries: a control exists, but the identity of the controlled object changes after the decision.

The second bug sits in WP_Query. The author__not_in argument is intended to represent author IDs. In the expected path, callers provide an integer list and upstream schema handling constrains the value. Once route confusion lets an unexpected scalar reach the query builder, that type guarantee collapses. The patched branches normalize the value with the core ID-list parser before it can influence SQL construction.

Neither label captures the full impact alone. CVE-2026-60137 explains the query primitive. CVE-2026-63030 explains how an anonymous caller reaches it through the batch endpoint and turns it into a larger chain. The official WordPress 7.0.2 security release records both fixes and the affected backports:

BranchExposureFixed release
WordPress 7.0.xSQL injection and full route-confusion chain7.0.2
WordPress 6.9.xSQL injection and full route-confusion chain6.9.5
WordPress 6.8.xSQL injection only; not the complete RCE chain6.8.6
Earlier than 6.8Not affected by these two issues according to WordPressNot applicable for these CVEs

How the chain crosses from SQL to code execution

An SQL injection does not automatically equal shell access. The interesting work in wp2shell is the bridge. At a high level, the disclosed chain uses the query primitive to influence WordPress objects, abuses legitimate cache and save behavior to make selected state durable, reaches an administrator context, and then uses ordinary administrative capability to execute code. The exploit does not depend on a vulnerable plugin being installed beforehand.

Conceptual wp2shell attack flow from an anonymous request through route confusion and an unsanitized query to administrator control and code execution, with edge logging, patching, database audit, and file-integrity controls.
Conceptual flow only. The figure omits exploit syntax and emphasizes the control points defenders can still observe.

The sequence is valuable to defenders because each transition leaves a different kind of evidence:

  1. Anonymous batch traffic. Reverse-proxy, CDN, WAF, and web-server logs can identify requests to the batch endpoint, source addresses, timestamps, response codes, and request volume. Standard access logs usually do not retain POST bodies, so they rarely prove the injection.
  2. Database manipulation. Unexpected users, capability metadata, options, transients, or content objects can survive even when an attacker removes the visible administrator. Database history and backups are more useful than a current dashboard screenshot.
  3. Administrator actions. Plugin installation, option changes, and user creation are high-signal events if they occur outside a maintenance window. WordPress audit logging is not universal, so infrastructure telemetry must fill the gap.
  4. Filesystem execution. New or modified PHP under plugins, must-use plugins, uploads, the document root, or writable temporary paths can mark the point where application control became host-level execution.

The researcher’s full technical analysis documents the object-cache and WordPress state transitions in detail. Reproducing the public chain is unnecessary for triage and risky on production. A safer validation strategy is version proof, checksum verification, endpoint telemetry, database review, and filesystem comparison against a known-good baseline.

A safe validation sequence for owned WordPress estates

Start with inventory, not a scanner firing exploit-shaped requests at every hostname. WordPress frequently sits behind aliases, forgotten campaign domains, staging hosts, and origin addresses that do not appear in the primary CMDB. External discovery can help, but banner data is triage rather than proof; the same discipline applies when using Shodan for exposure validation.

On an authorized host with WP-CLI, capture the current state before changing it:

# Read-only inventory
wp core version
wp core verify-checksums
wp user list --role=administrator \
  --fields=ID,user_login,user_email,user_registered
wp plugin list --fields=name,status,version,update
wp option get active_plugins --format=json

# Preserve outputs with a UTC timestamp and host identity
date -u
hostname -f

wp core verify-checksums checks core files; it does not establish the integrity of plugins, themes, uploads, drop-ins, or server configuration. A clean core result is therefore compatible with a malicious plugin or a PHP implant elsewhere in the web root.

Search access logs for both common route forms, but treat a match as a lead, not a verdict:

# Adjust paths and decompression tooling to the host
zgrep -hE '(/wp-json/batch/v1|rest_route=/batch/v1)' \
  /var/log/nginx/access.log* /var/log/apache2/access.log* 2>/dev/null

# Bound the review to the exposure window and preserve the original logs
# before filtering or rotating anything.

Legitimate software may call the batch API. Prioritize bursts from unfamiliar networks, POST requests followed by authentication or plugin-management paths, unusual response-size sequences, and traffic close to the creation time of a new administrator or PHP file. A single WAF block is evidence that a request matched a rule, not proof that no alternate request reached the origin.

For filesystem triage, set the start of the exposure window from evidence rather than copying a date blindly:

DOCROOT=/var/www/html
WINDOW_START='2026-07-17 00:00:00 UTC'

find "$DOCROOT" -type f -name '*.php' -newermt "$WINDOW_START" \
  -printf '%TY-%Tm-%TdT%TH:%TM:%TS %u:%g %m %p\n' | sort

find "$DOCROOT/wp-content" -type f \
  \( -path '*/uploads/*' -o -path '*/mu-plugins/*' \) \
  -newermt "$WINDOW_START" -print

Timestamps are mutable and deployments create noise. Compare suspicious files with package sources, deployment manifests, backups, EDR history, and web requests. Pay special attention to wp-content/mu-plugins, writable upload directories, unfamiliar plugin slugs, and drop-ins such as object-cache.php or advanced-cache.php. Do not delete first and investigate later; hash and preserve the file, record ownership and timestamps, then acquire the surrounding evidence.

Patching closes the bug; it does not close the incident

Forced updates reduced the vulnerable population, but they create a dangerous cognitive shortcut: administrators see a fixed version today and infer that the site was never exposed. That conclusion does not follow. The relevant timestamps are the first public exploitation opportunity, the first hostile request observed in your telemetry, and the moment each instance completed its update.

If evidence suggests exploitation, preserve the database, web root, access logs, PHP-FPM logs, audit logs, and relevant cloud/CDN events before remediation. A current list of users is not enough because a short-lived administrator can be removed. Review historical database snapshots or binary logs where available, hosting control-plane events, plugin installation records, and outbound connections from the PHP worker. The shared-hosting web-shell forensics workflow is particularly relevant when one compromised account can reach multiple virtual hosts.

Containment must match the observed scope. At minimum:

  • Remove public exposure or place the site behind a controlled maintenance boundary while evidence is collected.
  • Rebuild from trusted packages and known-good content when code execution is confirmed; do not rely on deleting one visible file.
  • Rotate WordPress administrator credentials, application passwords, database credentials, hosting-panel access, deployment keys, and API secrets reachable by the web process.
  • Invalidate active sessions and review local users, cron jobs, SSH keys, adjacent virtual hosts, and outbound persistence.
  • Compare database and content changes against business-approved activity before restoring service.

Credential rotation before containment can alert an active intruder and leave stolen secrets usable elsewhere. Evidence acquisition without a containment plan can give the attacker more time. The sequence depends on business criticality and observed activity, but both functions must be coordinated.

WAF controls, cache conditions, and other false comfort

Cloudflare deployed managed rules for both CVEs on July 17, with blocking enabled for proxied customers. That is useful defense in depth. It is not a repair. Origin bypass, disabled rulesets, broad overrides, alternate hostnames, and traffic that never traverses the proxy can all invalidate the assumption that “Cloudflare is in front.” Teams should verify the actual request path and inspect security events rather than relying on a dashboard toggle. This is also why WAF testing must validate architecture, not just payload signatures.

If patching is briefly impossible, the researcher and Cloudflare identify blocking anonymous access to the batch endpoint as an emergency mitigation. Cover both /wp-json/batch/v1 and the rest_route form, test legitimate integrations, and set an expiration for the exception. Disabling the entire REST API can break editors, mobile clients, integrations, and custom applications. A temporary control with no owner tends to become permanent technical debt.

Cloudflare also noted that the demonstrated RCE path applies when a persistent object cache is not in use. Redis or Memcached may disrupt that specific cache-dependent path. They do not remove the underlying vulnerabilities, do not neutralize the SQL injection exposure, and should not be treated as a security boundary. Patch every affected branch.

What security engineering should take from wp2shell

For Red Teams, the lesson is not “run the public exploit.” It is to look for validation-to-execution drift in any system that multiplexes requests. Build authorized tests around malformed members, partial parse failures, nested batches, duplicate identifiers, retries, and mismatches between permission checks and final handlers. Use harmless canary parameters and assertions in a lab. The goal is to prove object-binding failure without creating an administrator or writing code to the target.

For AppSec teams, positional parallel arrays deserve suspicion when one element can be skipped. Validation should produce an immutable object containing the normalized input, matched route, handler identity, and authorization result. Execution should consume that object directly, not reconstruct or re-index relationships later. Failure must reject the member or the complete batch without shifting neighboring state.

For defenders, version compliance is the beginning of the query, not the end. The operational model is:

  1. Prove patch state for every reachable instance and every alias.
  2. Reconstruct the exposure window from update and edge telemetry.
  3. Hunt across layers: batch endpoint, database state, administrator actions, plugins, files, and process behavior.
  4. Preserve before cleaning when any artifact is suspicious.
  5. Reduce blast radius by running PHP and the database with least privilege, constraining filesystem writes, and monitoring high-risk administration events.

The small detail in this chain is an index that no longer refers to the object everyone thinks it does. The strategic failure is broader: trusting a security decision after the object behind that decision has changed. Patch wp2shell immediately. Then verify what happened while that assumption was still wrong.

💜 Enjoyed this content? Support the blog with USDT (TRC20):

TX7obcjHQbDUXb4mGqoASEu1QFTKT2CFGG

View support page

Paulo Rigonato

Security Engineer | Red Team | Pentest

Offensive security specialist with experience in assessments, pentesting, and Red Team operations. He works in enterprise cybersecurity and continues to share knowledge through this blog.

Certifications: OSCP | eWPTXv2 | ITILv4

💻 GitHub 🔗 LinkedIn

Similar Posts