<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: own2pwn</title>
    <description>The latest articles on DEV Community by own2pwn (@own2pwn).</description>
    <link>https://dev.to/own2pwn</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1662865%2Fbcc7f78d-baca-4f18-aba0-ce8eb46e5ed3.png</url>
      <title>DEV Community: own2pwn</title>
      <link>https://dev.to/own2pwn</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/own2pwn"/>
    <language>en</language>
    <item>
      <title>wp2shell: how a route confusion + a reachable SQLi become pre-auth RCE in WordPress core</title>
      <dc:creator>own2pwn</dc:creator>
      <pubDate>Sun, 19 Jul 2026 16:24:39 +0000</pubDate>
      <link>https://dev.to/own2pwn/wp2shell-how-a-route-confusion-a-reachable-sqli-become-pre-auth-rce-in-wordpress-core-3mkf</link>
      <guid>https://dev.to/own2pwn/wp2shell-how-a-route-confusion-a-reachable-sqli-become-pre-auth-rce-in-wordpress-core-3mkf</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;This is a republication. The original and always up to date version lives here:&lt;br&gt;
&lt;a href="https://own2pwn.fr/articles/appsec/wp2shell-wordpress-rce" rel="noopener noreferrer"&gt;https://own2pwn.fr/articles/appsec/wp2shell-wordpress-rce&lt;/a&gt; (it also embeds a live,&lt;br&gt;
non-intrusive exposure checker).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;One request. A &lt;code&gt;POST&lt;/code&gt; to &lt;code&gt;/wp-json/batch/v1&lt;/code&gt;, a slightly twisted JSON body, and the server&lt;br&gt;
answers a perfectly ordinary &lt;strong&gt;200 OK&lt;/strong&gt;. Thirty seconds later there is one more&lt;br&gt;
&lt;strong&gt;administrator&lt;/strong&gt; account in the database, an unknown plugin dropped under&lt;br&gt;
&lt;code&gt;wp-content/plugins/&lt;/code&gt;, and a shell that answers to &lt;code&gt;id&lt;/code&gt;. No password. No vulnerable&lt;br&gt;
third-party plugin. No exotic theme. Just WordPress, in its default install. This is&lt;br&gt;
&lt;strong&gt;wp2shell&lt;/strong&gt;, the bug that had security teams scrambling on 17 July 2026.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Legal note, read this first.&lt;/strong&gt; This article documents and defends. We dissect the real&lt;br&gt;
mechanism of wp2shell so you can detect it and fix it. We publish &lt;strong&gt;no ready-to-run&lt;br&gt;
payload&lt;/strong&gt; (the original researchers deliberately held theirs back, we do the same).&lt;br&gt;
Testing this flaw against a WordPress you neither own nor are mandated to audit is a&lt;br&gt;
crime. Everything below assumes an authorized pentest or a lab you own.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  What wp2shell actually is
&lt;/h2&gt;

&lt;p&gt;Despite the name, &lt;strong&gt;wp2shell is not a plugin&lt;/strong&gt;, nor yet another "admin to shell" tool that&lt;br&gt;
assumes you already have credentials. It is the name given by&lt;br&gt;
&lt;a href="https://slcyber.io/research-center/wp2shell-pre-authentication-rce-in-wordpress-core/" rel="noopener noreferrer"&gt;Searchlight Cyber&lt;/a&gt;&lt;br&gt;
(through its Assetnote unit) to a &lt;strong&gt;pre-authenticated RCE in WordPress core&lt;/strong&gt;, disclosed on&lt;br&gt;
17 July 2026. Pre-auth means exactly what it says: an anonymous attacker, with no account at&lt;br&gt;
all, gets code execution on the server.&lt;/p&gt;

