How to Manually Scan and Clean WordPress Malware (Without a Plugin)
Recently we published a full walkthrough of a hacked website recovery. In that one we used MalCare and an AI assistant to find the malware and clean it out. It worked, and for most site owners that's still the fastest way home.
Today we're doing the opposite. No security plugin. No AI. No paid scanner. Just free tools, your hosting panel, and a method that runs in order.
This is close to what our team does on a client site before we touch anything or bill anyone. The work isn't difficult. It isn't fast either, so block out an afternoon and do the steps in sequence rather than jumping to the one that sounds most relevant.
One thing before we start. Panic breaks more sites than malware does. People delete the wrong folder at 2am and lose a year of content that was never backed up properly in the first place. So we're going to slow down, take a copy of everything first, and work through it.
Let's get into it.
Read this before you touch anything
There are two things you can do in the next five minutes that you can't do later, and almost every guide on the internet skips both of them.
Download your logs. Go into your hosting panel and pull the raw access logs and error logs for your domain. Most shared hosts keep between seven and thirty days of them, then overwrite. Those logs are the only record of how somebody got in. Once they roll over, that answer is gone permanently and you'll be cleaning a site without ever knowing which door was open.
Take a snapshot with the timestamps intact. Not a backup plugin export. Download the files over SFTP, or use your host's full backup feature, and export the database separately through phpMyAdmin. Keep it somewhere off the server and don't touch it.
That snapshot is evidence, not a restore point. You will never put it live, because it's infected. Its whole job is to preserve the file modification dates, which are the closest thing you have to a timeline.
Here's why the order matters. The moment you replace your WordPress core files, every date stamp on those files resets to today. The infected copies are overwritten. The pattern that would have told you the injection landed on a Tuesday in March, and that four files were written within the same minute, is destroyed. You've cleaned the site and thrown away the case file at the same time.
We put evidence preservation ahead of cleanup on every job, and it's the single habit that separates a cleanup that holds from one that gets undone in a fortnight.
Should you even do this by hand?
Honest answer first. For most infections, installing a good security plugin and hitting scan is the correct move, and it's the whole job. That's what most professionals reach for, and there's nothing embarrassing about it.
Manual cleanup makes sense when the situation looks like this:
One WordPress site, on one hosting account
You still have access to your hosting panel, even if wp-admin is locked
The symptoms point at files, so redirects, injected scripts, spam pages, a defaced homepage
No live payment processing on the site
You've got a free afternoon and you'd rather understand your own site than pay somebody
It's the wrong call when:
You're running a store with live card data going through checkout
There are several sites sharing one hosting account, because cleaning one of them achieves almost nothing
Your host has already suspended the account
Downtime costs you more per hour than a cleanup would cost outright
You've cleaned it once already and it came back
If you're on that second list, stop here. Manual cleanup on a live store or a suspended account usually costs more than it saves. That's the situation our [emergency malware removal service](LINK: service URL) exists for, and this guide will still be here afterwards.
Why free and manual can beat a paid scanner
A scanner works by matching what's on your server against a database of known malicious code. It's fast, it's automated, and it catches the overwhelming majority of infections, because the overwhelming majority of infections are recycled.
The gap is what happens when the code isn't in the database. A backdoor written for your specific site, or an obfuscated loader with a fresh variable name, matches nothing. The scanner reports clean. The site is not clean.
Checksum verification doesn't work that way. It doesn't ask whether a file is malicious. It asks whether the file on your server is byte-for-byte identical to the one WordPress.org shipped. If a single character was added to wp-includes/functions.php, the hash won't match, and it doesn't matter one bit whether anyone has seen that particular malware before.
That's the argument for doing this by hand. You're trading speed for a different kind of coverage.
Part 1. Scan: nine free checks, in order
Work through these in sequence. Each one has an SSH version and a version that works with nothing but File Manager and phpMyAdmin, because plenty of shared hosting plans never give you a terminal.
Don't fix anything yet. Right now you're only building a list.
1. Run a free remote scan
Put your URL into Sucuri SiteCheck. It visits your site the way a visitor would and reports injected JavaScript, hidden iframes, spam links, redirects, and whether any major blocklist has flagged you. Quttera does the same thing, though free scans sit in a queue so it can take a while. VirusTotal and urlscan.io are worth a second opinion.
Understand the limit before you trust the result. A remote scanner only sees what a browser sees. It cannot read your server files. A backdoor sitting quietly in wp-content/uploads waiting for a request will not show up here, and a clean result from SiteCheck means very little on its own.
2. Open Google Search Console
Security Issues in the left menu. Free, takes ten seconds, and most site owners have never opened it. If Google has detected the hack it will tell you the category, sometimes with sample URLs, which is genuinely useful because it points you at the affected pages.
While you're there, check Pages for URLs you never created. Pharma spam and Japanese keyword injections usually show up as hundreds of indexed pages nobody wrote.
3. Verify your core files
This is the check that finds what scanners miss.
With SSH:
wp core verify-checksums
It compares every core file against the official hashes and lists anything modified, plus anything sitting in a core directory that shouldn't be there.
Without SSH, do it by hand. Check your WordPress version in the dashboard footer or in wp-includes/version.php. Download that exact version from wordpress.org, unzip it locally, then compare it against your live wp-admin and wp-includes folders in File Manager. You're looking for files that exist on your server and not in the fresh copy. Malicious files are very often given names designed to look native, so wp-admin/includes/class-wp-cache.php looks fine until you notice the clean download doesn't have it.
One false positive worth knowing about: if you've ever deleted readme.txt or license.txt for tidiness, the check fails on those and it means nothing.
Some hosts, Hostinger and a few others, expose a Terminal in the panel with WP-CLI already installed, so check before you assume you're stuck with the manual route.
4. Compare your themes and plugins against clean copies
For anything from the WordPress.org repository:
wp plugin verify-checksums --all
Premium plugins and custom themes have no checksum source, which is a real gap and it's where a lot of infections hide. The workaround is a folder diff. Download a fresh copy of the plugin from wherever you licensed it, put your infected copy in one folder and the clean copy in another, and run a comparison tool over both. Beyond Compare, WinMerge and Meld all do this and the free options are fine. Focus on .php and .js files that differ.
Custom themes with no clean copy anywhere are the hard case. There you're back to reading the files.
5. Find recently modified files
This is usually the moment the infection becomes visible.
find . -name "*.php" -mtime -15 -ls
Fifteen days is a starting point. Widen it to sixty or ninety if nothing turns up, and remember your infection is probably older than you think it is. We had one client site where the entry point was 464 days before anybody noticed anything was wrong. [LINK: pillar article on dwell time]
In File Manager, switch to list view and sort by Last Modified. Same result, more scrolling.
What you're looking for is a cluster. A single file modified during your last plugin update is normal. Nine files modified at 4:17am on a date when nobody logged in is not.
6. Look for PHP where PHP shouldn't be
find ./wp-content/uploads -name "*.php" -ls
Best signal-to-noise ratio of anything on this list. Your uploads folder holds images, PDFs and videos. A .php file in there is almost always malicious, and the handful of legitimate exceptions announce themselves clearly.
Check wp-content/cache too, and any folder at the site root that you don't recognise.
7. Search for suspicious code patterns
Here's the grep block that appears on every guide covering this topic:
grep -rn "base64_decode\|eval(\|gzinflate\|str_rot13\|shell_exec\|passthru(\|assert(" \
wp-content/ --include="*.php" | grep -v cache
Now here's the part those guides leave out. You're about to get somewhere between two hundred and two thousand results, and most of them are completely innocent. Caching plugins use base64_decode legitimately. So do licensing systems, image optimisers, and half the page builders on the market. If you start deleting on function name alone you will break your site before you clean it.
Triage them like this. Function name is the weakest evidence you have. Location and file date are the strongest. A base64_decode inside a well-known plugin's own directory, in a file dated the same as every other file in that plugin, is background noise. The same function in a file that appeared last month, in a directory where it doesn't belong, sitting on one enormous line with no formatting, is your malware.
Long strings of hex escapes and variables named things like $GLOBALS['_1234'] are worth a separate look:
grep -rn "\\\\x[0-9a-f]\{2\}" wp-content/ --include="*.php" | head -50
8. Search the database
Open phpMyAdmin and run these against your database. Swap wp_ for your actual table prefix if it's different.
SELECT ID, post_title FROM wp_posts WHERE post_content LIKE '%<script%';
SELECT ID, post_title FROM wp_posts WHERE post_content LIKE '%<iframe%';
SELECT option_name FROM wp_options WHERE option_value LIKE '%base64_decode%';
SELECT user_login, user_email, user_registered FROM wp_users ORDER BY user_registered DESC;
Then check for oversized autoloaded rows, which is where injected payloads like to sit because they load on every single page:
SELECT option_name, LENGTH(option_value) AS size
FROM wp_options WHERE autoload = 'yes'
ORDER BY size DESC LIMIT 20;
Anything over a few hundred kilobytes at the top of that list deserves a look. Some of them will be legitimate theme settings. Some won't.
On the users table, any account you didn't create is a finding. Attackers add administrators routinely, and they name them things like wpadmin, backup, or a plausible-looking first name.
9. Check the quiet places
These are where persistence hides, and where most DIY cleanups fall down a week later.
wp-config.php. Read it line by line. It should contain database credentials, salts, table prefix and a few defines. Any eval, base64_decode, include of an unfamiliar path, or a block of code above the opening comment is a problem.
.htaccess, at every level. Root, wp-admin, wp-content, uploads. Look for rewrite rules that redirect conditionally based on user agent or referrer. That's how cloaked spam serves one thing to Google and another to you. Also check for hidden .htaccess files, because File Manager hides dotfiles until you turn that setting on.
wp-content/mu-plugins. Must-use plugins load automatically and never appear in the plugins list. Most sites don't have this folder at all. If yours does and you didn't create it, read everything in it.
Drop-ins. object-cache.php, advanced-cache.php and db.php sit directly in wp-content and WordPress loads them without asking. They're legitimate when your caching plugin put them there. They're a common hiding place otherwise.
Cron. wp cron event list shows scheduled tasks. A cron job that re-writes a backdoor every six hours is the classic reason a site reinfects itself. Without SSH, look at the cron row in wp_options.
Application passwords. In wp_usermeta, meta key _application_passwords. These survive a password reset, which is exactly why attackers create them.
Write it all down
Before you clean anything, open a spreadsheet or a text file and record every hit. File path, modification date, what the code appears to do, and how confident you are that it's malicious. Three columns and an honest confidence rating.
This is the difference between cleaning a site and guessing at one. When something goes wrong in Part 2, and it might, this sheet is what lets you work backwards.
That sheet is also, roughly, what a security audit produces. The difference is scope. An audit runs these same checks across every site on the hosting account, correlates the findings against server access logs to work out the entry point, cross-references your installed versions against known vulnerabilities, and comes back as a written report rather than notes in a text file. If you'd rather have the diagnosis without spending the afternoon, that's our WordPress security audit. If you want to keep going, Part 2 is where the cleanup starts.
Part 2. Clean: replace, don't repair
One rule governs this entire section. Never edit malware out of a file when a clean copy of that file exists. Replace the whole file.
Editing feels safer because it's less destructive. It isn't safer. You remove the obvious injected block at the top, miss the second one buried at line 400, and now you have a file that looks clean and isn't.
Replace WordPress core
Don't overwrite the folders, rename them. Overwriting leaves behind any file the attacker added that isn't part of core, because nothing overwrites a file that has no counterpart.
mv wp-admin wp-admin-old
mv wp-includes wp-includes-old
wp core download --version=$(wp core version) --skip-content --force
Without SSH: rename wp-admin and wp-includes in File Manager, upload the fresh copies from your wordpress.org download, then replace the loose .php files at the site root with the clean versions. Leave wp-config.php and wp-content alone for now.
Once the site loads, delete the renamed folders.
Replace themes and plugins
Delete each plugin folder entirely, then reinstall from the repository. Deleting through the dashboard and reinstalling does the same thing more slowly.
For premium plugins, download fresh from your account and upload. For a custom theme with no clean copy, you're stuck comparing against your findings sheet and cleaning by hand, which is the one place in this whole process where careful reading beats any tool.
Delete anything you're not using. Not deactivate. Delete. A deactivated plugin's files still sit on your server and still execute if something calls them directly.
Deal with uploads separately
Don't try to clean the uploads folder. Delete the malicious PHP files outright. There's no legitimate version of them to restore, and nothing in there should be executable in the first place.
Never wipe the whole folder. That's your media library and it's usually irreplaceable.
Clean the database by hand
Export a fresh database backup before you change a single row.
Work from the query results in step 8. Open each flagged row, find the injected block, remove it, save. Injected scripts in post_content usually sit right at the end of the content or immediately after the opening tag, and they're recognisable because the formatting doesn't match anything around them.
Delete unauthorised admin accounts. If you're unsure whether an account is real, check user_registered against your own records rather than guessing from the name.
Test the site after each table you touch.
Kill the persistence
This is the section other guides compress into one sentence, and it's the reason so many DIY cleanups come back.
Go through every item from step 9 and clear it. Rogue cron entries. The mu-plugins folder. Drop-in files your caching plugin didn't create. Malicious .htaccess rules. Injected code in wp-config.php. Application passwords you didn't generate.
A backdoor that survives this stage undoes everything you've done in the last three hours, usually within a day or two.
Rotate every credential
All of these, not some of them. If the attacker had file access, assume they read wp-config.php, which means they have your database credentials.
Database password, and update it in wp-config.php
Hosting control panel
FTP and SFTP accounts, including any you forgot you created
SSH keys
Every administrator account in WordPress
WordPress salts, regenerated from the official secret-key service and pasted into wp-config.php, which force-logs-out every existing session
Any API keys stored in wp-config.php or plugin settings
Email accounts on the domain
Regenerating the salts is the step people skip, and it's the one that kicks an attacker out of a session they still have open right now.
Part 3. Verify: prove it's clean
Cleaning and confirming are two different jobs.
Run the remote scanners again, all of them. Then check what Google sees rather than what you see, because cloaked spam shows nothing to a normal visitor. The URL Inspection tool in Search Console fetches your page as Googlebot and shows you the rendered result, which is where the pharma links usually turn up.
Search your own domain with site:yourdomain.com and page through the results looking for anything you didn't publish.
Check outbound mail. A hacked site sending spam damages your domain reputation long after the files are gone, and if your newsletter starts landing in junk folders three months from now, this is why.
Then watch the logs for forty-eight hours. Reinfection is fast when it happens. Repeat requests to a file path you've never heard of mean there's still a backdoor answering, and you go back to Part 1.
Once you're satisfied, request a review in Search Console under Security Issues. Google typically takes a few days.
Be honest with yourself about what you've achieved. You've proved that no known infection remains and that the obvious persistence is gone. You haven't proved there's no backdoor anywhere on the server, and nobody can prove that, including any paid service that tells you otherwise.
Part 4. Close the door, for free
Cleaning without patching means you'll be doing this again next month. Every one of these is free.
Update everything. WordPress core, all plugins, all themes, PHP version. The vulnerability that let them in is usually one of them, and if you skip this the cleanup is temporary by design.
Block the file editor. Add this to wp-config.php:
define('DISALLOW_FILE_EDIT', true);
An attacker with a stolen admin login uses that editor to write a backdoor in about four seconds.
Stop PHP running in uploads. Create an .htaccess file inside wp-content/uploads containing:
<Files *.php>
deny from all
</Files>
On nginx you'll need a location block in the server config instead, so ask your host.
Fix permissions. Files 644, directories 755, wp-config.php 600 if your host allows it. Nothing needs 777, ever, whatever a forum post from 2013 told you.
Delete what you don't use. Every plugin and theme you're not actively running. Old staging copies. That backup folder you made in 2022 and forgot about, which is running an unpatched WordPress install right now.
Turn on two-factor authentication for every administrator. There are free options and one of them takes ten minutes to set up.
Set up monitoring. You now know which files should never change and which folders should never contain PHP, which means you know exactly what to watch for. A simple automated file-change alert catches the next attempt in hours rather than months, and it costs nothing to build.
Everything above is a one-time action. The part that isn't one-time is the watching. Plugins update, new vulnerabilities land every week, and the setup you hardened today drifts out of shape by itself. That's what a website maintenance plan covers, so updates get tested before they go live, files get monitored, backups get restored from occasionally to prove they work, and somebody reads the logs. If you'd rather build your own version of that, the free file-change alert above is a genuine starting point and we'd rather you had it than nothing.
When this method won't hold
There are situations where manual cleanup fails no matter how carefully you work, and it's better to know them now than at 3am next Thursday.
It came back within days. Either the entry point is still open or the backdoor was somewhere you didn't look. Cleaning again the same way produces the same result.
Several sites share the hosting account. Cross-contamination between sites in the same account is routine. Cleaning one of five is close to pointless.
The payload isn't on your server. Some modern loaders keep almost nothing locally. The file on your site is a few lines that fetch the real code from somewhere external at runtime, so it barely registers as suspicious and there's nothing on disk to find. We worked one incident where the payload was being served from a smart contract on a public blockchain, which meant it couldn't be taken down at all.
The compromise is above WordPress. If the server itself is compromised, everything in this guide is rearranging furniture.
You're blocklisted, suspended, or handling card data. Different problem, different urgency, and the clock is expensive.
Manual cleanup costs you a working day and carries a real chance of missing something. That's the honest trade and for a lot of sites it's a good one.
What we do differently from a scanner-driven cleanup is find the entry point and reconstruct the timeline, so you end up with a written record of how they got in and how long they were inside, not just a list of deleted files. On the client site mentioned earlier, that record ran to 464 days. If that's the level you need, that's our emergency malware removal service.
If you'd rather stay in-house but want continuous coverage from here on, a paid scanner buys you monitoring rather than a better clean. MalCare, Wordfence and Sucuri all do this well and they're worth comparing properly.
FAQ
How long does this take?
Three to six hours the first time, spread across an afternoon and evening. The scanning half takes longer than the cleaning half, which surprises most people.
Can I do this without SSH?
Yes. Every step here has a File Manager or phpMyAdmin equivalent. It's slower and the folder comparison in step 4 is more tedious, but nothing is impossible. Check whether your host offers a Terminal in the panel before assuming you don't have one.
Should I just delete everything and reinstall WordPress?
Tempting, and occasionally right. The problem is that your database, your uploads folder and your custom theme all have to come across anyway, and those are exactly where infections hide. You'd still be doing Part 1.
Will this remove the Google warning?
Not automatically. Clean the site, then request a review in Search Console. The warning usually clears within a few days once Google recrawls.
My site got reinfected. What did I miss?
Almost always one of four things. A cron job, an mu-plugin, a drop-in file, or a credential you didn't rotate. Go back to step 9 and read it properly this time.
Is this safe on a live ecommerce site?
No. Take it offline first, or hand it to somebody who does this for a living. Card data changes the calculation completely.