When the cleanup went wrong

Put the original files back without wiping the site

A scanner reported that core, plugin or theme files no longer match the originals. Each of the three needs a different source, two of them have traps that hand you a file which looks right and is not, and one class of file must never be restored at all.

Checked against a live WordPress install on .

What modified actually means

A scanner calls a file modified when its contents differ from the published copy of the version you are running. That is a factual statement about bytes. It is not a statement that the file is malicious, and several honest reasons produce it.

  • Your host patched it. Managed hosts routinely ship small adjustments to core files, and some bundle their own must-use plugin that edits behaviour.
  • A developer edited it. Editing core is a bad idea and it still happens, usually years ago, usually by someone who has left.
  • The version is newer than the published checksums. When a release lands before the directory publishes its checksums, every file in that component comes back unrecognised at once. A whole plugin lighting up in the same instant is this, far more often than it is an attack.
  • Something was injected. The case you are here for.

Read the difference before you overwrite it. A restore is not reversible unless you kept a copy first, and the file you are about to discard may contain the only record of what was done to your site. If you are not yet sure the detection is real, settle that first with the three checks that tell a real detection from a false flag.

Every command and URL below was run against a real install on 18 August 2026: WordPress 7.0.4, PHP 8.2.31, WP-CLI 2.12.0. Output is quoted as it appeared.

WordPress core

Core is the easy one. Every release stays downloadable and the file list is fixed, so a byte-for-byte original is always available.

First find out what you are running. Do not guess, because downloading a neighbouring release replaces working files with almost-right ones.

bash
$ wp core version --extra
WordPress version: 7.0.4
Database revision: 61833
Package language:  en_US

Now download that exact version. There are two archives and the difference matters more than it looks.

  • wordpress-7.0.4-no-content.zip holds 3,901 files and contains no wp-content entry at all. Nothing in it can reach your themes, your plugins or your uploads.
  • wordpress-7.0.4.zip holds 4,401 files, and the extra 500 are the bundled default themes, Akismet and Hello Dolly. If you deleted those on purpose, this archive puts them back.

Use the first one. Swap the version number for yours.

bash
curl -O https://wordpress.org/wordpress-7.0.4-no-content.zip
unzip wordpress-7.0.4-no-content.zip

Everything unpacks under a single wordpress/ directory. Replace wp-admin/ and wp-includes/ wholesale, then copy the root wp-*.php files over. Leave wp-content/ alone, and leave wp-config.php alone.

A version that does not exist returns a clean 404, so a typo fails loudly here rather than quietly: wordpress-9.9.9.zip is a 404 with no body worth saving.

The same thing with WP-CLI

bash
$ wp core download --version=7.0.4 --skip-content --force
Downloading WordPress 7.0.4 (en_US)...
Success: WordPress downloaded.

Three things about that command are worth knowing before you run it, and all three were measured rather than assumed.

--skip-content does not skip wp-content. WP-CLI downloads the whole archive and then drops the bundled themes and plugins during extraction. Measured across 9,124 files, the run added exactly three 28-byte index.php files under wp-content/, wp-content/plugins/ and wp-content/themes/, and left every other file there byte-identical with unchanged timestamps. Those three are stock files that belong in a normal install. Pass the flag anyway: without it the full archive restores the bundled themes and Akismet as well.

--force restores and never deletes. In a test with five attacker-added files present, all five survived the run untouched. The install then verified against checksums while still carrying five warnings about them. Re-downloading core is repair, and it is not a cleanup.

It cannot touch wp-config.php. Not because WP-CLI protects it, though it does, but because no official archive contains one. They ship wp-config-sample.php and nothing else. The same is true of a root .htaccess.

Plugins and themes

These come from their own source, and the archive is per version. The URL pattern is the slug, a dot, the version, and .zip.

bash
https://downloads.wordpress.org/plugin/akismet.5.7.zip
https://downloads.wordpress.org/theme/twentytwentyfour.1.5.zip

Drop the version and you get the current release instead, which is not what you want mid-incident. Find your installed versions first:

bash
wp plugin list --field=name --field=version
wp theme list --field=name --field=version

To browse what is available, a plugin has a Previous Versions list at wordpress.org/plugins/<slug>/advanced/. Themes have no such page, and that URL is a 404 for them. Use the version directories at themes.svn.wordpress.org/<slug>/ instead. Plugins have an equivalent at plugins.svn.wordpress.org/<slug>/tags/.

The 404 that saves a file anyway

This one costs people an hour. A withdrawn version returns HTTP 404, and it returns it with a content-disposition header naming the zip you asked for and a nine-byte body containing the words "Not found". Your browser saves a file with the right name. It is not an archive.