&lt;p&gt;Technically, wp2shell is not &lt;em&gt;one&lt;/em&gt; flaw but a &lt;strong&gt;chain of two WordPress core bugs&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CVE-2026-63030&lt;/strong&gt;, a &lt;strong&gt;route confusion&lt;/strong&gt; in the REST API &lt;em&gt;batch&lt;/em&gt; endpoint
(&lt;code&gt;/wp-json/batch/v1&lt;/code&gt;). This is the link that turns a "theoretical" bug into an
account-free, exploitable RCE.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CVE-2026-60137&lt;/strong&gt;, a &lt;strong&gt;SQL injection&lt;/strong&gt; in &lt;code&gt;WP_Query&lt;/code&gt;'s handling of &lt;code&gt;author__not_in&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Taken separately, these two are annoying. &lt;strong&gt;Chained&lt;/strong&gt;, they give full RCE on a stock install&lt;br&gt;
with no third-party component. The route confusion was found by &lt;strong&gt;Adam Kues&lt;/strong&gt; (Assetnote /&lt;br&gt;
Searchlight Cyber) and reported through WordPress's HackerOne program; the SQL injection was&lt;br&gt;
reported in parallel by researchers &lt;strong&gt;TF1T&lt;/strong&gt;, &lt;strong&gt;dtro&lt;/strong&gt; and &lt;strong&gt;haongo&lt;/strong&gt;. WordPress responded by&lt;br&gt;
&lt;strong&gt;force-pushing automatic updates&lt;/strong&gt; to the affected installs.&lt;/p&gt;

&lt;p&gt;The exact version scope:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  BRANCH         STATE                                  FIXED IN
  ------------   ------------------------------------   ------------
  &amp;lt; 6.8.0        not vulnerable to the chain            --
  6.8.0 - 6.8.5  SQL injection only (no RCE)            6.8.6
  6.9.0 - 6.9.4  FULL pre-auth RCE                      6.9.5
  7.0.0 - 7.0.1  FULL pre-auth RCE                      7.0.2
  7.1 beta       FULL pre-auth RCE                      7.1 Beta 2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The 6.8 branch is hit only by the SQL injection; the full RCE starts at 6.9.0. Fixed in&lt;br&gt;
6.8.6 / 6.9.5 / 7.0.2.&lt;/p&gt;
&lt;h2&gt;
  
  
  The root bug: route confusion in /wp-json/batch/v1
&lt;/h2&gt;

&lt;p&gt;The REST API &lt;strong&gt;batch&lt;/strong&gt; endpoint exists to bundle several calls into a single HTTP request:&lt;br&gt;
you send an array of &lt;em&gt;sub-requests&lt;/em&gt;, WordPress processes them one after another and returns&lt;br&gt;
an array of responses. Handy for a front-end that wants to create ten objects at once.&lt;br&gt;
Internally, the controller keeps two &lt;em&gt;parallel&lt;/em&gt; arrays indexed together: one for the&lt;br&gt;
&lt;strong&gt;validation&lt;/strong&gt; of each sub-request, one for the &lt;strong&gt;handler&lt;/strong&gt; that will route it.&lt;/p&gt;

&lt;p&gt;The bug fits in one sentence, the one from the researcher who reproduced it: "a sub-request&lt;br&gt;
whose path fails &lt;code&gt;wp_parse_url()&lt;/code&gt; is added to &lt;code&gt;$validation&lt;/code&gt; but not to &lt;code&gt;$matches&lt;/code&gt;, so the&lt;br&gt;
arrays desynchronise." In other words: slipping in &lt;strong&gt;one&lt;/strong&gt; sub-request with a deliberately&lt;br&gt;
malformed path is enough to offset the two arrays by one. From there, every following&lt;br&gt;
sub-request is validated under one index but &lt;strong&gt;executed under its neighbour's handler&lt;/strong&gt;.&lt;br&gt;
Method and authentication checks apply to one route; the code runs for another.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Schematically (reconstruction).&lt;/strong&gt; The exact WordPress core code is not reproduced here.&lt;br&gt;
The idea, without the implementation detail: two &lt;code&gt;foreach&lt;/code&gt; loops over the same array of&lt;br&gt;
sub-requests, but one &lt;em&gt;skips&lt;/em&gt; invalid paths and the other does not. One array advances by&lt;br&gt;
N, the other by N-1.&lt;br&gt;
&lt;/p&gt;


