This started as an SEO problem. Google had stopped crawling my new WordPress posts — brand-new articles sat for weeks marked “URL is unknown to Google, crawled: never.” I went looking for a technical reason it wouldn’t index. What I found instead was that the site had been quietly backdoored for roughly five months.
This is the write-up of that incident: how I found it, why the obvious fix (deleting the files) does nothing, the exact workflow that actually worked, and the one habit that would have caught it in March. I’ve deliberately kept the offensive detail out — no working payloads, no copy-paste attacker toolkit — and left in everything a site owner needs to detect and clean the same thing.
What you’ll learn
- Why “Google won’t index my posts” can be a security symptom, not an SEO one
- How a modern WordPress infection hides in plain sight
- Why deleting malicious files doesn’t remove the infection
- The detection workflow: one grep, one checksum command, one log read
- A prevention routine that turns a five-month breach into a one-day one
The symptom that didn’t fit
The pages were live. They returned HTTP 200, they were in the sitemap, they were internally linked. On paper there was no reason Google shouldn’t crawl them. When a site is technically correct and still isn’t being crawled, the usual explanation is authority — a low-trust domain gets crawled slowly. But the pattern here was too absolute: not “crawled rarely”, but “never crawled at all”, across every new URL for weeks.
So instead of tuning titles and internal links, I started reading the site’s own files — and that’s where it stopped being an SEO job.
The first thing that looked wrong
WordPress has a folder called mu-plugins (must-use plugins). Anything placed there loads automatically on every request and never appears in the normal Plugins screen. It’s a legitimate feature. It’s also a perfect hiding place.
In that folder sat a file with a boring, housekeeping-style name and a professional-looking comment header claiming to be a privacy/cache utility. Its actual contents were a large blob of obfuscated code — strings assembled character by character specifically so a human skimming the file, or a keyword search, wouldn’t recognise it. Nothing I had written looked remotely like that.
The uncomfortable part: I had seen that file listed earlier the same day. It was an order of magnitude larger than any real “cleanup” utility should be, and I skimmed straight past it because the name looked benign. I was pattern-matching filenames instead of reading contents. That’s the first lesson, and it’s a human one.
Anatomy of a self-healing infection
Once I started reading rather than skimming, the shape of it became clear. This wasn’t a single malicious file — it was a small toolkit designed to survive having pieces of it deleted:
The gear I run for this
Hardware from my own homelab, relevant to this guide — direct Amazon links.
As an Amazon Associate I earn from qualifying purchases. Affiliate links cost you nothing extra. Browse my full homelab store →
- A hidden administrator account, created recently, and a second component whose only job was to hide that account from the Users list in the dashboard, so it would never show up if you glanced at your users.
- A remote-control endpoint disguised as a normal-sounding WordPress file, gated by a fixed secret token, capable of running attacker-supplied commands and re-creating admin access on demand.
- A database drop-in file that would rebuild the main payload from a copy stored inside the database if the payload file ever went missing.
- A fake “plugin” that regenerated itself from an encoded data file.
- A scheduled task that kept the whole thing warm.
- The remote-control endpoint was scattered as several identical copies in different folders, so removing one changed nothing.
This is why the naive response fails. If you spot one bad file and delete it, the drop-in notices on the next page load and writes it straight back from the database. Delete the database copy and a request to one of the scattered endpoints restores everything. You can spend an afternoon deleting files and the site is re-infected before you finish.
The key that unlocked the cleanup
Every component — the scattered endpoints especially — shared a single fixed token string. That token was the thread to pull. One recursive search for that string across the whole account listed every copy at once, including the ones in folders I’d never have thought to check. The same search, run afterwards, is how you prove the site is actually clean rather than hoping.
Root cause, from the logs
The web server access logs told the origin story. Months earlier there had been a sustained brute-force campaign against xmlrpc.php — an old WordPress endpoint that accepts authentication attempts and is a perennial target. Around the same window, the site’s security plugin had been disabled. Shortly after, the first backdoor files appeared, and the logs showed the operator returning repeatedly, hitting those scattered endpoints to keep the infection alive.
So the entry wasn’t exotic. It was the boring, preventable combination that causes most WordPress compromises: an exposed brute-forceable endpoint, and a firewall that had been switched off. No integrity monitoring meant nothing raised a hand for five months.
The cleanup, in the order that matters
The single most important idea: break the self-healing loop before you delete anything. If you remove files while the restore mechanisms are still armed, they simply come back. So the order is deliberate:
- Kill the restore sources first. Remove the payload copy stored in the database, delete the malicious scheduled task, and neutralise the drop-in that does the rebuilding — together, so nothing can re-write anything.
- Quarantine, don’t delete. Move every malicious file (found via that shared token) into a folder outside the web root, so it’s no longer executable but still available as evidence. I kept a manifest of everything moved.
- Remove the rogue admin and the component hiding it, and confirm you’re the only administrator again.
- Restore clean core files. One tampered login file was flagged; re-downloading the exact WordPress version over the top fixed it and made the integrity check pass.
- Invalidate every session. Rotating the secret keys logs everyone out — including anyone holding a stolen login cookie.
- Close the door. Disable the brute-forced endpoint, turn the firewall back on, and disable in-dashboard file editing.
Then — and this is the step people skip — I re-ran the token search across the whole account and waited through live traffic to confirm nothing regenerated. Only when that came back empty was it actually over.
Verifying it’s really clean
- The core integrity check passes with no tampered files
- The unique token returns zero matches anywhere executable
- There is exactly one administrator — you
- The removed files stay removed after real traffic hits the site
- The database payload copy and malicious scheduled task are gone
Why this wasn’t a reason to abandon WordPress
It’s tempting, after something like this, to conclude the platform is the problem. It wasn’t. The breach was operational: an endpoint left open, a firewall switched off, and no integrity monitoring — none of which are WordPress’s fault, and all of which are cheap to fix. If anything, WordPress is what saved the cleanup: a single built-in command compares every core file against known-good checksums and tells you instantly which ones were altered. A hand-built system would have given me none of that and a larger custom attack surface to audit.
The prevention routine
The difference between a five-month breach and a five-day one is monitoring. What I put in place afterwards is deliberately boring:
- Keep the firewall on, and treat it being off as an incident in itself
- Disable the legacy brute-force endpoint unless something genuinely needs it
- Run a monthly integrity check and a scan of the auto-loading plugin folder
- Alert on any new administrator account
- Assume that after any compromise, every stored credential should be rotated — the attacker had months of read access
The lesson I’d underline
Two things. First, “Google won’t index my new content” is worth treating as a possible security signal, not just an SEO one — search engines quietly deprioritise sites that look compromised. Second, and more personally: the infection survived because it looked ordinary. Innocuous filenames, a professional-looking header, a hiding place that never shows in the dashboard. I’d looked right at the largest piece of it and moved on because the name was dull. The fix for that isn’t cleverness; it’s a routine that reads contents and checks integrity on a schedule, so a human skimming filenames was never the last line of defence.
This is a sanitised, first-person account of a real incident on a site I run. Specifics that would help an attacker — the live token, exact filenames as a kit, and server internals — have been deliberately left out; the detection and cleanup workflow is complete. AI assisted with drafting this write-up; the investigation, evidence and remediation were carried out on the live system.