CVE-2026-63030 and CVE-2026-60137 (wp2shell): WordPress RCE Explained

Umut Bayram | 9 MIN READ

| July 20, 2026

Key Takeaways

  • wp2shell is a full remote code execution chain living entirely in WordPress core, not in plugins or themes.
  • An unauthenticated attacker can exploit a default WordPress installation with no plugins and no special configuration required.
  • A route confusion bug in the batch REST endpoint lets unvalidated requests reach handlers they were never checked for.
  • Object hydration, oEmbed caching, and nested saves let the attacker forge an administrator account, then achieve code execution.
  • The Picus Platform lets teams simulate wp2shell attacks and validate whether security controls detect and prevent them.

WordPress powers a large share of the web, so any flaw in its core code becomes a high-value target overnight.

wp2shell (CVE-2026-63030 and CVE-2026-60137) is exactly that: an exploit chain that lives entirely in WordPress core, requiring no plugins, no special configuration, and no authentication.

Starting from a request-handling bug in the batch REST endpoint, an anonymous attacker can escalate through SQL injection and object hydration to forge an administrator account and, ultimately, achieve remote code execution against a default installation.

In this blog, we break down how each link in the wp2shell chain works, and how to validate your security controls against it.

What is WordPress Core?

WordPress ships in three layers, and the boundary between them is central to this vulnerability.

WordPress Core is the base application maintained by the WordPress project: the PHP that handles routing, the query layer, the user and capability system, the REST API, and the admin dashboard.

On top of core sit plugins and themes, which are third-party code that extend behavior. A large share of WordPress vulnerabilities have historically resided in plugins.

wp2shell is more significant because it resides entirely in core, and it is exploitable by an unauthenticated user against a default installation that requires no plugins and no special configuration.

How Does wp2shell Work?

wp2shell turns a request-handling bug in WordPress core's batch REST endpoint into an SQL injection in WP_Query, and finally into an unauthenticated attacker creating an administrator account [1].

Here's how the chain works:

The Batch Endpoint and Route Confusion

WordPress exposes a REST API under /wp-json/. Anyone can hit public routes such as /wp-json/wp/v2/posts to read published content. Some routes are protected: /wp-json/wp/v2/users requires the create_users capability to register a new user, so an anonymous caller normally gets 401 Unauthorized.

The batch endpoint, /wp-json/batch/v1, lets a client send several sub-requests in one HTTP call. The server parses each member, matches it to a route handler, runs the permission checks, executes the callbacks

The batch handler, WP_REST_Server::serve_batch_request_v1(), effectively maintained three parallel arrays indexed by position:

  • $requests holds the parsed request objects, or a WP_Error if a member failed to parse.
  • $matches holds the route-and-handler tuple for each request.
  • $validation holds validation results.

The vulnerable logic reduces to two loops, simplified here:

// Simplified Vulnerable Code

foreach ( $requests as $single_request ) {

if ( is_wp_error( $single_request ) ) {

$validation[] = $single_request;

continue; // No corresponding $matches entry was added.

}


$matches[] = $this->match_request_to_handler( $single_request );

}


foreach ( $requests as $i => $single_request ) {

$match = $matches[ $i ];

// Request i may now execute with a LATER request's handler.

}

Consider the two loops in sequence. When a member is a WP_Error, the code appends to $validation and continues, but it never appends a placeholder to $matches. From that point on, $matches is one element shorter than $requests. The second loop then walks $requests by index $i and reads $matches[$i], so every valid request after the error reads the handler tuple that belongs to a later request.

The resulting index misalignment can be shown directly:

$requests (by index): 0 = ERROR 1 = request A 2 = request B

$matches (by index): 0 = handler A 1 = handler B (index 2 never created)


The dispatch loop reads $matches[i] for the request sitting at index i:

request A is at index 1 -> $matches[1] = handler B (WRONG: handler A was intended)

The consequence is that the entire handler tuple shifts, including its permission callback, execution callback, and schema. An unsanitized request can therefore reach a handler it was never validated for, such as the posts handler.

From Route Confusion to SQL Injection