&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST /wp-json/batch/v1
  (a batch of sub-requests, one with a deliberately malformed path, placed first)
        |
        v
  $validation array   the malformed sub-request FAILS wp_parse_url() but is still counted here
        |
        v
  $matches array      it does NOT enter here. Indexes slip: request i runs with handler i-1
        |
        v
  Result              an anonymous read lands in a code path that thought it handled something else
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The shape of the request is nothing fancy: standard batch JSON, with a broken path slipped&lt;br&gt;
in at the right spot. We show the &lt;em&gt;structure&lt;/em&gt; (the endpoint is public and documented), not a&lt;br&gt;
working exploit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;POST&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;/wp-json/batch/v&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;HTTP/&lt;/span&gt;&lt;span class="mf"&gt;1.1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;Host:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;target&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;Content-Type:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;application/json&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"requests"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"method"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GET"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"deliberately-invalid-path"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"method"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GET"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/wp/v2/users?author_exclude=..."&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Worth noting: the batch route is also reachable via &lt;code&gt;?rest_route=/batch/v1&lt;/code&gt;, which matters&lt;br&gt;
for detection and filtering (both forms must be watched).&lt;/p&gt;
&lt;h2&gt;
  
  
  From index slip to SQL injection (CVE-2026-60137)
&lt;/h2&gt;

&lt;p&gt;The slip is used to deliver a sub-request to the &lt;strong&gt;wrong handler&lt;/strong&gt;. Concretely, the exploit&lt;br&gt;
sends a &lt;code&gt;GET /wp/v2/users&lt;/code&gt; carrying an &lt;code&gt;author_exclude&lt;/code&gt; parameter. That parameter does &lt;em&gt;not&lt;/em&gt;&lt;br&gt;
exist in the &lt;em&gt;users&lt;/em&gt; endpoint schema, and would normally be ignored. But thanks to the route&lt;br&gt;
confusion, the request is dispatched to the &lt;strong&gt;posts&lt;/strong&gt; handler, where &lt;code&gt;author_exclude&lt;/code&gt; is&lt;br&gt;
recognised and mapped onto &lt;code&gt;WP_Query&lt;/code&gt;'s &lt;code&gt;author__not_in&lt;/code&gt; query variable.&lt;/p&gt;

&lt;p&gt;And that is where it breaks. &lt;code&gt;author__not_in&lt;/code&gt; is meant to be a &lt;strong&gt;list of numeric IDs&lt;/strong&gt;. The&lt;br&gt;
core expects integers; the exploit hands it a &lt;strong&gt;string&lt;/strong&gt;, which is not normalised and ends&lt;br&gt;
up &lt;strong&gt;interpolated straight into the SQL query&lt;/strong&gt;. SQL injection, on the WordPress server's&lt;br&gt;
database account, unauthenticated. The fix is as telling as the bug: it forces the input&lt;br&gt;
through &lt;code&gt;wp_parse_id_list()&lt;/code&gt;, which coerces it into a list of clean integers. One forgotten&lt;br&gt;
line of type-safety; that is often all that stands between an API and an injection.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET /wp/v2/users ?author_exclude=...   (unknown to the users schema)
        |
        v
  Handler /wp/v2/posts                 here author_exclude is valid, becomes author__not_in
        |
        v
  WP_Query author__not_in = string     expected a list of ints, got a string, straight into SQL
        |
        v
  Database                             pre-auth SQL injection, arbitrary reads incl. wp_users
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  From "SQL" to "shell": the "2shell" part
&lt;/h2&gt;

