← Blog

Security

Spotting a hacked WordPress install: a five-minute audit

A client emails on a Friday afternoon. Google has flagged their site. Here is the seven-command audit we run before the train at 17:30, in order.

Jacob Molkenboer· Founder · A Brand New Company· 3 Jun 2026· 6 min
Open leather logbook with brass key across spine, index card with green wax seal, brass loupe and inkwell on ivory paper.

A client emails on a Friday at 16:40. Google has flagged their homepage as deceptive site ahead. The site is a six-year-old WordPress install running their event-ticketing business. They want to know two things before they call their lawyer: are we hacked, and how bad is it. You have one coffee left and a train at 17:30.

This is the audit we run. It takes about five minutes if you have SSH and a database password, and it answers the only question that matters at the start: is the site compromised, yes or no.

The 90-second triage

Before SSH, look at the site from the outside. Open it in a private window. Search the domain in Google with site:domain.com and scroll past page one. A compromised WordPress install almost always sprouts SEO-spam pages: Japanese pharmacy, replica handbags, fake essay writing. If those show up under your client's domain, the compromise is real and probably weeks old.

Then check Google's transparency report at transparencyreport.google.com and paste the domain. If Safe Browsing has flagged it, you have a timestamp for when Google noticed. That is enough to know whether to keep going. Most of the time, the answer is yes.

Admin users and roles

SSH in. If wp-cli is installed (and it usually is on any host worth using), the first command is:

wp user list --role=administrator --fields=user_login,user_email,user_registered

You are looking for two things. Accounts you do not recognise, and account creation dates that cluster around a single hour. Attackers who get RCE almost always create a backup admin user with a plausible name (wpadmin, support, wp_user_42). Sometimes they edit the email of an existing admin instead, so cross-check emails against your client's real team.

If wp-cli is not available, hit the database directly:

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

Adjust the wp_ prefix. Many older sites use a custom one, and that itself is a clue if it does not match what the live config says.

File modification times in wp-content

The fastest signal of a compromise is files modified after the site's last known deploy. From the WordPress root:

find . -type f -mtime -14 \
  -not -path './wp-content/cache/*' \
  -not -path './wp-content/uploads/202*/*.jpg' \
  -not -path './wp-content/uploads/202*/*.png' \
  | sort

That gives you everything touched in the last fourteen days. Skim it. Anything in wp-includes or wp-admin is almost certainly bad: core files do not change between WordPress updates. Anything .php inside wp-content/uploads is bad full stop, no exceptions. WordPress never writes executable PHP into the uploads directory in normal operation.

A second pass narrows it to the worst offenders:

find ./wp-content/uploads -type f -name "*.php"
find . -type f -name "*.php" -mtime -7 \
  | xargs grep -l -E "(eval|base64_decode|gzinflate|str_rot13)\s*\("

The grep is noisy (some legitimate plugins use base64 for icons) but it surfaces obvious webshells in seconds. If you find a file called wp-login-old.php, xmlrpc-backup.php, or anything that looks like a plausibly-named core file in the wrong place, you have your answer.

Warning

Do not delete anything you find during the audit. You need the timestamps and contents intact for the cleanup phase, and for the insurance claim if the client has a cyber policy. Quarantine by moving outside the webroot, never by rm.

The wp-config and .htaccess checkpoint

Open wp-config.php. Look at the top and the bottom. Attackers love to prepend or append a single line that includes a remote file or registers an auto_prepend handler. If you see anything before <?php or after the closing PHP tag (there should be no closing PHP tag in a clean wp-config.php), that is the compromise.

Then .htaccess at the root. The WordPress block is well-known and stable. Anything outside the # BEGIN WordPress and # END WordPress markers is suspect. Specifically, look for:

RewriteCond %{HTTP_USER_AGENT} (google|bing|yandex) [NC]
RewriteRule .* http://evil.example/redirect.php [R=302,L]

