What visitors were seeing
The first sign of trouble wasn't spam or a warning from Google. Visitors to a long-running WordPress review site started getting what looked like a Cloudflare "verify you are human" page, except there was no checkbox. It told them to press Win+R, paste in a command, and hit Enter. Apparently, proving you're human now requires Windows and a complete lack of suspicion.
That's a ClickFix attack, and it goes after the people reading the site, not the site itself. Anyone who followed those steps would have run the attacker's command on their own computer.
The site is hosted on Kinsta. The client needed me to find everything the attacker had left behind, get rid of it without losing any real content, and put the site back online in a state they could trust.
What I found
The fake check page was just the part people could see. Behind it were:
- Five must-use plugins dressed up as caching and "site guard" helpers. MU plugins load on every request and don't show up in the regular plugin list, so they're a great place to hide. They did guard something. It just wasn't the site.
- Four plugin folders on disk that never appeared in wp-admin.
- Small backdoors that took an encoded command from a POST field and ran it.
- A spam API in the site root that a remote script could call to publish posts. Even spammers have a content workflow now.
- 1,224 casino and betting posts in several languages, all published through that API. Say what you want about the attacker, but they took internationalization seriously.
- Settings and scheduled jobs that kept everything running.
- An administrator account the attacker was using, plus Application Passwords, which give API access without going through the normal login.
The code injecting the fake page was in the child theme's functions.php, tacked
on below 25 lines of legitimate code. A plugin vulnerability had nothing to do with
it. There was no zero-day and no hooded genius typing green text in a dark room.
According to the logs, the attack started a month earlier with a valid
administrator login. Hacking gets a lot easier when you have the password.
Doing things in the right order
Most of the work was WP-CLI over SSH, along with Kinsta's dashboard and logs. The tools were nothing special. What mattered was the order I did things in.
First, I cut off access. Before I changed a single file, I put the site behind password protection and blocked the attacker's IP addresses. I had to turn off the CDN and edge caching first, because Kinsta won't password-protect a site while a cached copy can still be served. Once that was done, visitors stopped getting the fake page, and the attacker got to find out what being locked out of somebody else's site feels like.
Next, I saved the evidence. The malicious files and a full database dump went into a private folder outside the web root. Whenever a question came up later, like when this all started or whether a certain post was ours or theirs, that archive had the answer.
Then I made sure I could undo everything, which I learned the hard way. On my first pass, I permanently deleted all 1,224 spam posts. The client had expected them to go to the Trash so anything real that got swept up could be restored, and they were right to expect that. I restored all 1,224 from the database dump and moved them to the Trash instead. After that, nothing got removed for good until the client signed off on it.
I also reinstalled instead of trying to repair anything. WordPress core passed checksum verification. Every free plugin got replaced with a clean copy from wordpress.org. The premium plugins came from the client's other sites, which weren't affected, and never from the compromised backup, because restoring the attacker's handiwork seemed counterproductive. I added those one at a time and checked the front end after each one.
Hardening came last:
- new salts and a new database password
- file editing disabled in
wp-config.php - Application Passwords turned off with a two-line MU plugin (it turns out they can be used for good, too)
- Wordfence for scanning and brute-force protection
- a review of every user account, including the attacker's, which has been let go without severance
The 27 minutes
Before I took the site public again, I scheduled a log check for one hour later. I figured it would be a formality.
The access log showed a successful administrator login 27 minutes after the site went public, and another casino post went up seconds later. Whatever else you can say about these people, they're punctual and they don't miss a content deadline.
The account belonged to the client. It was the one administrator account I hadn't changed the password on yet, and whoever got in a month earlier still had that password.
Fixing it took a few minutes:
- I moved the post to the Trash.
- I set a new password on that account and ended all of its sessions.
- I gave every other account a new random password too.
Nothing else had been touched. The next day's logs showed bots still trying the old login and hitting backdoors that were already gone, and every one of those requests either failed or got a 404. It's the nicest rejection letter I've ever sent.
So my cleanup was thorough, but my checklist for reopening the site wasn't.
Cleaning up Google
Search Console showed no manual action and no security issues. Google hadn't flagged the spam at all. It had just indexed it, which is about as close as spam gets to a five-star review. I matched every URL Google listed as indexed or ranking against the trashed spam posts and filed 440 removal requests. Those pages return a 404 now, so the removals won't need to be renewed when they expire. I also confirmed that the sitemap listed only real content, 242 URLs in all, and resubmitted it.
What I'd do differently
I'd treat changing every password as something that has to be finished before the site comes back up, not something that happens alongside it. I changed the passwords on the accounts I could see being abused and left one for later, and later turned out to be 27 minutes. When an attacker gets in with a real password, you have to assume they have every password they could have seen. Change all of them before the site is reachable again.
I'd also schedule that post-launch log check every single time. That check is the only reason this is a short section in a write-up and not a site that got quietly reinfected.
And to whoever is still calling those backdoors: they're gone, and the 404 is all yours.