&lt;p&gt;A read-only SQL injection is already a full database leak. But wp2shell goes all the way:&lt;br&gt;
&lt;strong&gt;from SQL read to code execution&lt;/strong&gt;. The approach, as documented by the public PoC, abuses&lt;br&gt;
WordPress internals rather than a dangerous function:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Forge fake &lt;code&gt;wp_posts&lt;/code&gt; rows&lt;/strong&gt; via a UNION injection, rows crafted to be rendered through
the &lt;strong&gt;oEmbed cache&lt;/strong&gt; mechanism.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recover the real IDs&lt;/strong&gt; of the cache posts through the same SQLi, then &lt;strong&gt;recast&lt;/strong&gt; them as
customizer &lt;em&gt;changesets&lt;/em&gt; and &lt;em&gt;navigation menu items&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Create a "manufactured" administrator account&lt;/strong&gt; in a single poisoned batch request.&lt;/li&gt;
&lt;li&gt;Log in with that admin and &lt;strong&gt;drop a webshell plugin&lt;/strong&gt;, the step that yields command
execution.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A precondition that limits the RCE (but not the SQLi).&lt;/strong&gt; According to the analysis&lt;br&gt;
relayed by Cloudflare, the &lt;strong&gt;RCE&lt;/strong&gt; path applies when &lt;strong&gt;no persistent object cache&lt;/strong&gt; (Redis,&lt;br&gt;
Memcached and so on) is in place: the oEmbed cache trick depends on a database-backed&lt;br&gt;
cache. The &lt;strong&gt;underlying SQL injection stays exploitable in every case&lt;/strong&gt;. Translation: an&lt;br&gt;
external object cache mitigates the shell, it does not fix the data leak. Only the patch&lt;br&gt;
does.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The full code exists in the public PoCs; the point here is that you understand the chain&lt;br&gt;
well enough to &lt;em&gt;recognise it in your logs&lt;/em&gt;, not to replay it against the first WordPress you&lt;br&gt;
find.&lt;/p&gt;
&lt;h2&gt;
  
  
  Reproducing in a lab: the public PoC
&lt;/h2&gt;

&lt;p&gt;An independent PoC, &lt;a href="https://github.com/Icex0/wp2shell-poc" rel="noopener noreferrer"&gt;Icex0/wp2shell-poc&lt;/a&gt; (MIT&lt;br&gt;
license), automates the chain in a tool plainly named &lt;code&gt;wp2shell.py&lt;/code&gt;. On &lt;em&gt;your&lt;/em&gt; lab, its&lt;br&gt;
sub-commands tell the progression well: a non-destructive test, a database read, then&lt;br&gt;
execution:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# non-destructive detection: is the target vulnerable?&lt;/span&gt;
./wp2shell.py check http://lab-wordpress

&lt;span class="c"&gt;# confirm the SQL injection (still read-only)&lt;/span&gt;
./wp2shell.py check http://lab-wordpress &lt;span class="nt"&gt;--confirm-sqli&lt;/span&gt;

&lt;span class="c"&gt;# extract data from the database (union / error / blind)&lt;/span&gt;
./wp2shell.py &lt;span class="nb"&gt;read  &lt;/span&gt;http://lab-wordpress &lt;span class="nt"&gt;--preset&lt;/span&gt; &lt;span class="nb"&gt;users&lt;/span&gt;

&lt;span class="c"&gt;# command execution through the pre-auth bridge to admin&lt;/span&gt;
./wp2shell.py shell http://lab-wordpress &lt;span class="nt"&gt;--cmd&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt;
./wp2shell.py shell http://lab-wordpress &lt;span class="nt"&gt;-i&lt;/span&gt;        &lt;span class="c"&gt;# interactive shell&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Searchlight Cyber also put up a non-offensive checker, &lt;a href="https://wp2shell.com" rel="noopener noreferrer"&gt;wp2shell.com&lt;/a&gt;,&lt;br&gt;
which simply says whether an instance is exposed. On the defensive tooling side, the&lt;br&gt;
disclosure has already triggered detection &lt;strong&gt;Nuclei templates&lt;/strong&gt; and authenticated&lt;br&gt;
verification checks at scanner vendors (Rapid7 among others). As of writing, however, &lt;strong&gt;no&lt;br&gt;
Metasploit module or Exploit-DB entry&lt;/strong&gt; specific to CVE-2026-63030 is confirmed. Do not&lt;br&gt;
confuse it with the old generic &lt;code&gt;wp_admin_shell_upload&lt;/code&gt; module, which assumes you are&lt;br&gt;
&lt;em&gt;already&lt;/em&gt; admin.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Do not confuse wp2shell with WP-SHELLSTORM.&lt;/strong&gt; Around the same time (July 2026) another&lt;br&gt;
WordPress story is circulating: &lt;strong&gt;WP-SHELLSTORM&lt;/strong&gt;, an access-resale campaign that&lt;br&gt;
backdoored around 25,000 sites through &lt;strong&gt;27 old plugin flaws&lt;/strong&gt; (including the Breeze&lt;br&gt;
plugin, &lt;code&gt;CVE-2026-3844&lt;/code&gt;) by dropping &lt;code&gt;down.php&lt;/code&gt;-style webshells. That is a &lt;strong&gt;campaign&lt;br&gt;
exploiting known bugs&lt;/strong&gt;, with no relation to wp2shell: different flaws, core untouched.&lt;br&gt;
The two names just both end in "shell".&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  Detecting exposure in blackbox, without exploiting it
&lt;/h2&gt;