Check the size before you unzip anything. A real plugin zip is measured in tens or hundreds of kilobytes. Nine bytes is a 404 wearing a filename.

Reinstalling with WP-CLI, and how it differs from core

bash
$ wp plugin install akismet --version=5.7 --force
Downloading installation package from https://downloads.wordpress.org/plugin/akismet.5.7.zip...
Removing the old version of the plugin...
Plugin updated successfully.
Success: Installed 1 of 1 plugins.

Read the third line. Unlike a forced core download, this deletes the existing directory before it unpacks the new one. That is good news for an infection: an injected file inside a plugin directory goes with it, and in the test that produced this output an injected akismet/dropper.php was gone afterwards, with verify-checksums moving from an error to a pass.

It is bad news for anything you legitimately kept inside a plugin or theme directory. Custom configuration, a licence file, a generated cache or an edit somebody made to a template goes too. Copy the directory somewhere else before you run it.

A version that cannot be found is safe. All of --version=9.9.9, --version=1.0 and a nonexistent theme version exited with an error and left the installed copy exactly as it was. Nothing is removed until the download succeeds.

One oddity worth knowing. If the version you pin happens to equal the current release, WP-CLI may resolve it to the unversioned download URL. A plugin was observed installing from the plain <slug>.zip under --version=8.0.4, even though the versioned URL for 8.0.4 is a 404. If the vendor ships a new release between your check and your run, you get that one.

When the version is gone, or never existed

Three situations have no published copy waiting for you, and each needs a different answer.

The version was withdrawn. Try the SVN listing, which often still carries tags the download server has dropped. Failing that, read the changelog for the nearest version you can get and decide whether the difference is acceptable. A file you cannot verify is worse than a file that is one patch newer.

The component is paid. Your account with the vendor has the build you licensed. Download it there and match the version number rather than taking the newest, because a major upgrade in the middle of an incident gives you two problems.

It is a child theme, or your own code. A child theme is a sibling directory of its parent, linked only by a Template: line in its style.css. In a test, the child theme directory contained exactly one file and every other template resolved from the parent. Reinstalling the parent from WordPress.org never touches the child, and asking WP-CLI to install the child fails with "Theme not found", because it was never published anywhere.

For that last case, restoring from upstream is not a thing that exists. Your sources are your own version control, a staging copy, or your host's backup. When none of those has a clean version either, the file has to be cleaned by hand, which is its own procedure.

The files you must not overwrite

Some files are site-specific by design. Restoring them from anywhere breaks the site more thoroughly than the infection did.

  • wp-config.php. Your database credentials, your salts and your table prefix. No archive contains it, no checksum set covers it, and no tool in this chain will even tell you it was modified. It also need not be in your docroot: on one install checked for this page it sat a level above. Run wp config path rather than assuming.
  • .htaccess. Rewrite rules, redirects and whatever your host added. The core archive contains no root .htaccess, so there is nothing to restore from. If yours was tampered with, read it and remove what does not belong. WordPress regenerates only its own block, between the BEGIN WordPress and END WordPress markers.
  • wp-content/uploads/. Your media library. It is in no archive and it survived every forced restore in testing. A PHP file found in there is worth removing on its own, and the images around it are not replaceable.
  • wp-content/mu-plugins/ and drop-ins. Often placed by your host. They load before regular plugins and they have no activation record, so a broken one cannot be turned off from the admin at all.

Checking the result, and what the check misses

For core, the check is one command.

bash
$ wp core verify-checksums --include-root
Success: WordPress installation verifies against checksums.

A file that differs, or one that should be there and is not, reports like this:

bash
Warning: File doesn't exist: wp-includes/atomlib.php
Warning: File doesn't verify against checksum: wp-includes/rss.php
Error: WordPress installation doesn't verify against checksums.

Now the part that catches people out, and it is the reason this section exists at all.

An unexpected file is a warning, not a failure. Here is real output from an install with four unknown files present, two of them named the way a dropper is named:

bash
$ wp core verify-checksums
Success: WordPress installation verifies against checksums.
Warning: File should not exist: wp-includes/unexpected_wp_includes.php
Warning: File should not exist: wp-config-backup.php
Warning: File should not exist: wp-admin/includes/index-injected.php
Warning: File should not exist: wp-admin/unexpected_wp_admin.php
$ echo $?
0

Success, printed before the warnings, and an exit code of zero. Only a missing or mismatched file sets the failure flag. Read the warnings, and never gate a script on the exit code alone.

