All work SECURITY / INCIDENT RESPONSE shipped

31  /  SECURITY / INCIDENT RESPONSE

Children's coding school

Visitors got a success code and a blank page, twelve days after a cleanup that missed why.

10 – 11 September 2026WordPressWP-CLIPHP 8.2MariaDBBashSSH (key auth)mysqldump

The challenge

The site was serving a blank page to every visitor. The homepage, the feeds and the REST API all returned HTTP 200 with a zero-byte body, while the login page worked perfectly. Any URL ending in .xml returned a sitemap of 1,001 fabricated spam URLs, regenerated on every request, and visitors arriving from Google or Bing were redirected to a spam store. The host's own malware scanner had run twelve days earlier and the problem had come straight back, which told me the previous cleanup had removed symptoms and left the persistence in place.

What I built

I ran recon through a browser rather than curl, because the CDN serves a bot challenge to command-line clients. I fanned six research agents out against the confirmed indicators off that recon alone, and they kept running through the access handover and the full files-and-database backup that followed. The injection point was index.php: a loader hiding its payload in invisible Unicode characters, which had also deleted WordPress's own bootstrap line and left the frontend with nothing to load. I eradicated files first, then the database, quarantining every sample as evidence rather than deleting it: the loader, a passwordless login backdoor, fourteen attacker accounts, a cloaking plugin dressed as a security tool, and a fake page-builder add-on that pulled its JavaScript from a public blockchain contract, so there was no hostname to blocklist.

How it works

THE CLOAK DEFAULT QUERY WITH THE FLAG Fake security pluginrecreates a hidden admin The filter hides itlist, REST API and counts The user querythe account is not there Declared cleanthe first pass saw nothing wp --skip-pluginsbypasses the filter The account appearsvisible only this way The difference is one flag every user query in the investigation was run with it
The mechanism, drawn from the build.

The detail

01

Invisible payload

index.php looked almost normal in an editor. The code was encoded into Unicode variation selectors, invisible codepoints interleaved through the file, so to decode it I walked the source codepoint by codepoint and mapped each selector back to a byte. The decoded loader wrote itself to a temp file, included it, then removed it on shutdown, leaving nothing on disk to find.

02

Skip the plugins

The real persistence was a fake security plugin that recreated a hidden administrator on every admin request, then filtered that account out of the user list, the REST API and the user counts alike. It hid itself from the plugin screen and threw a fake deactivation error to block its own removal. I only ever saw it because I ran every user query in the investigation with wp --skip-plugins, which bypasses the filter, and that single flag is the difference between finding it and declaring the site clean.

03

Plugin recovery

The attacker had deleted every plugin, page builder included, so the restored site rendered as raw shortcodes. The only pre-compromise copy was an April migration archive, which the owner's own hosting restore then deleted; it survived solely inside the pre-clean tar I had taken thirty minutes earlier. Reverse-engineering that container format by inspection took me two wrong guesses about the field padding before my parser was right, and it pulled 20,509 files across 39 plugins, every one of which I scanned against the full indicator set before anything went near the live site.

04

Staged re-clean

Every host backup predating the cleanup still contained the backdoor, so I staged an idempotent re-clean script on the server before the owner restored anything. His restore deleted index.php outright and brought the backdoor back. My script caught it, and the reintroduced files were what finally surfaced the cloaking plugin my first pass had never seen.

05

Keys, not passwords

The owner opened by pasting his WordPress username and password into chat. I never used them. I generated a keypair up front instead, so the ask was a single paste into the hosting panel, and I ran the whole job with full file access and no password typed into a form.

The outcome

The site came back during the session and then verified clean: core passes checksum verification, a site-wide signature sweep returns nothing, 33 plugins active, and a frontend that renders with zero unparsed shortcodes where it had been serving 184. A nonsense .xml path now returns 404 instead of generating 1,001 spam URLs. I scanned every site on the hosting account; the loader existed in exactly one file, and I left four restore points behind, files and database, before and after the clean. The remediation itself ran just over an hour. I handed the owner a short list of follow-up hardening items with it.

The client is not named, and the remaining hardening steps are deliberately not itemised, because the site is live and reachable.