&lt;p&gt;Good news for anyone auditing a fleet: &lt;strong&gt;the whole chain is gated by the core version&lt;/strong&gt;. No&lt;br&gt;
need to fire a payload to know whether an instance is exposed; just read its version and&lt;br&gt;
compare it to the vulnerable ranges. That is exactly the logic of our open-source detector,&lt;br&gt;
&lt;a href="https://github.com/own2pwn-fr/wp2shell-detect" rel="noopener noreferrer"&gt;wp2shell-detect&lt;/a&gt; (Python, zero dependency,&lt;br&gt;
&lt;strong&gt;detection only&lt;/strong&gt;). Here it is step by step.&lt;/p&gt;
&lt;h3&gt;
  
  
  1. Fingerprint the core version
&lt;/h3&gt;

&lt;p&gt;WordPress leaks its version through several public, read-only channels. We query them and&lt;br&gt;
cross-check:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The home page &lt;strong&gt;generator&lt;/strong&gt; tag: &lt;code&gt;&amp;lt;meta name="generator" content="WordPress 6.9.3"&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;RSS feed&lt;/strong&gt; (&lt;code&gt;/feed/&lt;/code&gt;): &lt;code&gt;?v=6.9.3&lt;/code&gt; in the &lt;code&gt;&amp;lt;generator&amp;gt;&lt;/code&gt; tag.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/readme.html&lt;/code&gt;, which shows &lt;code&gt;Version 6.9.3&lt;/code&gt; in plain text.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;/wp-json/&lt;/code&gt; index, sometimes chatty about the version in its description.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Parsing must be &lt;strong&gt;strict&lt;/strong&gt;: accept only a purely numeric &lt;code&gt;X.Y&lt;/code&gt; or &lt;code&gt;X.Y.Z&lt;/code&gt;, otherwise return&lt;br&gt;
"unknown" rather than guess. A misread version is a guaranteed false positive (or worse, a&lt;br&gt;
false negative).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# vulnerable ranges (inclusive), from the disclosure
&lt;/span&gt;&lt;span class="n"&gt;RCE_RANGES&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[((&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))]&lt;/span&gt;
&lt;span class="n"&gt;SQLI_RANGES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[((&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;))]&lt;/span&gt;          &lt;span class="c1"&gt;# SQLi only, no RCE
# fixed in 6.8.6 / 6.9.5 / 7.0.2
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;parse_version&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;^(\d+)\.(\d+)(?:\.(\d+))?$&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;   &lt;span class="c1"&gt;# strict
&lt;/span&gt;    &lt;span class="nf"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;verdict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lo&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;hi&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;lo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hi&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;RCE_RANGES&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pre-auth RCE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lo&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;hi&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;lo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hi&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;SQLI_RANGES&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SQL injection&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;out of range (likely patched)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The hardened install case.&lt;/strong&gt; A well-configured WordPress &lt;strong&gt;removes the generator tag&lt;/strong&gt;&lt;br&gt;
and blocks &lt;code&gt;readme.html&lt;/code&gt;. The version then becomes invisible in pure blackbox. Our detector&lt;br&gt;
says so plainly ("WordPress confirmed, version hidden") instead of bluffing: confirming&lt;br&gt;
wp2shell without the version would amount to &lt;em&gt;exploiting&lt;/em&gt; it, which we refuse to do for a&lt;br&gt;
mere detection. The healthy reflex: patch anyway.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  2. Confirm the batch route is reachable
&lt;/h3&gt;