Now that route confusion lets an unvalidated request reach the posts handler, we need a sink. It is in WP_Query.

WP_Query is the object that turns query variables into SQL and runs it against the wp_posts table. REST controllers translate their public parameters into WP_Query variables.

For example, the posts controller exposes author_exclude and maps it internally to the query variable author__not_in. The REST schema says author_exclude is an array of integers, and under normal routing the request is validated against that schema before it ever reaches WP_Query.

Because route confusion delivers the request to the posts handler without it being validated against the posts schema, author__not_in no longer arrives as a clean integer array. It can arrive as a raw, attacker-controlled scalar. At that point the only remaining defense is whatever normalization WP_Query performs on its own, and in vulnerable versions of WordPress that normalization is incomplete.

if ( ! empty( $query_vars['author__not_in'] ) ) {


// Sanitizing runs only if already an array. This is the flaw.

if ( is_array( $query_vars['author__not_in'] ) ) {

// A scalar string never reaches this, so it stays uncleaned.

$query_vars['author__not_in'] = array_map(

'absint',

$query_vars['author__not_in']

);

}


// Cast happens here: the raw string survives.

$ids = implode( ',', (array) $query_vars['author__not_in'] );


// SQL injection.

$where .= " AND {$wpdb->posts}.post_author NOT IN ($ids) ";

}

From SQL Injection to Remote Code Execution

At this point the attacker can influence a SELECT, but MySQL is not executing shell commands. So how does query manipulation become application control? The answer is object hydration.

When WP_Query runs a query, it does two things: it converts each returned database row into a WP_Post object, and it stores those objects in the request-local object cache. After that, calls to get_post( $id ) return the cached object instead of reading the database again.

The SQL injection from the previous step controls what the query returns. So the WP_Post objects that WP_Query builds, and then caches, hold attacker-chosen field values. For the rest of that PHP request, any code that calls get_post() for those IDs receives the forged object and treats it as a real post.

To move the attack forward, WordPress has to act on the forged data, which means getting the poisoned object written back to the database. That write matters for two reasons: it turns the attacker's chosen fields (status, type, parent) into a saved record that later core logic treats as genuine, and every save fires WordPress's post lifecycle hooks, which the rest of the chain relies on. So the attacker needs legitimate core code that reads one of these poisoned objects and writes it back. Finding that code is the next step.

The oEmbed Write Path

oEmbed is the WordPress feature that turns a plain URL (a YouTube video, a tweet, a link to another WordPress post) into rich embedded content instead of a bare link. To avoid re-fetching that content on every page load, WordPress stores the generated embed HTML in the database and in its cache. That caching path is what the attack abuses.

When oEmbed thinks a cached embed is stale, it re-saves the post with just an ID and new content and lets WordPress supply the remaining fields from get_post(). Since get_post() now returns the poisoned object, that step writes the attacker's chosen status, type, and parent into the database.

From a Poisoned Write to Code Execution

Writing a post does not create an administrator that can install plugins for code execution, which only a protected API can do. The attacker needs two post-save side effects, and their timing is the catch. One save makes WordPress temporarily act as an administrator, but that identity is switched on inside a publish routine and switched back the moment it ends. The save that re-enters the REST API must run inside that window. Two separate writes cannot do this, so the second save must be nested inside the first.

A stock safety routine supplies that nesting. The poisoned posts form forbidden parent loops (a post that ends up being its own ancestor), and WordPress repairs a loop by re-saving its posts, one repair running inside another. That nesting is what keeps the administrator identity active across the save that matters.

The first useful save publishes a Customizer changeset. A changeset remembers which user created each setting, and when it publishes, WordPress briefly switches its current user to that remembered user before saving. Normally that is safe, because the user was checked when the setting was created. Here the changeset is forged, so the remembered user is an administrator the attacker picked. For that moment, WordPress is running as an administrator.

The second useful save starts the REST loader. WordPress builds some internal event names by joining a post's status and type, and core happens to hook its REST loader to an event named parse_request. By setting a poisoned post's status and type so they join into parse_request, the attacker makes an ordinary save to start the REST loader, and it starts while the current user is still that administrator.