That pattern, redirect search engine crawlers but show real users the normal site, is how SEO-spam infections hide from the site owner for months. The owner visits and sees their site. Google visits and sees a pharmacy.

Database options worth grepping

Two wp_options rows tell you a lot:

SELECT option_value FROM wp_options WHERE option_name = 'siteurl';
SELECT option_value FROM wp_options WHERE option_name = 'active_plugins';

If siteurl does not match the production domain, the site was rewritten to load assets from an attacker host. If active_plugins contains a slug that does not exist on disk in /wp-content/plugins, it is a phantom plugin loaded from elsewhere. Both are unambiguous.

While you are in there:

SELECT option_name FROM wp_options
WHERE option_name LIKE '%transient%'
  AND LENGTH(option_value) > 50000;

Webshells sometimes hide payloads in oversized transients. A 200KB transient with no obvious purpose is a flag.

Scheduled tasks and the cron table

WP-Cron is a comfortable place to hide a persistence mechanism. List it:

wp cron event list

Unknown hooks running every fifteen minutes (especially anything named like wp_update_check, wp_remote_call, or a 32-character hex string) are how attackers re-infect a cleaned site within a day. If you only fix the files and miss the cron, the site reinfects itself before you finish writing the post-mortem.

When to pull the plug

Three findings put the site into take it offline now territory: a webshell in uploads, a modified wp-config.php, or an unknown admin user with a recent login. Any one of those means the attacker has had code execution. Two or more means they probably still do.

The reflex is to delete the bad files and move on. Do not. The faster move is to put the site behind a maintenance page (a static HTML file in the docroot returning a 503 is enough), snapshot the filesystem and database for forensics, then start the rebuild from a known-good backup taken before the earliest suspicious file timestamp.

Takeaway

The five-minute audit gives you a yes-or-no, not a fix. Its job is to decide whether the next call to the client is “we're fine” or “we're going to maintenance mode while we investigate.”

The wider context

The economics of WordPress compromise have got worse this year. Automated scanners cycle through newly-disclosed plugin CVEs within hours, not weeks, so the gap between a vulnerability landing in the WPScan vulnerability database and the first opportunistic exploit reaching your client's install is now measured in the same business day. The audit above does not prevent that. It just makes sure you find out before the attacker has time to dig in. The Sucuri write-up on cleaning a hacked WordPress site is still the right reference for the next stage.

When we ran this audit for a Dutch events platform last month, the give-away was a single 14KB file at wp-content/uploads/2022/04/index.php that had been there for eleven months. Removing it was the easy part. The hard part was tracing the entry vector to an unpatched plugin CVE from late 2022. We then folded the same checklist into our standard legacy migration handover so the next person who inherits a site can run it on day one.

Five minutes of investment. Save the seven commands above into a single shell script on your laptop. The next time a client calls about a flagged site, you will have an answer before the kettle boils.

Key takeaway

The five-minute WordPress audit exists to deliver a yes-or-no, not a fix: do you take the site offline right now, or carry on with your day.

FAQ

Can I just reinstall WordPress core to fix a hack?

Reinstalling core overwrites wp-admin and wp-includes but leaves wp-content, the database, and any added admin users intact. So no, it does not fix most real compromises.

How long does a full cleanup take after a positive audit?

Plan for half a day for a small site if you have a clean pre-compromise backup, two to five days without one. Most of the time goes into identifying the entry vector so the site does not reinfect.

Will a plugin like Wordfence catch this for me?

Sometimes. Wordfence and similar plugins catch known signatures and unexpected file changes, but a tuned attacker can sit quietly under their thresholds. Run a manual audit anyway.

Is there a tool that automates the seven commands?

WPScan, Sucuri SiteCheck, and the wp-cli-security package cover most of it. We still run the manual checks because tools miss the contextual signals like staff names and deploy timestamps.

wordpresssecurityphplegacy sitesoperations

Building something?

Start a project