Using AI to fight AI hacks: finding the web shells every scanner called clean

Using AI to fight AI hacks: finding the web shells every scanner called clean

Here is a pattern we have run into repeatedly over the past few months while cleaning up compromised web servers. The site is clearly hacked — injected redirects, spam pages, an administrator account nobody created. It gets cleaned. Every malware scan comes back with no threats found. A commercial engine with tens of thousands of same-day signatures says clean. ClamAV says clean. And two weeks later the site is doing it again.

The backdoors were there the whole time. They were simply built to defeat the exact thing a signature engine does — and built the way software gets built now: in bulk, with a different disguise on every target, at a level of tedious variation no human attacker would sit down and hand-produce.

What finally worked was to stop asking “does this file match something we already know is bad?” and start asking two questions that scale the same way the attack does: what is different here that isn't different anywhere else, and what does this file actually do?

TL;DR: Modern web-server malware is mass-produced and randomized per target — a different filename on every site, function names assembled from string fragments, control flow shredded into hundreds of goto jumps, timestamps forged years into the past. Signature scanning cannot see it, and a clean scan is not an all-clear. Two techniques do work: differential integrity (compare every file against known-good copies of the same code, because malware is almost always a singleton) and behavioural heuristics (score files on structure, not on strings). Together they reduce a haystack to a short list a person can actually read.

What these compromises look like

The way in is almost never the CMS itself. It is add-on code — a plugin, a module, a page-builder extension. The flaw we have seen exploited most often this year is depressingly simple: an upload endpoint with no permission check, no session-token check, and no filtering of what came out of the uploaded archive. It unpacked a zip and copied the contents into a web-accessible folder. Anyone on the internet could write arbitrary files into the site, with no login at all. These endpoints get mass-scanned within days of a flaw becoming known, indiscriminately, across every site on the internet running that code.

What follows is a fairly ordinary escalation once you have file write: web shells planted in places nobody looks, an administrator account created quietly and left dormant for weeks, then used for a run of middle-of-the-night logins, a file-upload plugin installed through the admin interface to make re-entry easy, and site content edited to inject spam or redirects.

A variant worth knowing about involves no exploit at all. A cloaked gambling redirect gets injected into a template file through the CMS's own built-in file editor, using a valid administrator session. No vulnerability, no shell — just a legitimate login being used by the wrong person. That kind of injection is cloaked: it serves the redirect to search engines and ad crawlers while showing the normal page to you. It can sit for over a week and surface not through any scanner but through a Google Ads disapproval notice.

Two things it usually is not

  • Not a server or control-panel breach. In the cases we have worked, the operating system, the hosting control panel and the mail stack were never touched. Everything arrived through application code or a valid application login. That distinction matters, because it determines what you actually have to fix.
  • Not a stolen password. Existing administrator passwords frequently still verify afterwards, because the attacker created their own account rather than using anyone else's. Rotating credentials feels productive and closes nothing. The vendor patch is what closes it.

Why this looks machine-made