That is the final trap. The REST loader would start handling a fresh request even though the original one was still running, and the new nested request shared the same current user. So a request to create a user, which the attacker included in the original anonymous batch and which was first rejected with 401, is re-evaluated as the administrator and now succeeds with 201 Created. The attacker has created an administrator account without ever logging in.

Once the attacker has an administrator account, he can install plugins that allow him to execute code remotely.

How Picus Simulates wp2shell Attacks?

We also strongly suggest simulating wp2shell attacks to test the effectiveness of your security controls against real-life cyber attacks using the Picus Platform. You can also test your defenses against other vulnerability exploitation attacks, such as regreSSHion, Citrix Bleed, and Follina, within minutes with a 14-day free trial of the Picus Platform.

Picus Threat Library includes the following threats for the wp2shell attacks:

Threat ID

Threat Name

Attack Module

53669

Wordpress Web Attack Campaign - 3

Web Application

Picus also provides actionable mitigation content. Picus Mitigation Library includes prevention signatures to address wp2shell vulnerability exploitation attacks in preventive security controls. Currently, Picus Labs has validated the following signatures for wp2shell vulnerability:

Security Control

Signature ID

Signature Name

F5 BIG-IP

200022001

Generic Remote File/Path Include Attempt 1 (path param, http/https)

F5 BIG-IP

200002430

SQL-INJ expressions like ""or 1 --""

F5 BIG-IP

200002480

SQL-INJ expressions like ""sleep()"" (2) (Parameter)

F5 BIG-IP

200002478

SQL-INJ expressions like ""sleep()"" (1) (Parameter)

TippingPointTPS

13413

HTTP: SQL Injection (SLEEP)

Check Point NGX

asm_dynamic_prop_CVE_2026_9082

Drupal Core SQL Injection (CVE-2026-9082)

FortiGate IPS

60908

Drupal.Core.jsonapi.SQL.Injection

Start simulating emerging threats today and get actionable mitigation insights with a 14-day free trial of the Picus Platform.

References

[1] “GitHub - Icex0/wp2shell-poc: wp2shell (CVE-2026-63030 & CVE-2026-60137) - full RCE chain,” GitHub. Accessed: Jul. 20, 2026. [Online]. Available: https://github.com/Icex0/wp2shell-poc

 
wp2shell is an exploit chain that turns a request-handling bug in WordPress core's batch REST endpoint into an SQL injection in WP_Query, and finally into an unauthenticated attacker creating an administrator account. It affects a default WordPress installation, requiring no plugins and no special configuration to exploit.
Many WordPress vulnerabilities historically reside in third-party plugins. wp2shell is more significant because it resides entirely in core, the base application maintained by the WordPress project. It is exploitable by an unauthenticated user against a default installation that requires no plugins and no special configuration.
The batch handler maintains parallel arrays indexed by position. When a member fails to parse, the code appends to validation but never adds a placeholder to matches. From that point, matches is one element shorter than requests, so later valid requests read a handler tuple belonging to a different request.
Route confusion delivers requests to the posts handler without validation against the posts schema. The parameter author__not_in can then arrive as a raw, attacker-controlled scalar instead of a clean integer array. Because WP_Query only sanitizes values already formatted as arrays, the scalar string survives uncleaned and reaches the SQL query.
SQL injection lets the attacker poison cached WP_Post objects through object hydration. The oEmbed write path saves these forged objects back to the database. Through nested saves, a Customizer changeset, and the REST loader, WordPress re-evaluates a rejected user-creation request as an administrator, creating an admin account that can install plugins.
wp2shell affects default WordPress installations running vulnerable versions of WordPress core. Because the flaw lives in core rather than plugins or themes, sites are exposed even without additional third-party code. The attack requires no authentication, no plugins, and no special configuration to succeed.
The Picus Platform lets teams simulate wp2shell attacks to test the effectiveness of security controls against real-life cyber attacks. The Picus Threat Library includes threats for wp2shell, and the Picus Mitigation Library provides validated prevention signatures for security controls such as F5 BIG-IP, TippingPointTPS, Check Point, and FortiGate.

Table of Contents

Ready to start? Request a demo