&lt;p&gt;The version says "vulnerable"; the remaining question is whether the chain's entry point is&lt;br&gt;
exposed or already filtered at the edge. We check that &lt;em&gt;without ever triggering it&lt;/em&gt;, with an&lt;br&gt;
&lt;code&gt;OPTIONS&lt;/code&gt; request: the REST router returns the route &lt;em&gt;schema&lt;/em&gt; (the allowed methods) &lt;strong&gt;without&lt;br&gt;
running the handler&lt;/strong&gt;. A &lt;code&gt;200&lt;/code&gt; with &lt;code&gt;Allow: POST&lt;/code&gt; confirms that &lt;code&gt;/wp-json/batch/v1&lt;/code&gt; responds;&lt;br&gt;
a &lt;code&gt;403&lt;/code&gt; signals a mitigation is already in place.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# OPTIONS = route introspection, no handler fired, zero payload&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; OPTIONS &lt;span class="nt"&gt;-i&lt;/span&gt; http://target/wp-json/batch/v1 | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s1"&gt;'^Allow:'&lt;/span&gt;
&lt;span class="c"&gt;# -&amp;gt; Allow: POST   (batch route exposed)   |   403/401 (filtered at the edge)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. The verdict
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  version readable?
    +- no   -&amp;gt; WordPress, version hidden: patch as a precaution
    +- yes  -&amp;gt; 6.9.0-6.9.4 or 7.0.0-7.0.1 ? -&amp;gt; PRE-AUTH RCE   (critical)
               6.8.0-6.8.5 ?                 -&amp;gt; SQL INJECTION  (high)
               otherwise                     -&amp;gt; likely patched
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Across a whole fleet, in one command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/own2pwn-fr/wp2shell-detect &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;wp2shell-detect
./wp2shell_detect.py &lt;span class="nt"&gt;--targets&lt;/span&gt; fleet.txt        &lt;span class="c"&gt;# one URL (or host) per line&lt;/span&gt;
&lt;span class="c"&gt;# [!!] https://blog.example.com -&amp;gt; VULNERABLE (pre-auth RCE)  6.9.3  CVE-2026-63030&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It exits with status &lt;code&gt;2&lt;/code&gt; as soon as one target is vulnerable: handy for a nightly sweep in CI&lt;br&gt;
or cron. Nothing in there touches the flaw; these are requests a plain crawler could send.&lt;/p&gt;
&lt;h2&gt;
  
  
  Detecting exploitation in your logs
&lt;/h2&gt;

&lt;p&gt;Exposure detection answers "am I vulnerable". Your logs answer "am I being attacked". No&lt;br&gt;
vendor has published a definitive IOC list: the flaw is a few days old. But the mechanism&lt;br&gt;
&lt;em&gt;dictates&lt;/em&gt; its own signatures. What to hunt for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Anonymous requests on the batch endpoint&lt;/strong&gt;: &lt;code&gt;POST /wp-json/batch/v1&lt;/code&gt; or
&lt;code&gt;?rest_route=/batch/v1&lt;/code&gt; with no admin session. Legitimate anonymous use of this route is
rare: treat it as suspicious by default.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Batch bodies with a &lt;code&gt;requests&lt;/code&gt; array&lt;/strong&gt; where one path is clearly malformed (the desync
trigger).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An &lt;code&gt;author_exclude&lt;/code&gt; on &lt;code&gt;/wp/v2/users&lt;/code&gt;&lt;/strong&gt; (illegitimate for that schema), especially if its
value contains SQL syntax (&lt;code&gt;UNION&lt;/code&gt;, quotes, comment markers).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Post-exploitation signs&lt;/strong&gt;: an administrator account out of nowhere, abnormal &lt;code&gt;wp_posts&lt;/code&gt;
rows tied to the oEmbed cache, &lt;code&gt;customize_changeset&lt;/code&gt; or &lt;code&gt;nav_menu_item&lt;/code&gt;, and above all a
&lt;strong&gt;new plugin&lt;/strong&gt; with an unknown PHP file under &lt;code&gt;wp-content/plugins/&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two simple passes over your access logs and filesystem clear most of the ground:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1) anonymous hits on the batch route (both forms)&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'/wp-json/batch/v1|rest_route=/batch/v1'&lt;/span&gt; access.log &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-iv&lt;/span&gt; &lt;span class="s1"&gt;'wp-admin'&lt;/span&gt; | less

&lt;span class="c"&gt;# 2) author_exclude passed to the users endpoint (has no business there)&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'/wp/v2/users'&lt;/span&gt; access.log | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s1"&gt;'author_exclude'&lt;/span&gt;

