Last week I woke up to find my own security blog serving Russian gambling spam and hosting roughly 45 admin accounts I never created. This is the timeline of how I found it, misdiagnosed it, and finally fixed it โ and what it taught me about running WordPress in 2026.
๐ฐ Beginner’s Guide โ the short version
A critical bug nicknamed “wp2shell” let anyone on the internet take over a WordPress site without logging in, by chaining two flaws in WordPress core itself (not a plugin). It affected WordPress 6.9.0โ6.9.4 and 7.0.0โ7.0.1, and attackers used it to plant rogue admin accounts and spam content. Changing your password doesn’t stop it, because the attacker never needed a password in the first place. The fix is simple: update WordPress to version 7.0.2 (or 6.9.5), and make sure automatic updates are actually turned on. If you run WordPress in Docker or on managed hosting, double-check the update actually applied to your files โ don’t just assume it did.
The Discovery
The first sign of trouble was cosmetic: a handful of published posts I never wrote, all pointing to Russian-language gambling sites. My initial assumption was a compromised plugin or a leaked password, so I went straight to wp-admin’s Users screen.
That’s when it got worse. Instead of one rogue account, there were roughly 45 unfamiliar administrator users, most with random-looking usernames, all created within a tight window. No password reset request, no email notification I’d missed โ just accounts that appeared as if from nowhere.
At that point I stopped clicking around and started treating it as a live incident.
The Diagnosis: Database, Not Server
My first instinct was to assume the server itself had been popped โ a webshell dropped somewhere, or SSH access compromised. So I checked the obvious server-level indicators first:
authorized_keyson the host was empty โ no attacker SSH key had been added.- Filesystem scan of the WordPress install turned up no unfamiliar PHP files, no modified core files, and no obvious webshell.
- Backups were intact and running on schedule, untouched.
In other words, the server layer was clean. That ruled out a classic “dropped backdoor file” compromise and pointed somewhere else: the attacker never needed to write to disk at all. The rogue admin accounts and spam posts were pure database-level manipulation โ new rows inserted directly into the wp_users and wp_posts tables via WordPress’s own application logic, not through a filesystem backdoor.
That distinction matters. A server-level compromise means you rebuild the box. A database-level compromise via an application vulnerability means the vulnerability is still open even after you’ve cleaned up the visible mess โ which is exactly the trap I fell into next.
Once I stopped looking at symptoms and started looking for a root cause, the pattern matched a very recently disclosed WordPress core vulnerability: wp2shell, tracked as CVE-2026-63030 combined with CVE-2026-60137. This wasn’t a plugin bug โ it lived in WordPress core itself.
What wp2shell Actually Is
wp2shell is the name given to a chain of two vulnerabilities, publicly disclosed on July 17, 2026 by security researcher Adam Kues:
- CVE-2026-63030 is a flaw in WordPress’s REST API batch-route processor (
/wp-json/batch/v1). The batch endpoint validates a set of sub-requests in one loop and then dispatches them in a second, separate loop. When one sub-request contains a URL that WordPress’s internal URL parser can’t handle, the error gets recorded in the validation results but the two loops fall out of sync with each other. The practical effect is that a later request in the batch can get routed to the wrong internal handler โ one that expects different, less-sanitized input than what it actually receives. - CVE-2026-60137 is a SQL injection vulnerability in WordPress’s core query logic (
WP_Query).
Neither bug alone is catastrophic on its own in every configuration. Chained together, though, they become something much worse: the route-confusion bug lets an attacker smuggle SQL injection past the sanitization it would normally hit, using nothing but a single anonymous HTTP request against a stock WordPress install โ no plugins, no theme, no prior access required. From there, an attacker can manipulate the database directly: forge session/authentication state, create an administrator account, and ultimately install a malicious plugin to achieve full remote code execution on the server.
That’s the “shell” in wp2shell โ full RCE, reached entirely pre-authentication.
Who’s Affected
According to the official WordPress.org advisory:
- WordPress 6.9.0โ6.9.4 and 7.0.0โ7.0.1: vulnerable to the full chain (route confusion + SQLi = unauthenticated RCE).
- WordPress 6.8.0โ6.8.5: vulnerable to the SQL injection alone (CVE-2026-60137), since the batch-route confusion bug wasn’t present before 6.9.
- Versions before 6.8 are not affected.
No plugin or theme is required โ this is a vulnerability in WordPress core itself, which is why it hit so many sites so quickly regardless of what plugins they ran.
Real-World Impact
This wasn’t a theoretical bug. PatchStack observed active exploitation beginning the same evening the vulnerability was disclosed, and by July 20, 2026, both Wiz Research and VulnCheck’s Canary honeypot network had independently confirmed widespread in-the-wild attacks against production WordPress sites. My blog was one of many caught in that window โ the spam posts and rogue admin accounts are the textbook post-exploitation signature: create an admin account, then use it to publish content (in my case, gambling spam) or install further tooling.
Given the severity, WordPress.org took the unusual step of force-enabling automatic background updates on vulnerable sites, even for sites that had auto-updates disabled by default policy.
Why the “Standard” Cleanup Didn’t Work
My first response followed the standard WordPress incident-response playbook: change the admin password, delete the rogue accounts, and consider the incident closed.
Within a short time, a new rogue admin account reappeared.
That’s the part that took a moment to click: this exploit doesn’t need a login at all. It’s a pre-authentication vulnerability โ the attacker’s request never touches an existing account or password, so rotating credentials does nothing to close the hole. As long as the vulnerable version of WordPress core was still running, the site was exploitable again the moment the same request was replayed. Deleting the symptom (rogue accounts) while leaving the vulnerable code in place is like mopping the floor while the pipe is still leaking.
The Real Fix: Patching Core, and a Docker Wrinkle
The actual fix was updating WordPress core to 7.0.2, the version WordPress.org shipped to close both CVEs. (Sites on the 6.9.x branch should update to 6.9.5; sites still on 6.8.x need at minimum 6.8.6 to close the SQLi.)
Here’s where my setup added a complication: my WordPress install runs in Docker, with the WordPress files living on a host-mounted volume rather than inside the container image itself. Pulling a newer WordPress Docker image did not update the actual files on disk, because the mounted volume’s existing files took precedence over whatever shipped in the new image layer. I had to explicitly update the WordPress core files within the mounted volume โ the equivalent of running WordPress’s own core update process against the live files โ rather than assuming a fresh image pull would handle it.
This is worth flagging for anyone running WordPress in a containerized setup: verify the update actually landed on disk, don’t just trust that pulling a new image did the job. WordPress’s forced auto-update mechanism, referenced in the official advisory, may not behave the way you expect when the core files live outside the container’s own writable layers.
Hardening Afterward
Patching stopped the active vulnerability, but I didn’t want to be one incident away from this happening again. Afterward I put the following in place:
- Wordfence WAF, to catch and block exploitation attempts at the application layer, including the batch-route request pattern this vulnerability relies on.
- Two-factor authentication for all admin accounts, closing off credential-based attack paths even though this specific incident didn’t need one.
- Brute-force protection, rate-limiting login and REST API attempts.
- File-change email alerts, so any future filesystem-level tampering gets flagged immediately instead of discovered days later.
- Automated weekly WordPress core updates, verified to actually apply against the mounted volume, so a delayed patch window can’t be the reason this happens again.
Detection Notes for Others
If you’re checking whether your own site was targeted, a few concrete signals from the research on this chain are worth checking:
- HTTP
207 Multi-Statusresponses (or unexpected200s) from/wp-json/batch/v1or/?rest_route=/batch/v1in your access logs โ a strong indicator of exploitation attempts against the batch endpoint. - Request logs containing the strings
wp2shellorrezwp2shell, associated with automated exploitation tooling (though one vendor briefly flagged and later retracted a false positive on this indicator, so don’t treat a single match as conclusive on its own). - Unexpected new administrator accounts โ check
wp_usersdirectly, not just the admin UI, since it updates in near real time. - Unfamiliar plugins or recently modified plugin/theme files, which would indicate the attacker progressed past database manipulation into a webshell or plugin-drop stage.
If you can’t patch immediately, blocking or rate-limiting the /wp-json/batch/v1 route at your WAF or reverse proxy, or disabling anonymous REST API access via a security plugin, buys time โ but it’s a stopgap, not a fix.
Lessons Learned
A few things I’ll carry forward from this:
- Core vulnerabilities are just as real as plugin vulnerabilities. I’d spent years hardening plugins and themes and hadn’t seriously considered that WordPress core itself could ship a pre-auth RCE.
- Credential rotation doesn’t fix unauthenticated vulnerabilities. If an exploit doesn’t need a login, changing passwords is cosmetic. Confirm the underlying vulnerability is patched before declaring an incident closed.
- “The filesystem is clean” doesn’t mean you’re safe. A database-level compromise leaves none of the usual filesystem artifacts, and it’s easy to misdiagnose as “nothing happened here” if you only check for dropped files.
- Containerized deployments need extra verification on updates. A host-mounted volume can silently prevent an image update from actually patching your live files. Always verify the running version, not just the image tag.
- Automatic updates matter, and so does verifying they actually applied. WordPress force-enabling auto-updates for this CVE chain was the right call โ but “auto-update enabled” and “auto-update actually succeeded” are two different things, especially outside a standard hosting setup.
Sources
- CVE-2026-63030: wp2shell โ a Critical Remote Code Execution Vulnerability in WordPress Core โ Rapid7
- Exploitation in the Wild of wp2shell โ Wiz Blog
- WordPress 7.0.2 Release โ WordPress News (official advisory)
- CVE-2026-60137 โ SQL Injection Leading to Unauthenticated RCE โ IONIX
- CVE-2026-63030 and CVE-2026-60137: WordPress RCE Explained โ Picus Security
- WP2Shell WordPress Vulnerabilities Exploited in the Wild โ SecurityWeek
Leave a Reply