Read enough of these and you develop a feel for what a person types versus what a generator emits. This material reads like a generator. The identical payload turns up on unrelated sites under completely different filenames, and every technique below is applied consistently to every file — which costs an automated pipeline nothing and would cost a human hours:

  • Callables assembled from fragments. Instead of file_put_contents, the code builds the name as "fi"."le_p"."ut_c"… at runtime. Grepping for the function name finds nothing.
  • Control-flow flattening. Hundreds of goto statements shredding the logic into unreadable jump targets while behaving identically.
  • Evaluation of a decode chain, so the classic eval(base64_decode( pattern never appears literally anywhere in the file.
  • A custom stream wrapper, meaning the second-stage payload is never written to disk as a file at all.
  • Auth tokens passed in the URL fragment — the part after the #, which browsers never send to the server. The attacker's own requests therefore leave nothing identifying in any access log.
  • Forged timestamps. Files planted this year carrying modification dates of 2020 and 2023, so “show me what changed recently” skips straight past them.
  • Typosquatted neighbours. A malicious codemiirrr.php sitting inside the real codemirror plugin folder reads as correct to a tired eye at 11pm.
  • An .htaccess staged as a .php file, denying PHP execution to everything except a whitelist of the attacker's own filenames — malware locking the door behind itself. We wrote about that family of trick in the web shell that isn't a .php file.
  • A backdoor disguised as a database-upgrade file, sitting in the CMS's schema-update folder with a .sql extension and a filename dated a year into the future. Scan by extension and you never open it — yet it was byte-identical to a shell found elsewhere on the same server.

We cannot prove a language model wrote any of it, and we are not going to claim we can. What we can say is that it is indistinguishable from output that was generated rather than authored: uniform, variant-per-target, and produced at a consistency that only makes economic sense when the marginal cost of another variant is close to zero. That is the shift worth planning for. The defensive assumption that used to hold — “we will recognize it when we see it again” — quietly assumes you will ever see the same thing twice.


Why the scans keep coming back clean

Successive signature-based scans can report “no threats found” while a compromise is live and active on the same server. Both statements are accurate. They are simply not the same statement.

“No matches found” and “this server is clean” are different claims, and treating the first as proof of the second is how a compromise survives several cleanups in a row. A signature engine is a floor, not a verdict. It tells you that you do not have yesterday's malware. It can tell you nothing about a file that has never existed before in that exact form — which, when payloads are generated per target, is every file.


What actually finds them

None of this requires a security product. It requires being willing to process an enormous number of files carefully, which is precisely the sort of work worth handing to a machine.

1. Differential integrity — the highest-yield technique by a distance

A CMS installation is not a unique snowflake. The overwhelming majority of its files are identical to the same files in a clean copy of the same version, and identical to the same files on any other site running that version. So hash every file and compare it, by path, against known-good copies. Then flag two things:

  • a file that exists where no clean copy has a file at all;
  • a file whose contents are a minority variant at a path where the clean copies all agree.

Malware is nearly always a singleton. It does not have to match a signature to stand out — it only has to be the one file that nothing else has. This is the technique that surfaced backdoors four separate scans had walked straight past. It works with a single site and a freshly downloaded copy of the same CMS and extension versions.

2. Behavioural heuristics — score structure, not strings

The second sweep ignores content entirely and scores files on shape. Flag anything hitting two or more of: fifteen or more goto statements; two hundred or more hex or octal escape sequences; a quoted base64-looking literal over 1,500 characters; eval or assert applied to a variable; a callable built by string concatenation; Apache directives inside a .php file; an include or a file write driven directly by $_GET or $_POST.

Every one of those is a property of obfuscation itself, which is the one thing an attacker cannot remove — the payload has to hide, and hiding leaves a shape. Run over an entire server this reduces the problem by several orders of magnitude, down to a shortlist small enough to read by hand in an afternoon. In our experience the handful of false positives are all legitimate Unicode and encoding libraries, which are honestly full of escape sequences, and you learn to recognize them immediately.

3. Sweep the database as well as the disk

Extract every external hostname referenced by src= or href= in module content, article content and page-builder data. Whitelist the CDNs you expect and look hard at what is left. Injected spam always has to point somewhere, and a clean result here is itself worth having.

4. Verify the core against the publisher, not against yesterday

Rebuild confidence in the CMS itself by verifying every core file against the officially signed release package — checksums confirmed against the publisher's own signature endpoint, not against a copy you already have lying around, which may itself be the thing you are trying to validate. That is the difference between hoping the core is intact and knowing it.

Where the AI actually does the work

Not in delivering a magic verdict. In the parts that are pure volume and pure translation: writing the sweep scripts, holding an entire server's worth of file hashes in a single comparison, reading a goto-flattened payload and explaining in plain language what it beacons to and what it does with the response, and cross-referencing findings against access logs and admin action logs to reconstruct a timeline. The judgement calls stay human — including the decision to quarantine every suspect file with its hash recorded rather than delete it, so nothing is destroyed before it is understood.


The shape of a campaign like this

These are not smash-and-grab events. They unfold over months, and the gaps between stages are where the damage is done. A representative sequence:

  • Late April — first backdoors planted. Entry vector unrecoverable, because retained access logs did not reach back far enough. Assume this is normal: by the time you find something, the evidence of how it arrived is often already rotated away.
  • Early June — internet-wide scanning of a newly disclosed extension vulnerability begins. This is indiscriminate and automated; nobody is targeting you personally.
  • Mid to late June — a redirect injector and additional shells planted through that endpoint, plus repeated probing over a two-day window.
  • Late July — further shells dropped into vendor and plugin directories within a five-minute window, and a dormant administrator account finally used for the first time. Logins continue nightly for several days.
  • Late July — the valid-session incident: a cloaked redirect injected through the CMS's own file editor. No exploit involved.
  • Early August — the redirect surfaces via a Google Ads disapproval, more than a week after injection. Rogue accounts and injected content removed, and the vulnerable extension updated to the patched release everywhere.
  • Days later — a full differential-integrity and behavioural sweep finds the older, quieter backdoor sets that predated the whole thing, core integrity is verified against the signed release, and developer mirrors are cleaned so that a routine deployment cannot re-infect production.

Note the last point. If you keep local or staging copies of a site, they hold the payloads too. Clean production, deploy from an uncleaned mirror, and you have put it all straight back.


What we do about it

  • Patch the actual hole. Add-on code gets updated promptly, because that is the front door. Credential rotation is not a substitute and never closes this.
  • Continuous integrity monitoring on template and administrator files, running on a short cycle. An injection that would otherwise sit unnoticed for a week and a half gets caught in minutes.
  • Differential baselining as a scheduled sweep rather than as incident response, so a file that exists nowhere else is caught while it is new.
  • Core verification against publisher signatures as part of that sweep.
  • Quarantine, not delete. Every removed file is preserved with its hash, so the analysis can be checked and rechecked rather than taken on faith.
  • Developer copies treated as production. Anywhere the code lives is somewhere the malware lives.

What to do if you run your own sites

  • Update extensions like they are the front door, because they are. The CMS core is rarely the way in; add-on code almost always is.
  • Do not accept a clean scan as an all-clear. Ask what the tool actually checked. Signature engines answer a much narrower question than the one you are asking.
  • Compare, do not just scan. One freshly downloaded, known-good copy of your CMS and extensions gives you a diff, and a diff finds things no signature ever will.
  • Turn on administrator action logging with source IP addresses today, before you need it. It costs nothing, and it is unrecoverable after the fact — without it, a rogue login has no source address attached and an investigation gets far harder than it needs to be.
  • Require multi-factor authentication on every administrator account. A meaningful share of what we clean up was reached with a valid login rather than an exploit, and MFA is the control that stops exactly that.
  • Distrust file timestamps during an investigation. They are trivially forged, and they are forged routinely.
  • Look outside .php. One of these backdoors wore a .sql extension. Scan by content, never by extension.
  • Keep your access logs longer. The most common reason we cannot tell a client how an attacker got in is that the logs had already rolled off.

The takeaway

The economics of this have changed, and the defensive posture has to change with them. When producing a fresh, uniquely disguised variant for every target is essentially free, matching against known-bad stops being a strategy and becomes a floor. What still works is comparison and behaviour — the two questions that do not care whether anyone has ever seen this exact file before. Both are grindingly tedious at scale, which is precisely why it is worth putting the same class of automation to work on the defensive side of the problem.

If your site has been behaving oddly while every scan comes back clean, that is a strong hint you are looking in the wrong place. And if you host somewhere that has never once shown you an integrity report, that gap is worth closing before it gets closed for you. It is the kind of check we run as a matter of course on the sites we host and manage, and we are glad to take a look at one we do not.


This article describes techniques and patterns in general terms. It contains no client names, domains, indicators or other details that could identify any specific site or system.