&lt;span class="c"&gt;# 3) recently dropped plugins (suspect webshell)&lt;/span&gt;
find wp-content/plugins &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s1"&gt;'*.php'&lt;/span&gt; &lt;span class="nt"&gt;-mtime&lt;/span&gt; &lt;span class="nt"&gt;-7&lt;/span&gt; &lt;span class="nt"&gt;-printf&lt;/span&gt; &lt;span class="s1"&gt;'%T+ %p\n'&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Mitigating: patch first, filter second
&lt;/h2&gt;

&lt;p&gt;The only real answer is the &lt;strong&gt;patch&lt;/strong&gt;. Move to &lt;strong&gt;WordPress 7.0.2&lt;/strong&gt; or &lt;strong&gt;6.9.5&lt;/strong&gt; (or &lt;strong&gt;6.8.6&lt;/strong&gt;&lt;br&gt;
for the 6.8 branch, which only carried the SQLi). WordPress force-pushes automatic updates to&lt;br&gt;
affected installs, but do not rely on it: verify. Rapid7 puts it bluntly, workarounds do not&lt;br&gt;
replace the patch.&lt;/p&gt;

&lt;p&gt;If you &lt;em&gt;really&lt;/em&gt; cannot patch right away, filter at the edge. Block the batch endpoint at the&lt;br&gt;
reverse proxy, in both its forms:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="c1"&gt;# nginx: cut anonymous access to the batch API while waiting for the patch&lt;/span&gt;
&lt;span class="k"&gt;location&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;/wp-json/batch/v1&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kn"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# and the query-string form&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="s"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$arg_rest_route&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/batch/v1")&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kn"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finer, without breaking a legitimate authenticated use: a &lt;strong&gt;mu-plugin&lt;/strong&gt; (must-use) that&lt;br&gt;
rejects &lt;em&gt;anonymous&lt;/em&gt; batch requests before they reach the controller. Drop it in&lt;br&gt;
&lt;code&gt;wp-content/mu-plugins/&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="c1"&gt;// wp-content/mu-plugins/000-block-anon-batch.php&lt;/span&gt;
&lt;span class="c1"&gt;// Temporary measure: reject /wp-json/batch/v1 for logged-out users.&lt;/span&gt;
&lt;span class="nf"&gt;add_filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'rest_pre_dispatch'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$server&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;strpos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get_route&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="s1"&gt;'/batch/v1'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;is_user_logged_in&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;WP_Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'rest_forbidden'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Batch API disabled'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'status'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, restrict &lt;strong&gt;anonymous access to the REST API&lt;/strong&gt; in general (through a security&lt;br&gt;
plugin), and post-incident, audit &lt;strong&gt;admin accounts&lt;/strong&gt; and &lt;strong&gt;unknown plugins&lt;/strong&gt;, rotate the&lt;br&gt;
&lt;em&gt;salts&lt;/em&gt; and credentials, and review the cache/oEmbed tables. This flaw is a useful reminder:&lt;br&gt;
&lt;a href="https://own2pwn.fr/articles/appsec/gestion-des-vulnerabilites" rel="noopener noreferrer"&gt;vulnerability management&lt;/a&gt; is&lt;br&gt;
not won at the moment of the CVE, but in your ability to know, within minutes, &lt;em&gt;where&lt;/em&gt; you&lt;br&gt;
run the vulnerable version.&lt;/p&gt;

&lt;h2&gt;
  
  
  What wp2shell says about your external attack surface
&lt;/h2&gt;

