Skip to content

Security

How to clean up a WordPress site after wp2shell.

In July 2026, WordPress fixed two chained bugs in its core, known together as wp2shell (CVE-2026-63030 and CVE-2026-60137). They sit in the REST API batch endpoint, which every standard WordPress installation has. An attacker needs no account and no special plugin. One anonymous request is enough to run code on your server.

The affected versions are 6.9.0 to 6.9.4 and 7.0.0 to 7.0.1. The fix came out on 17 July 2026, and attacks in the wild started a few days later.

If your site ran one of those versions in that window, updating is only half the job. The update closes the hole, but anything an attacker installed before that stays where it is.

Before you touch anything, back up the files and the database, and keep the copy away from the live site. A backup of a hacked site sounds pointless until the cleanup breaks something, or until you need to work out later what happened.

Was your site hit?

Your server access logs will tell you whether someone tried. Look for requests to /wp-json/batch/v1 or ?rest_route=/batch/v1, especially POST requests from addresses you do not know. A log entry only proves an attempt, so work through the checks below either way.

Step 1: close the hole first

Update WordPress core to 6.9.5, 7.0.2, or later before you start cleaning. Otherwise you are mopping the floor with the tap still running.

If you cannot update today, block anonymous access to /wp-json/batch/v1 and ?rest_route=/batch/v1 in your firewall or ask your host to do it. That buys you time until you can update.

Step 2: remove files that do not belong there

The published attack chain creates a new administrator account and then uses it to upload a plugin with a webshell inside. A webshell is a small PHP file that gives the attacker a remote control for your server. Look in these places:

  • wp-content/plugins/ for plugins nobody on your side installed. They often have random names and no description.
  • wp-content/mu-plugins/ for anything you did not put there. Must-use plugins load automatically and do not appear in the normal plugin list, which makes this folder a good hiding place. If you are not sure what a file does, ask whoever built the site before you delete it.
  • wp-content/cache/, wp-content/uploads/, and wp-content/upgrade/ for PHP files of any kind. They have no business there. Webshells with random names have turned up in cache folders.
  • wp-content/ itself for new folders with random names. Some webshells were written there too.

Ignore the file dates. Attackers can change them.

Step 3: check users, passwords, and sessions

Assume the attacker read your whole database, password hashes included.

Remove unknown administrators

The known attack accounts have usernames that start with wp2_ or w2s_ and email addresses at wp2shell.invalid or wp2shell.shellcode.lol. An attacker can pick any name, so look for every account you do not recognise, and check the database as well as the dashboard, because malicious code can hide users from the user list.

This query lists new and suspicious accounts:

SELECT ID, user_login, user_email, user_registered
FROM wp_users
WHERE user_registered >= '2026-07-01'
   OR user_login LIKE 'wp2\_%'
   OR user_login LIKE 'w2s\_%'
   OR user_email LIKE '%@wp2shell.%';

This one lists everybody with administrator rights:

SELECT u.ID, u.user_login, u.user_email
FROM wp_users u
JOIN wp_usermeta m ON m.user_id = u.ID
WHERE m.meta_key = 'wp_capabilities'
  AND m.meta_value LIKE '%administrator%';

Both use the default table prefix wp_. If your site uses a different one, change it everywhere, including inside wp_capabilities.

Reset every password

Start with the administrators, then do everyone else. Change the database password too, and put the new one in wp-config.php.

Log everyone out

Generate new keys with the official WordPress secret key generator and replace the old ones in wp-config.php. Every existing login stops working, including the attacker’s.

Step 4: clean up the database

To get past the security checks, the exploit abuses a few kinds of internal WordPress records: customizer changesets (customize_changeset), navigation menu items (nav_menu_item), and oEmbed cache entries (oembed_cache). Some of them can stay behind in the database.

Your site uses the same record types for normal work, so do not delete them in bulk. Look first:

SELECT ID, post_type, post_date, post_parent, post_title
FROM wp_posts
WHERE post_type IN ('customize_changeset', 'nav_menu_item', 'oembed_cache')
  AND post_date >= '2026-07-01'
ORDER BY post_date;

Be suspicious of a post_parent far higher than any real post ID on your site, of oEmbed cache entries that point back at your own site, and of several of these records created within seconds of each other.

Once you are sure a record is not yours, delete it with its metadata. Swap the example IDs for the ones you found:

DELETE FROM wp_postmeta WHERE post_id IN (1234, 1235);
DELETE FROM wp_posts WHERE ID IN (1234, 1235);

While you are in there, check active_plugins, siteurl, and home in wp_options. If any of them lists a plugin or an address you do not know, someone changed it.

Step 5: make the next attack harder

Stop file changes from the dashboard

Add these lines to wp-config.php:

define( 'DISALLOW_FILE_EDIT', true );
define( 'DISALLOW_FILE_MODS', true );

The first line removes the built-in theme and plugin editor. The second also blocks plugin and theme installs from the dashboard, so even an attacker with an administrator login cannot upload a webshell plugin that way.

The catch is that you cannot install or update plugins, themes, or WordPress itself from the dashboard either. You need another way to update, such as WP-CLI, a deployment process, or managed updates from your host. If you do not have one, use only the first line. A site that never gets updated is a bigger problem than the one you are trying to solve.

Take away the database user’s FILE privilege

One variant of the attack writes PHP files straight from the database to the disk with SELECT ... INTO OUTFILE. That only works if the WordPress database user has the FILE privilege, and WordPress never needs it. Log in to the database as root and run:

REVOKE FILE ON *.* FROM 'wp_user_name'@'localhost';
FLUSH PRIVILEGES;

Replace wp_user_name and localhost with the values from your setup. Managed hosts almost never grant this privilege in the first place. On shared hosting you probably cannot change it yourself, so ask your host to confirm.

When you are done

Keep the pre-cleanup backup for a few weeks, and keep an eye on the site, the user list, and the plugin folders. If something you deleted comes back, there is a backdoor you missed, and the hunt starts again.

For the general order of steps after any compromise, read what to do when your WordPress site looks compromised.

From the packages

Do you need help with the cleanup?

WordPress hack recovery gets your site cleaned, hardened, and back online, with a clear record of what was done and why.

Work with me

Tell me about your project

Send the details and I will reply within one working day. No automated responses, no offshore handoffs.

Get in touch