A few days ago someone created an administrator account on this blog through a hole in WordPress core itself. The reason it worked on my supposedly auto-updated site is a bit embarrassing, so I am writing it down.
If you found a wpenginebot admin on your own site and landed here, the cleanup steps are further down.
It started with a Wordfence email that arrived while I was asleep, saying that an admin user with the username wpenginebot was created outside of WordPress The account had been created at 02:20 with the email address wpenginebot@wpengine.com, and it certainly was not me.
What got in
The account was created with wp2shell, an exploit chain for WordPress core that needs no login and no plugin. It was mass-exploited within a day of being published. The first bug, CVE-2026-63030, is in the REST API batch endpoint (/wp-json/batch/v1). A specially crafted batch of requests confuses the routing, and one of the sub-requests runs without the authentication check it should have gone through. The second, CVE-2026-60137, is a SQL injection behind that check. Together they let anyone create an admin account, and the next step would be uploading a malicious plugin to run code on the server. The batch endpoint has been in core since 5.6 in 2020, the bug came with 6.9.
| Disclosed | 17 Jul 2026, by Searchlight Cyber |
| Vulnerable versions | WordPress 6.9.0–6.9.4 and 7.0.0–7.0.1 |
| Fixed in | 7.0.2 / 6.9.5 / 6.8.6 |
| My version at the time | 7.0.0 – in range |
My logs have the first probe on July 18 at 18:20 (an Azure IP, User-Agent wp2shell) and then hundreds of POSTs to the batch endpoint:
18/Jul 18:20 20.120.169.215 POST /wp-json/batch/v1 207 first probe, UA: wp2shell
20/Jul 02:17 91.202.233.61 POST /?rest_route=/batch/v1 207 x669 from this IP
20/Jul 02:20 91.202.233.61 POST /?rest_route=/batch/v1 207 admin "wpenginebot" created
22/Jul 01:27 45.79.167.23 POST /wp-json/batch/v1 207 still scanning, days later
Status 207 means the batch endpoint accepted the request, so on a vulnerable core the exploit worked.
Why the fix never installed
WordPress shipped the fix in 7.0.2 on July 17, the same day the bug went public, and I was still on 7.0.0. The patch existed before the attack even started, it just never got installed.
For weeks my site had been quietly failing its automatic updates with an error that the update cannot be installed because some files could not be copied. My core files are owned by root as a hardening measure and the WordPress updater runs as the web-server user, which cannot overwrite them, so every automatic update bounced off. I had seen the update emails and kept putting them off for later. The patch sat there available for five days.
What saved me
In the end nothing much came of it. I found no defaced pages, no spam and no web shells, and the database was untouched. Part of that was luck, and part was an old habit of mine.
The attacker had a valid admin account and came back about sixteen hours later to use it. The whole session is nine seconds in my logs:
attacker 87.120.93.46 -- the whole session
18:55:13 POST /xmlrpc.php 403 blocked
18:55:13 GET /wp-admin/ 444 connection dropped
18:55:14 GET /wp-admin/ 444 connection dropped
18:55:15 GET /my-account/ 200 WooCommerce login page (public)
18:55:19 POST /my-account/ 302 authenticated... as a customer
18:55:20 GET /wp-admin/ 444 connection dropped
18:55:22 GET /wp-admin/ 444 dropped -- gave up
444 is nginx dropping the connection without a response. Years ago I restricted /wp-admin and /wp-login.php to my home IP address at the nginx level. So the attacker had working admin credentials but could not reach the admin pages, logged in through the WooCommerce account page as a plain customer, tried /wp-admin a few more times and gave up. Uploading a malicious plugin from wp-admin is the whole point of this exploit chain, and that is the step the IP restriction blocked.
Cleaning up
The cleanup took an evening. I deleted the rogue admin, rotated all the secret keys and cleared every session (that logged me out as well).
I do not use the REST batch endpoint for anything, so I blocked /wp-json/batch/v1 at the nginx level, and a fail2ban jail now bans any IP touching it on the first hit (there were over 100 probes on the cleanup day alone).
The nginx block has to cover both the /wp-json/ path and the ?rest_route= query form, my logs show the scanners using both.
# Block the WP REST batch endpoint - the whole attack surface here.
location = /wp-json/batch/v1 { return 403; }
if ($arg_rest_route ~* "^/batch/v1") { return 403; }
Here are the three commands I would run on any WordPress box, patched or not.
# Any administrator you don't recognise is a red flag
wp user list --role=administrator --fields=ID,user_login,user_email,user_registered
# Rotate all salts and invalidate every session/cookie
wp config shuffle-salts
# The exploit landing in your access log - both URL forms, HTTP 207 is the tell
grep -E "batch/v1" /var/log/nginx/access.log
I had automatic updates enabled and thought I was covered, while they had been failing for weeks and I had learned to ignore the emails telling me so. That part is on me.
I used an AI agent in my terminal for the forensics and the cleanup. The same kind of tools help the attackers of course, I wrote about that collapse of technical barriers before.

I just had a client get attacked with this exploit even though their site is on 7.0.2
Thanks for flagging that – worth pinning down. From everything I have read, 7.0.2 does fully close the chain, so my best guess is the site was hit just before the patch landed: the update stops new attacks but does not undo a break-in that already happened, and whatever they dropped (a rogue admin, a shell, a cron job) survives it. Might be worth confirming the site is genuinely clean, and that 7.0.2 actually applied – mine kept silently failing to update while reporting it was fine. Curious what you find.
Hi Martin – This is great information. We had a ClickFix hack put on our site and we found a wpenginebot account. Our WordPress was running 6.0.12. Our logs from the when the hack are not available any more due to roll over. Are there public files on my domain I can see to determine if this exploit was used?
Hi Nick – I don’t think you had the same exploit. The batch endpoint bug was a regression introduced in 6.9, so 6.0.12 never had the vulnerable code. Same “wpenginebot” name means the same crew, but on a core that old they most likely came in through an outdated plugin or stolen admin credentials.
Either way the account creation itself leaves no public files – it’s just a row in the database. With logs gone, that row is your best evidence: user_registered on the wpenginebot user is the exact compromise time (survives in any DB backup even if you deleted the account), and session_tokens in wp_usermeta for that user ID stores their IP and user agent. Then look for files modified around that timestamp, PHP in uploads/, and plugins you don’t recognize.