&lt;p&gt;The real trap with a WordPress core CVE is not the WordPress you administer and monitor. It&lt;br&gt;
is &lt;strong&gt;the other one&lt;/strong&gt;: the corporate blog spun up three years ago for a campaign, a&lt;br&gt;
subsidiary's &lt;code&gt;events.&lt;/code&gt; subdomain, the test install a contractor never shut down. Those&lt;br&gt;
instances are in no inventory, and they are exactly the ones still running 6.9.3 while your&lt;br&gt;
known assets are patched the same day. You cannot fix what you do not know you expose: that is&lt;br&gt;
the whole point of&lt;br&gt;
&lt;a href="https://own2pwn.fr/articles/appsec/decouverte-assets" rel="noopener noreferrer"&gt;asset discovery&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This is precisely the job of an&lt;br&gt;
&lt;a href="https://own2pwn.fr/articles/appsec/qu-est-ce-que-l-easm" rel="noopener noreferrer"&gt;External Attack Surface Management&lt;/a&gt;&lt;br&gt;
platform: continuously discover every exposed WordPress you have (including the ones nobody&lt;br&gt;
manages), &lt;strong&gt;fingerprint their version&lt;/strong&gt;, and surface first those carrying a critical CVE like&lt;br&gt;
wp2shell. A scan that probes &lt;code&gt;/wp-json/batch/v1&lt;/code&gt; and compares the detected version to the&lt;br&gt;
fixed threshold turns a Friday-night disclosure into a list of URLs to patch on Monday&lt;br&gt;
morning. That is what our&lt;br&gt;
&lt;a href="https://own2pwn.fr/produits/external-attack-surface-management-easm" rel="noopener noreferrer"&gt;EASM&lt;/a&gt; does. And where&lt;br&gt;
automated detection stops, a&lt;br&gt;
&lt;a href="https://own2pwn.fr/services/pentest-web-blackbox" rel="noopener noreferrer"&gt;blackbox web pentest&lt;/a&gt; confirms real&lt;br&gt;
exploitability, with no false positives. For the exact boundary between the two, we detailed&lt;br&gt;
it in&lt;br&gt;
&lt;a href="https://own2pwn.fr/articles/appsec/pentest-vs-scan-vulnerabilite" rel="noopener noreferrer"&gt;pentest vs vulnerability scan&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;wp2shell&lt;/strong&gt; (&lt;code&gt;CVE-2026-63030&lt;/code&gt;) is a &lt;strong&gt;pre-authenticated WordPress core RCE&lt;/strong&gt;, not a
plugin: no third-party dependency, a default install is enough.&lt;/li&gt;
&lt;li&gt;It &lt;strong&gt;chains two bugs&lt;/strong&gt;: a route confusion in &lt;code&gt;/wp-json/batch/v1&lt;/code&gt; (&lt;code&gt;$validation&lt;/code&gt; / &lt;code&gt;$matches&lt;/code&gt;
desync) and a SQL injection via &lt;code&gt;author__not_in&lt;/code&gt; (&lt;code&gt;CVE-2026-60137&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;RCE-risk versions: &lt;strong&gt;6.9.0-6.9.4&lt;/strong&gt; and &lt;strong&gt;7.0.0-7.0.1&lt;/strong&gt;. The 6.8.x branch only has the SQLi.
Fixes: &lt;strong&gt;6.8.6 / 6.9.5 / 7.0.2&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Detection: anonymous hits on the batch route, &lt;code&gt;author_exclude&lt;/code&gt; on &lt;code&gt;/wp/v2/users&lt;/code&gt;, new admin
accounts and unknown PHP plugins.&lt;/li&gt;
&lt;li&gt;Mitigation: &lt;strong&gt;patch&lt;/strong&gt; first; failing that, block &lt;code&gt;/wp-json/batch/v1&lt;/code&gt; (WAF or a
&lt;code&gt;rest_pre_dispatch&lt;/code&gt; mu-plugin). An external object cache hinders the RCE but leaves the SQL
leak.&lt;/li&gt;
&lt;li&gt;The real risk is the WordPress you &lt;strong&gt;do not know&lt;/strong&gt; you expose: asset discovery and version
fingerprinting make the difference between a Monday patch and a compromise.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Not sure how many WordPress instances carry your name on the internet? That is exactly the&lt;br&gt;
first thing&lt;br&gt;
&lt;a href="https://own2pwn.fr/produits/external-attack-surface-management-easm" rel="noopener noreferrer"&gt;own2pwn&lt;/a&gt; surfaces.&lt;br&gt;
Otherwise, talk through a specific exposure with a human on the&lt;br&gt;
&lt;a href="https://own2pwn.fr/contact" rel="noopener noreferrer"&gt;contact page&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>security</category>
      <category>wordpress</category>
      <category>php</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