The scan for unexpected files is narrow. By default it looks at wp-admin/, wp-includes/, and root files whose names begin with wp-. In testing, a file dropped at the web root under any other name was invisible, while wp-config-backup.php was caught only because of how it was named. Always pass --include-root, which widens the root check.

Core checksums say nothing about your themes, plugins, uploads or wp-config.php. The command discards every wp-content entry before comparing. The published data for 7.0.4 does carry 444 of them out of 3,945 entries, covering the bundled default themes and Akismet, and the command ignores all of them.

Plugins

bash
$ wp plugin verify-checksums --all
Error: Only verified 7 of 9 plugins (2 failed).
plugin_name  file         message
akismet      dropper.php  File was added

Two limits, both measured. It cannot see a deleted file, because it walks what is on disk and checks whether each file belongs. A plugin with a file removed reported "Success: Verified 1 of 1 plugins" and exited zero. And readme.txt and readme.md are treated as soft changes and ignored unless you add --strict.

A plugin that is not on WordPress.org is skipped, and the word Success still appears:

bash
Warning: Could not retrieve the checksums for version 1.0.0 of plugin acme-pro, skipping.
Success: Verified 0 of 1 plugins (1 skipped).

Read the count, not the verb. Verified 0 of 1 means your premium plugin passed by not being examined.

Themes

There is no check. wp theme verify-checksums is not a command, and the checksum endpoint behind it does not exist either.

bash
$ wp theme verify-checksums
Error: 'verify-checksums' is not a registered subcommand of 'theme'.

Plenty of guides tell you to run that. They did not try it. To check a theme, download the same version, unzip it somewhere else, and compare the two directories:

bash
diff -rq wp-content/themes/twentytwentyfour /tmp/twentytwentyfour

The check that matters most

Verification tells you your files match the originals. It does not tell you the site is clean, and after a restore those two feel like the same sentence. They are not. A backdoor added as a new file passes core verification with a warning and a zero exit code, sits in a plugin directory you did not reinstall, or lives somewhere neither command looks at all, such as your uploads folder or a must-use plugin.

Finish by looking at what changed recently rather than at what differs from upstream. If the site is still broken after the restore, the file you fixed was not the only one, and reading the fatal error properly will name the next one. If it comes back a week later, the entry point is still open and that is a different problem.

Questions

Will restoring core files delete my posts, pages or images?
No. Your content is in the database and your uploads are in wp-content, and neither is in the core archive. The archive that carries no wp-content at all is the safest one to work from, because then there is nothing in it that could reach your themes, your plugins or your uploads.
Does re-downloading core remove the malware?
Only the part of it that was written into a core file. Anything the attacker added as a new file is untouched, because a forced re-download writes the files the archive contains and walks past everything else. This was measured: five added files survived a forced core re-download, and the install then verified against checksums while still carrying five warnings about them.
Is there a wp theme verify-checksums?
No. WordPress.org publishes checksums for core and for plugins, and nothing at all for themes. The command does not exist and neither does the API endpoint behind it. Any guide that tells you to run it is repeating something it did not try. Download the same version of the theme and compare the directories instead.
Why did my premium plugin pass verification?
Because it was never checked. A plugin with no manifest on WordPress.org is skipped, and the command still reports success. Read the count in the output rather than the word Success: verified 0 of 1 plugins with 1 skipped means nothing was examined.
The exact version I have is no longer downloadable. What now?
Check the SVN listing for the plugin or theme, which usually still carries tags the download server has withdrawn. If it is genuinely gone, restoring the neighbouring version and accepting the difference is safer than leaving a file you cannot verify, so long as you read the changelog first. For code that never had an upstream copy at all, the file has to be cleaned by hand.
Can I just restore everything from my host's nightly backup?
You can, and it is the right move when the site is unusable and you know the backup predates the infection. What it does not do is close the way in. A backup from before the infection still contains whatever hole was used, so the site is available again and equally reachable.

Next

Doing this without the manual work

Everything above is one comparison repeated by hand: fingerprint each file, ask what it should be for that exact version, and put the right bytes back. Segurium runs that comparison for core, plugins and themes together and gives you the three answers per file. A modified file can be returned to its published contents, a missing one re-downloaded and recreated, and an unexpected one removed, one file at a time or in one pass, with a diff you can read first.

Restoring files that are merely wrong, rather than infected, is free and uncapped on both plans. It has the same blind spots as the manual procedure and we would rather you knew them: components with no published copy, such as a premium plugin or a custom child theme, are listed and never compared, and wp-config.php and your uploads are never fingerprinted at all.