Invasion Investigation · Part 10
Most hardening advice wouldn’t have saved you
Every checklist opens the same way. Update your plugins. Use strong passwords. Install a security plugin. Enable two-factor.
All correct. None of it would have prevented the compromise documented in this series, and pretending otherwise is how people do everything on the list and get hit anyway.
No plugin was exploited here. No password was brute-forced. There was no vulnerability, and none was needed. So this article is organised honestly, in three tiers: what would have stopped it, what would have caught it sooner, and what’s worth doing regardless while changing nothing about this case.
Tier one: what would actually have stopped it
Get credential-bearing files out of your web root
This is the whole case in one instruction.
From the case file. The publishing software used to build these sites offered to keep its project file on the server. That file contains the publish profile — hostname, username, and password — scoped to the account root. Every site, not one.
The owner used that option, as designed. No exploit was ever required.
And it generalises well past one product. The credential doesn’t have to be stolen from you — it can be published by you:
- FTP client site managers and saved connection files
- Editor and IDE sync configurations (
sftp-config.jsonand its relatives) - An exposed
.gitdirectory - A stray
.env - Any backup archive sitting somewhere reachable
Go and look. List everything in your web root that isn’t a web page, and ask what each file knows. That single audit is worth more than everything in tier three combined.
And know that rotation doesn’t save you from it
From the case file. The owner changed that password regularly — more than once a year, for twenty years. It made no difference, because every time he published, the project file went back up carrying the current password.
Rotation only works if nothing on the server is holding a copy. Otherwise you’re refreshing the credential in a reachable location on a schedule.
Turn PHP off where it doesn’t belong
The other half of this case, and it’s a setting most owners have never seen.
From the case file. The first intrusion wrote twelve PHP files across two static websites, ten of them into
assets/images/autogen/— image directories, on sites with no application on them at all.Which raises the question worth taking to your own host: why would PHP execute there? Those files are inert text unless something is configured to run them. Check whether it is — on every site in your account, including the ones that have no use for it.
Deny PHP execution in uploads, assets, image and thumbnail directories, and in cgi-bin. On sites with no application at all — a brochure site, a legacy static site, an archived project — turn PHP off entirely.
This is the single highest-value change on this page, and it’s usually a few lines of configuration your host can apply.
Audit your .htaccess for handlers you didn’t add
While you’re there, read the whole file. You’re looking for anything that makes a file type executable that shouldn’t be — including directives that hand HTML files to a processor, which quietly turns an innocent-looking .html into something that can run.
If you find one and can’t explain it, ask your host whether they added it and why.
Stop using plaintext FTP
Port 21 sends credentials in the clear and has no second factor, and never will. Use SFTP, or SSH keys, or nothing. Two-factor authentication on your control panel does not protect a protocol that doesn’t support it.
Lock down remote database access
From the case file. One domain in this account had remote database access configured to accept connections from any host on the internet.
Your database should accept connections from localhost and nothing else, unless you have a specific reason and a specific address.
Protect what your backup plugin writes
The modern equivalent of the project-file problem. A full-site archive contains your configuration file, live database credentials, your users table and everything the site stores. If it lands somewhere reachable, that’s the same failure with a newer logo.
Verify it rather than trusting it. Backup plugins protect their own folders with a rule that is often written to disable itself silently if the server doesn’t support it — and on some configurations it’s ignored entirely.
Test it: request one archive’s URL directly in a browser. You should get a refusal. If a download starts, you have a serious problem right now, and it has nothing to do with whether you were ever hacked.
The one that didn’t prevent anything — and decided everything
This belongs in its own category, because it stopped nothing and still turned out to be the most valuable decision in the whole story.
Build somewhere else and publish up.
If your site is authored locally and pushed to the server, then the server is a deployment and the master lives on a machine an intruder never touches. Getting compromised means republishing. Hours, not weeks, and no question about whether the result is clean.
If your site is edited live in a browser — which is how most WordPress sites are run — the server is the master. There’s no clean copy anywhere. Your only fallback is a backup, and a backup is only useful if you know it predates the compromise, which after a long dwell time you usually can’t.
From the case file. Every site in this account was built locally and published up. The server held more than 45,000 files and a compromise nobody could fully account for. The desktop held a clean copy of the same sites, slightly out of date and entirely trustworthy.
That difference is what made walking away affordable. Cleaning meant vouching for 45,000 files. Leaving meant republishing.
Nobody frames local-first development as a security posture, and it is one. Not because it stops anyone getting in — it doesn’t — but because it decides the price of every recovery you will ever do.
For WordPress specifically, where live editing is the norm, the equivalent is: keep your themes and custom code in version control off the server, keep a documented list of exactly which plugins you run and why, and make sure your backups are stored somewhere the web server can’t reach. That won’t give you a true master, but it converts “rebuild from nothing” into “reinstall and redeploy,” which is a different order of cost.
And the honest limit: a clean master restores your website. It restores nothing about your standing in search — that lives in somebody else’s index and there is no copy of it you control. Which is exactly why it’s worth making the site half cheap: it frees the hours for the half that isn’t.
Tier two: what would have caught it eleven months sooner
A known-good baseline
The reason comparison beats scanning: modern payloads are built fresh per victim, so there’s no signature to match. But there’s nothing to compare against unless you captured a clean state first.
Take a full file listing with dates and sizes when the site is healthy. Store it off the server. That listing is what makes “what’s changed?” a five-minute question instead of a three-week one.
File integrity monitoring automates this, and it’s worth it — but the manual version costs nothing and is far better than neither.
Search Console on every domain — including the ones you’ve forgotten
From the case file. The intruders lived for five months in a static brochure site from 2022 that nobody had looked at since. It had no dashboard to raise an alarm, no updates to fail, and no Search Console property — so when it started publishing thousands of pages, there was nothing anywhere to notice.
Ten minutes per domain. Free. It’s the cheapest insurance in this entire series, and it should cover the sites you don’t think about, because those are the ones you won’t be watching.
Ask your host to extend log retention
Most hosts keep seven to thirty days.
From the case file. The earliest surviving log in this investigation begins seventeen months after the intrusion started. The entry point can be narrowed and argued but never proven, because the only evidence that would have settled it had already rotated away.
One support ticket. It costs a sentence and it’s the difference between an answer and an inference.
And watch for one specific event
If every plugin on your site deactivates at once, that is not a glitch and not a failed update.
From the case file. The bulk account-creation tool recovered here runs one query before anything else: it empties the active plugins list. Security software goes first, then the administrator account gets created.
That’s an alarm worth wiring up, and most people would read it as a bug.
Tier three: correct, worth doing, and irrelevant to this case
Everything here is genuinely good practice. None of it touches a credential-based entry, and it’s listed honestly rather than padded into tier one.
Update everything. Closes the most common door, which is not the one used on you.
Delete unused plugins and themes. Inactive code is still on disk and still reachable. And know what’s installed, so an addition is visible — this compromise added two file-manager plugins a minute apart and nobody noticed.
File permissions. 644 for files, 755 for directories, 640 for your configuration file. A world-writable config is not a setting anyone chooses on purpose.
DISALLOW_FILE_EDIT. Removes the built-in theme and plugin editors, so nobody can write code into your site through a browser — including anyone using your credentials.
Unique passwords. They wouldn’t have stopped the first break-in here, but reuse is what turns one compromise into several. That’s the accurate reason to bother.
Two-factor authentication — everywhere, not just WordPress. Your hosting control panel outranks your site. Your domain registrar outranks your hosting. Your email is the recovery path for all of them. And Search Console, where an intruder who verifies ownership keeps reading your data after a full rebuild.
Cloudflare or a firewall. Useful. Sits in front of your website, and not in front of FTP.
Move your login to a different address
Change /wp-login.php to something only you know. A plugin will do it, or your host may offer it.
Be honest about what this does and doesn’t do. It is not a security control. Anyone holding valid credentials still gets in, and anyone who really wants your login page will find it. Treating it as protection is how people end up with a hidden login and nothing else.
What it genuinely buys you is quiet. Every WordPress site on the internet is hit continuously by automated login attempts — for many small sites, that traffic is the single largest category in the access log. Move the login and it stops, because the bots are hammering a path that no longer answers.
That matters more than it sounds, and Part 3 explains why: the hardest part of reading a log is separating signal from weather. Removing the loudest recurring noise in the file makes a real successful login visible instead of buried. You’re not hiding from an attacker; you’re clearing the view so you can see one.
Two practical notes: keep the new address somewhere you won’t lose it, and expect a couple of things to complain — some plugins and the mobile apps assume the default path.
The three holes in two-factor nobody mentions
Worth its own space, because “I enabled 2FA” is often where people stop.
FTP isn’t covered. No second factor exists for it. The fix isn’t 2FA, it’s removing FTP.
Application passwords bypass it by design. They exist so tools can authenticate without a second factor. Turn on 2FA and every application password already issued keeps working. Audit them per user — they’re at the bottom of each profile page and almost nobody knows they’re there.
Existing sessions predate it. Enabling 2FA doesn’t log anyone out, including someone already inside.
Which brings us to the one item almost every checklist omits
Rotate your security keys and salts — the eight constants in your configuration file.
They’re what sign your login cookies. Changing a password does not end an existing session; replacing those values ends every session on the site at once, including theirs. It’s a single edit, it’s free, and it’s the only thing that evicts someone who is already logged in.
But read that last sentence carefully, because it’s narrower than it looks. Salts evict a session. They do nothing about a foothold that never needed one — a shell that runs on request, a must-use plugin, a scheduled task, a rule in .htaccess. If someone can still write files to your server, rotating salts logs them out of a door they weren’t using.
From the case file. The salts on the compromised account were rotated at least five times during the fighting. Every rotation worked exactly as advertised, and none of it mattered — the operator’s access was file-level, not session-level, and a standalone payload doesn’t log in.
Rotating salts is step five, not step one. Its value is entirely conditional on the entry point being closed first. Done before that, it’s a rotation you’ll do again next week.
So: rotate them, and rotate them after you’ve shut the door — position 5 in the order at the end of this article, not before it. Then on a schedule afterwards, when there’s nothing left to race.
Replace, don’t inspect
Before the order below, the principle that makes it fast.
Anything with a known-good source doesn’t get audited. It gets deleted and replaced.
That means all of it — every plugin, every theme, WordPress core itself. Not the suspicious ones. Not the ones you don’t recognise. All of them.
Auditing a plugin means reading code you didn’t write, looking for a modification you can’t characterise, in a file that’s supposed to look exactly like that. It’s slow, it’s unreliable, and a single missed line puts you back to square one. Deleting the folder and reinstalling from the repository takes a minute and produces certainty.
Two things to get right:
Reinstall from the source, never from your own backup. Your backup may be older than the compromise, or it may not — and you’d be restoring the same uncertainty you’re trying to eliminate.
Delete the folders from the filesystem, not the dashboard — the dashboard is running their code while you use it. And note that deleting plugins does not touch mu-plugins or the drop-ins in wp-content; those are separate, invisible from that screen, and go first.
Settings live in the database, so most plugins pick their configuration straight back up when they’re reinstalled. Premium plugins need their licence keys to hand before you start.
Delete every theme you don’t use
Themes get overlooked because only one is active, and the rest feel harmless. They aren’t — an inactive theme is a folder full of PHP sitting on your server, reachable, and unexamined by anyone in years. It survives every “I reinstalled the theme” cleanup because nobody reinstalls the ones they’re not using.
Delete all of them except the one you’re running, plus one current default as a fallback in case your theme breaks. Every WordPress install accumulates two or three of these, and none of them are doing anything for you.
From the case file. This is worth doing after you’ve taken the site out of service, not while it’s live. Deleting unused themes was one of the actions in this case that appeared to provoke a response — the operators noticed removals and redeployed. Do it once nothing of theirs is running and there’s nothing left to react.
What this leaves behind is the real work. After replacement, the only things still requiring inspection are the ones with no clean source: your custom theme, anything bespoke, a nulled or abandoned plugin nobody publishes any more, wp-config.php, .htaccess, and the database.
That’s a list of six things instead of forty thousand files — and it’s short enough that “did I get them all?” becomes an answerable question rather than a hope.
The order that actually breaks the loop
If you were cleaning a site repeatedly and it kept coming back, sequence was probably the reason.
- Take the site out of service without destroying it. Static
index.html, renameindex.php, rename.htaccessand replace it with a minimal file that denies PHP execution site-wide. Renamingindex.phpalone is not enough — their files run standalone and don’t need WordPress. Denying PHP is what covers the payloads you haven’t found yet. Nothing is deleted; it all reverses in minutes. Verify by requesting a known spam URL — if content comes back, PHP is still executing somewhere. - Work outside the dashboard — FTP, SFTP or the host’s file manager. With step 1 done, there isn’t a working dashboard to be tempted by anyway.
- Remove must-use plugins and drop-ins.
- Close the entry point.
- Rotate the salts. Every session dies, including theirs.
- Delete application passwords.
- Then remove their accounts — at which point nothing remains to recreate them.
- Then deal with the files — by renaming, not deleting.
ws70.phpbecomesws70.php.20260114071843.txt: original name, timestamp, and an extension nothing executes. Leave it where you found it. You will be wrong about some of them — this case produced six findings that turned out to be the owner’s own software or his host — and a rename reverses in five seconds where a deletion doesn’t. Work one domain at a time, copying every file to your own machine as you go, in a dated folder per domain. If a finished site gets touched again while the others are still dirty, the vector is account-level. Actually delete later, once you’re sure. - Bring the application back — restore
index.phpand the.htaccessrules. - Then check whether the spam URLs still return content.
Doing this in the intuitive order — accounts first, entry point last — is a war against a tool that recreates in one query what you remove by hand.
What you can do yourself, and what you can’t
Yours, today, no technical skill required: verify Search Console on every domain. Open the log retention ticket. Enable 2FA on hosting, registrar and email. Audit application passwords. Delete plugins and themes you don’t use. And walk your web root asking what each non-web-page file knows.
Needs someone with access: disabling PHP execution by directory, auditing server configuration directives, rotating salts, and verifying that your backup folder actually refuses to serve.
And the honest closing note: standard hardening is a list of the most common doors. Yours wasn’t a common door. Do all of it anyway — then go and look at what your own software has been leaving on the server.
Back: How an SEO Spam Hack Works — and Why It Outlives the Cleanup
Hardening the wrong things is how a site gets locked down and reinfected anyway. Finding out which door was actually used comes first, and it is a different job from securing the ones that were not.
Start a Recovery ReviewPaid assessment · scope agreed up front
Next: this series covered how to find out what happened. The next one covers getting back what it cost — rankings, trust, and the parts of your search presence that don’t recover on their own.
Hub: Invasion Investigation Blog Series
ProVAE builds and recovers websites in Douglas, Georgia, serving South Georgia.
