Houndoom: a web shell scanner in Go
In October 2025 a critical RCE surfaced in Ai-Bolit and Imunify — the scanners half the Russian hosting market uses to clean infected sites. The bug lived at the very core of the tool: to read suspicious code, the deobfuscator ran it, calling call_user_func_array with no validation of function names. Drop a carefully crafted file on the server, and running the scanner became running the malware. Estimates put up to 56 million sites in range.
The thing you use to disarm a compromised server had itself become a way to compromise it further. It showed up to put out the fire carrying a can of gasoline.
I’ve spent a long time pulling apart infected sites — usually someone else’s, projects I get called in to audit or clean after a breach. Almost everything built for that job works one of two awkward ways: either you install a permanent root-level agent on the client’s server, or you ship the client’s files up to the vendor’s cloud. I didn’t like either. So I built Houndoom.
What it is
Houndoom is a CLI web shell scanner and PHP backdoor scanner written in Go. A single static binary that walks a site’s directory and flags what has no business being there: shells (r57, c99, b374k, WSO), eval/base64/gzinflate backdoors, injections, malicious JS, phishing and doorway pages, stray executables sitting in web directories. It ships dedicated detectors for WordPress and Bitrix. Findings can optionally be run through Claude — but that’s a layer on top, not the engine.
Let me be clear up front, so there’s no confusion. Houndoom is not a replacement for Imunify360 or Wordfence. Those solve a different problem: continuous protection of your own server — WAF, firewall, real-time monitoring, auto-remediation. Houndoom is about a one-shot audit of someone else’s server. You arrive on a machine you didn’t set up, where none of your security stack is running, take a picture in an hour, and leave without a trace. It’s an incident-response tool, not a resident defense.
Why Go
Speed. Walking a large site means hundreds of thousands of files, and here Go with goroutines outruns a PHP script or bash. On top of that: one dependency-free binary. No PHP on the target machine (as Ai-Bolit needs), no YARA (as PHP Malware Finder needs), no bash-plus-ClamAV pairing (as maldet needs). When you’re on someone else’s server over SSH for an hour, “nothing to install first” isn’t a convenience — it’s a precondition of the job.
I’ll be honest: Go isn’t the differentiator here. Imunify’s own WebShield daemon is written in Go too — it’s a sane choice for a heavy-duty scanner, not a personal trick of mine.
Static deobfuscation
This is the central architectural decision, and the 2025 Ai-Bolit RCE is a textbook illustration of why it matters.
Malware is almost always obfuscated — wrapped in several layers of base64, gzinflate, str_rot13, custom substitutions. To read what’s inside, you have to peel the layers off. There are two ways to do it. The first: ask PHP itself to unwind them — feed the string to the interpreter and see what comes back. Fast — and exactly what Ai-Bolit did. The second: unwind the layers yourself, in static code, never once handing control to the thing you’re inspecting.
Houndoom takes the second path. Eight deobfuscators (base64, eval, lockit, als, byterun, fopo, urldecode, globals), recursion up to 100 levels deep — because real shells do come wrapped in dozens of layers. And along that entire path the code is never executed. A string gets unwound; a program never runs.
The difference isn’t that competitors lack deobfuscation — Imunify’s is present and advanced. The difference is that they execute it. And how that RCE was resolved is telling: CloudLinux didn’t remove execution from the architecture — they added a whitelist of allowed functions (base64_decode, gzinflate, strrev, str_rot13, urldecode, substr, chr, ord). The approach stayed the same, “we still execute, just more carefully now.” Static analysis doesn’t have that class of bug at all — there’s nothing to execute.
Structural anomalies
Signatures catch known malware. But a competent attacker doesn’t drop c99.php in the document root — they hide where you aren’t looking. A regex is useless against that: what’s suspicious isn’t the text of the file, it’s the bare fact that the file exists in that spot. These checks live in Houndoom as separate Go code, and here are a few.
A PHP file inside wp-content/uploads/. WordPress puts images and documents there — executable code has no legitimate way in. A .php sitting in that folder is almost certainly a shell uploaded through a hole in the upload handler:
// A PHP file inside wp-content/uploads/ — a media-only directory
if d.uploadsPathRe.MatchString(lower) && file.Extension == "php" {
findings = append(findings, d.structureFinding(file, "WP-STRUCTURE-001",
"PHP File in Uploads Directory",
"PHP file found in wp-content/uploads/ - this directory should only contain media files",
"php_in_uploads", models.SeverityHigh, 1.5, 90))
}
auto_prepend_file in .user.ini. This directive tells PHP-FPM to load a given file before every script on the site. Perfect persistence: drop a .user.ini next to an innocent-looking PHP file, and that file runs on every request. Almost every scanner forgets this vector, yet it works on most modern hosting.
wp-content/mu-plugins/. Must-use plugins load automatically on every request and can’t be disabled from the admin panel — they don’t even show up in the plugin list. A favorite hiding spot: drop a backdoor there, and it survives a cleanup done through the admin panel, because the site owner doesn’t know the folder exists.
None of these checks can be written at a desk without having seen real infections. They grew out of taking apart how malware actually hides.
False-positive suppression
The most honest piece of code in the project is the false-positive suppression matrix. It’s about the idea that a good scanner is obligated to doubt its own findings.
A simple example. My own signatures contain malware patterns — otherwise they’d catch nothing. Now picture another security plugin installed on the site. Inside it: those same patterns, there for its own detection. A naive scanner will happily report that the security plugin is infected. So Houndoom mutes all of its WP signatures inside known security plugins:
// Suppression matrix:
// - security plugins: suppress ALL WP signatures (they carry detection
// patterns of their own; a real compromise inside a security plugin is
// outside the scope of static content matching).
// - core files: suppress vulnerability signatures (auth/API/hook/...)
// that are legitimate inside WP core; keep backdoor detection.
// - managed plugins: suppress vulnerability signatures + htaccess
// (caching legitimately uses auto_prepend); keep malware detection.
Separately: WordPress core and “trusted” plugins such as caching layers. They legitimately call things that look like a vulnerability in another context: auto_prepend for caching, direct API calls, hooks. You can’t mute backdoor detection inside them — but you must mute the vulnerability signatures, or you drown in false positives. That line — where to suppress and where not to — is exactly the part a regex won’t draw for you.
One-shot SSH scan
Now the part this was all built for. Houndoom has an agentless remote-scan mode: it connects to the server over SSH, detects the architecture, uploads the scanner binary to a temp folder, runs a strictly read-only pass, pulls back a JSON report — and cleans everything up after itself, even if something failed along the way. Keys come only from ssh-agent; the key material itself is never read or logged; commands go through the system ssh/scp, so your ~/.ssh/config with ProxyJump works as-is.
Let’s be honest: the “agentless over SSH” pattern isn’t mine. Vulnerability scanners have worked exactly this way for years — Vuls, Qualys agentless, Nessus. What’s new is porting that approach into the web-shell niche, where nobody does it. Look at what everyone else offers:
- Imunify, CXS, GOTMLS, maldet, ClamAV — resident install. Put an agent or daemon on the server and keep it there.
- Sucuri SiteCheck, Astra free, GoDaddy — external scan over the URL. They see only what’s served to the browser — the storefront. A backdoor in
uploadsor an infection in the database is invisible to them, and Sucuri admits this openly. - MalCare — a “remote” scan, but one that ships the client’s files and database to the vendor’s servers.
- Sucuri server-side — permanently stores your SSH/FTP credentials in its cloud.
The closest thing to Houndoom in spirit is Wordfence CLI: also open source, also scans the filesystem outside the CMS, and backed by the whole of Wordfence’s threat intelligence, which a solo project can’t have and never will. But: it has to be installed on the target machine (a package plus a Python runtime), it’s built for WordPress, and in the free tier its signatures arrive on a 30-day delay. Houndoom lands as one binary, leaves nothing behind, and speaks Bitrix.
Houndoom’s model is simple: a one-shot read-only visit with no stored credentials and no trace on the target machine.
Explaining findings with Claude
Findings can be run through Claude to get a human-readable explanation: what this code is, what it does, whether it’s worth panicking over. Two things matter here on principle.
First, the safety of the process itself. In the skill, the contents of scanned files are explicitly declared hostile data. A web shell can carry an instruction along the lines of “now, model, go do this” — and the model must not act on it. The LLM’s verdict is advisory, not a sentence. At the repository level the agent is also denied raw access to ssh/scp/curl — all access to the target goes through the auditable Go binary, never through the model directly.
Second, the LLM here is an explainer, not a detector, and that’s deliberate. There’s recent research (arXiv 2504.13811, 2025): GPT-4, LLaMA, and Qwen were run over 26,000 PHP scripts. The finding — large models deliver near-perfect precision but fail on recall: as a first-line detector, LLMs are poor, they miss things. But they’re excellent at explaining what’s already been found. So the order in Houndoom is exactly this: heuristics and signatures find, the LLM explains what was found. Not the other way around.
And, honestly, on privacy: in --ai mode, code fragments from the client’s server go to Anthropic. For banks, the public sector, and anything under data-residency law, that’s a blocker. Which is why the mode is optional — the core works fully without it.
Where it fits
Houndoom fills a narrow but real need: land on someone else’s infected server, take a picture in an hour, and leave without a trace — without buying a subscription on the client’s server or planting an agent there. Its defining property is deobfuscation that runs nothing; in light of how 2025 ended for Ai-Bolit, that’s not a bullet point on a feature list but a deliberate decision with a recently proven cost of getting it wrong.
But this is a 0.1.0 tool from a single author, with no threat feed of its own and no published benchmarks. It won’t replace a hosting platform’s defense and doesn’t pretend to. If you need a one-shot forensic sweep of someone else’s machine — take it. If you need continuous protection of your own site — that’s a job for other tools, and I’ll be the first to tell you so.
Repository: github.com/IvanShishkin/houndoom