
Last Update: November 2, 2026 10 min read
BY
eric
Last weekend a client asked us a simple question: "I think our website might have been hacked. Can you check?"
They had already looked. They had logged in to WordPress and seen nothing odd. An earlier round of checks with AI tools had come back clean too. The site loaded, the pages looked right, and Google was happy.
It had been compromised for almost two years. By the time we finished, we had pulled out 18 separate backdoors, decoded a password stealer, a credit card skimmer and a remote-control script, and identified 49 logins by the attacker from 21 countries. The attacker never once showed up in a way anyone would notice.
This is the story of how it hid, how it was found, and the one setting that quietly stopped the attacker in their tracks. I've kept the client anonymous. Everything else is real.
What "nothing looks wrong" actually looked like
The first clue was in the plugin list, and it was easy to miss. Among 54 plugins sat entries like these:
"By WordPress" and "Official WordPress plugin" are the oldest trick in the book, and they work, because nobody reads plugin metadata. They're also not how WordPress ships anything. Note the misspelling of Automattic as "Automatic".
But these were the visible ones. The really dangerous plugins weren't in the list at all.
The 18 backdoors
Over 18 months the attacker planted:
- 14 fake plugins. Eleven disguised as official WordPress plugins, three with random names (Achafofine, Achykax and Qyfutyhi) complete with invented authors and descriptions stitched together from random scraped text.
- 3 fake themes, all called "Twenty One", in folders named
twentystd,twentysevenandtwentyfour. WordPress has never shipped a theme with any of those names, but at a glance they sit comfortably next to the real Twenty Nineteen and Twenty Twenty. - 1 folder with no plugin header at all (
uhytyqob). WordPress doesn't list it, so it can't be seen or deleted from the admin screen. Its only job was to be there.
The very first one, a fake theme, arrived on 19 January 2024. About sixteen hours later, a file manager plugin on the site was used for the first time. The last new backdoor arrived in June 2025.
And the code wasn't stored where you'd expect code to be:
Where the malware hid
Why nobody saw it: hiding was the product
What impressed me, in the way a well-built safe impresses a locksmith, was how much of the code existed purely to avoid being noticed.
It removed itself from the plugin list. Five of the plugins hooked into WordPress's own plugin screen (the all_plugins filter and the pre_current_active_plugins action) and deleted their own row before the page was drawn. Active, running, invisible.
It hid from the people who owned the site. Every time an administrator or editor opened the dashboard, the malware quietly recorded a fingerprint of their IP address in the database. For the next 90 days, anyone visiting from that address got the clean version of everything. The owner, their staff, their web developer: all permanently looking at a site that had been sanitised for them.
It never repeated itself. No defacement, no pop-ups, no slowdowns, no deleted content. Vandals want to be seen. These people were running a business, and every extra month undetected was another month of income.
That's also why the earlier AI checks came back clean. They saw what the site was willing to show them: a normal front end, a normal-looking plugin list, a normal dashboard. You can't find malware by asking the infected system whether it is infected.
What the malware actually did
Decoding the hidden files gave us a full kit, from a family that labels itself SMILODON in its own code and server paths:
The toolkit
Two details stood out.
The skimmer never sent card data from the browser to a suspicious address. It posted to the shop's own checkout URL, and the PHP side of the malware forwarded it from the server. Browser-based security tools see nothing unusual leave the page.
The copies of those scripts inside the fake plugins were wired to fire only on a WooCommerce checkout page, and this business didn't run an online shop. For a day, that's where our analysis stopped: the visitor scripts looked dormant.
They weren't.
The second hiding place: 281 JavaScript files
While porting the site's theme to a new front end, I opened the theme's own global.js, the small script that runs the menu and search box on every page. Below the theme's last line, }( jQuery ) );, sat the remote-control loader.
A scan of the backup found the same block appended to 281 JavaScript files: the theme, gallery and slider plugins, the AMP plugin, Leadpages, even libraries nobody loads. It was byte-for-byte identical every time, glued onto the end of each file. Through the theme script alone it ran on every page of the site, for every first-time visitor who arrived from Google, WeChat or Facebook, and it was still active the day we found it. Its server was still answering too.
Because the injection was identical and always at the very end, removing it was precise: a one-off plugin cut each file at the marker and stopped. We tested it first on all 281 copies from the backup: every file came back byte-for-byte identical to its original content, and every script still parsed. On the live site it cleaned 273 files (the rest were inside plugins we had already deleted), a second pass found nothing, and the caches were cleared.
The breakthrough: the malware's own logbook
Here's the part I enjoyed most.
Remember that 90-day "hide from admins" feature? To do it, the malware saved an entry for every IP address that logged in to the dashboard: the address (hashed with MD5) and the time of the last visit. It kept that list to protect itself.
That list was also a record of every time the attacker logged in.
MD5 sounds like protection, but there are only about 4.3 billion possible IPv4 addresses. A short C program on a 16-core workstation tries all of them in a few minutes. We recovered 53 of the 54 entries, and the picture was immediate.
Every backdoor lines up with a logged-in administrator visit, to the minute. The attacker wasn't sneaking in through a hole in a plugin. They were signing in at the front door with a real admin password and uploading fake plugins like any site owner would. Given what the malware did, the most likely source of that password is the malware itself, collecting it at the login screen.
Forty-nine logins, twenty-one countries, never the same address twice
Between February 2024 and August 2025 the attacker logged in as an administrator 49 times. We recovered 48 of the addresses, and not one of them was a home connection in Australia.
They were almost all data centres, commercial VPNs and proxy networks: Hong Kong cloud hosts, a German node provider, a well-known consumer VPN's London exit, Russian and Polish hosting companies. Every address in the log is different.
To be precise about what that proves: the log stores one entry per address, so a reused address would simply update its old entry. What we can say is that every session we can see came from a fresh address, often in a different country from the last one. That isn't one person at a keyboard. It's a control panel managing a fleet of hacked sites through a rotating pool of proxies, and checking in on each one every few months to plant another backdoor in case the earlier ones are found.
The mix of malware families tells the same story. The "By WordPress" plugins, the random-name SMILODON plugins and the "Twenty One" themes are different kits. Access to compromised sites is commonly sold and resold, and this site may well have had more than one tenant.
The backdoors didn't need a login, which is exactly why there were so many of them. Two-factor locked the front door; the clean-up removed the windows they had propped open.
Why the early AI checks missed it, and what worked instead
I don't think the earlier AI tools were bad. They were pointed at the wrong evidence. An AI looking through the website or the admin dashboard sees exactly what the malware has decided to show, and this malware was built to show nothing.
Even with the files in hand, the payloads resisted the obvious approach. A standard JavaScript deobfuscator ran over the card skimmer and gave up on the strings: it left 8,372 encoded lookups in place because the decoder was a custom variant. The remote-control script was three layers deep, each layer a scrambled string that unscrambled the next, with the last step handing the result to eval.
What worked was treating it like evidence rather than a web page:
- Take a full backup first, download it, and verify it. Every later step works on the copy, not the live site.
- Read the files, not the dashboard. File timestamps in the backup showed each backdoor arriving as a single event, months apart, with nothing else on the site changing at the same moment.
- Compare against known-good. WordPress publishes checksums for its core files. All 2,913 matched, which told us the core was clean and the problem was entirely in plugins and themes.
- Decode without running. For each payload we ran only the decoding routine inside an empty sandbox (no network, no file system, no browser) and printed each stage instead of executing it. 8,372 out of 8,372 strings came back cleanly.
- Turn the attacker's tools against them. The cloaking list became the timeline. The C2 addresses and cookie names became indicators we could share.
AI did a lot of the work here, including most of the decoding and analysis in this post. The difference is that it had hands: a backup, a sandbox, a compiler and a terminal, not a browser tab. Give it evidence and it is fast and relentless. Give it a screenshot of an infected site and it will tell you the site looks fine, because it does.
If you run a WordPress site, do these this week
- Turn on two-factor authentication for every admin and editor. In this case it is the single thing that stopped the attacker.
- Change any password that has ever been typed into your WordPress login, and anywhere else it was reused, starting with email. If a stealer was running, it has them.
- Stop plugins and themes being uploaded from the dashboard.
define('DISALLOW_FILE_MODS', true);inwp-config.phpmeans a stolen admin password can't install a backdoor. Install plugins through your deployment process instead. - Don't trust the plugin list. Look at the
wp-content/pluginsandwp-content/themesfolders directly, and runwp core verify-checksumsandwp plugin verify-checksums --all. - Treat "inactive" as "still on the server". Delete what you don't use.
- Scan JavaScript as well as PHP.
grep -rl "zqxw" wp-contenttakes seconds and would have found this campaign's injected scripts. - Check your backups for things you don't recognise. A backup from a year ago is also a time machine for "when did this file appear?"
- Watch for a sudden security flag from your host, Cloudflare or Google. It is often the only symptom you'll get.
Longer term, we're moving this client to a setup where WordPress is only an editor, reachable from Australia by signed-in staff, and the public sees a separate site that runs no PHP at all. Staff keep writing the way they always have. Attackers lose the thing they need most: a door to knock on.
Indicators of compromise
If you look after WordPress sites, these are worth searching for.
The scariest thing about this attack wasn't the code. It was how ordinary the site looked the whole time. The encouraging thing is how little it took to stop it, and how much the attacker's own caution gave away once someone looked at the evidence instead of the website.
Previous Article
Next Article
Nov 02, 2026









Comments (0)
Leave a Comment