OtterCookie Modular JavaScript Implant

WindowsmacOSLinux

TTP Hunt medium Contagious InterviewOtterCookieBeaverTail

Command-and-control addresses, URI paths and on-disk keylog and screenshot artifacts for the four-module OtterCookie implant Talos analysed alongside BeaverTail.

What this hunt looks for

OtterCookie began as a complement to BeaverTail and has converged with it. Talos's October 2025 analysis breaks it into four modules: a socket.io remote shell that detects virtual machines and reads the clipboard through native commands; a file uploader; a Chrome and Brave extension stealer; and a keylogger and screenshotter that writes into a windows-cache folder.

The last module leaves files on disk at a predictable relative path, which is the most useful thing here for a defender. NTT Security Japan's fourth-version report adds command-and-control domains imitating Web3 API providers, a stealer writing decrypted Chrome credentials to 1.db in the local application-data folder, and native clipboard commands replacing clipboardy.

Technique
Modular JavaScript implant providing remote shell access (MITRE ATT&CK T1059.007), clipboard capture (T1115), keylogging (T1056.001), screen capture (T1113) and credential and wallet theft from browsers (T1555.003), delivered through compromised packages (T1195.002).
Data sources
EDR network telemetry, EDR file telemetry, EDR process telemetry, Proxy logs, Package inventory
Last updated
2026-10-04

Indicators

The atomic values this hunt matches on, listed separately from the queries so they can be lifted into tooling that speaks none of the query languages below. Every value here is reported by one of the sources cited at the foot of this page.

TypeValueNote
IP 23.227.202.244 OtterCookie command-and-control infrastructure published by Talos (2025-10-16).
IP 172.86.88.188 OtterCookie command-and-control infrastructure published by Talos (2025-10-16).
IP 138.201.50.5 OtterCookie command-and-control infrastructure published by Talos (2025-10-16).
IP 135.181.123.177 OtterCookie command-and-control infrastructure published by Talos (2025-10-16).
IP 144.172.96.35 OtterCookie command-and-control infrastructure published by Talos (2025-10-16).
IP 144.172.112.50 OtterCookie command-and-control infrastructure published by Talos (2025-10-16).
IP 172.86.73.46 OtterCookie command-and-control infrastructure published by Talos (2025-10-16).
IP 172.86.113.12 OtterCookie command-and-control infrastructure published by Talos (2025-10-16).
Port 1224, 1418, 1476, 1478, 5961 Observed C2 ports; 1418 carries the socket.io remote shell, 1476 carries uploads, 1478 carries the keylogging data and screenshots, and 5961 carries uploads on a second host (Talos).
URI path /api/service/makelog Reported across four separate C2 hosts (Talos).
URI path /client/14/144, /payload/14/144, /brow/14/144 C2 directory layout (Talos).
File path windows-cache/1.tmp Keylog file written by the keylogging module (Talos).
File path windows-cache/2.jpeg Screenshot file written every four seconds (Talos).
File path test/fixtures/eval/index.js, test/fixtures/eval/node_modules/file15.js, test.list Payload staging paths inside the delivered package; test.list holds a 100KB-plus obfuscated payload (Talos).
npm package node-nvm-ssh Malicious npm package used for delivery (Talos).
Repository https://bitbucket.org/dev-chess/chess-frontend.git Delivery repository (Talos).
Node dependency node-global-key-listener, screenshot-desktop, sharp Legitimate packages the keylogging and screenshotting module depends on; their appearance in an interview assignment's dependency tree is the signal (Talos).
Domain alchemy-api-v3[.]cloud, chainlink-api-v3[.]cloud, moralis-api-v3[.]cloud OtterCookie v4 command-and-control domains named to resemble legitimate Web3 API providers (NTT Security Japan).
File path %LOCALAPPDATA%\1.db Where one of the two v4 stealer modules writes decrypted Chrome credentials (NTT Security Japan).
Behaviour Clipboard theft through native OS commands rather than the clipboardy package v3 used clipboardy; v4 dropped it, so its absence from a dependency tree no longer rules out clipboard theft. v4 also checks for a virtual machine before running (NTT Security Japan).

Logic

Every query below implements these steps, so the same hunt returns the same result in each product.

  1. Collect: file writes, process starts and network connections on Windows, macOS and Linux endpoints; npm packages through osquery
  2. Match: a file in a windows-cache or test/fixtures/eval folder, or 1.tmp, 2.jpeg or AppData\Local\1.db written by node or electron
  3. Match: or node or electron reads the clipboard natively (Get-Clipboard, pbpaste, xclip)
  4. Match: or a command line names node-nvm-ssh or the dev-chess Bitbucket repository
  5. Match: or a connection to a v4 C2 domain or URL path, a Talos C2 address, or a C2 port on a public address (addresses and ports rotate)
  6. Report: devices with the signal kinds seen, first and last time, newest first

Queries

Defender / Sentinel KQL

Written for this site, from indicators reported by Cisco Talos, NTT Security Japan

One row per device with the signal kinds seen, so a device with a file hit and a clipboard or network hit is the one to isolate first.

// OtterCookie modules: file, process and network signals over one window, reported per device
let C2Domains = dynamic(["alchemy-api-v3.cloud", "chainlink-api-v3.cloud", "moralis-api-v3.cloud"]);
let C2Paths = dynamic(["/api/service/makelog", "/client/14/144", "/payload/14/144", "/brow/14/144"]);
union
  (DeviceFileEvents
   | where Timestamp > ago(30d)
   | where FolderPath contains "windows-cache"
        or FolderPath contains "test/fixtures/eval/"
        or FolderPath contains @"test\fixtures\eval\"
        or (InitiatingProcessFileName in~ ("node.exe", "node", "electron.exe", "electron") and (FolderPath endswith @"\1.tmp" or FolderPath endswith "/1.tmp" or FolderPath endswith @"\2.jpeg" or FolderPath endswith "/2.jpeg" or FolderPath endswith @"\AppData\Local\1.db"))
        // optional: test.list is a generic file name, the 100 KB size test is not applied
        or FileName =~ "test.list"
   | project Timestamp, DeviceName, Signal = "file", Detail = FolderPath),
  (DeviceProcessEvents
   | where Timestamp > ago(30d)
   | where (InitiatingProcessFileName in~ ("node.exe", "node", "electron.exe", "electron") and (ProcessCommandLine contains "Get-Clipboard" or FileName in~ ("pbpaste", "xclip")))
        or ProcessCommandLine contains "node-nvm-ssh"
        or ProcessCommandLine contains "bitbucket.org/dev-chess/chess-frontend"
   | project Timestamp, DeviceName, Signal = "process", Detail = ProcessCommandLine),
  (DeviceNetworkEvents
   | where Timestamp > ago(30d)
   | where RemoteUrl has_any (C2Domains)
        or RemoteUrl has_any (C2Paths)
        // optional: addresses dated 2025-10, they rotate
        or RemoteIP in ("23.227.202.244", "172.86.88.188", "138.201.50.5", "135.181.123.177", "144.172.96.35", "144.172.112.50", "172.86.73.46", "172.86.113.12")
        // optional: these ports are shared with ordinary services
        or (RemotePort in (1224, 1418, 1476, 1478, 5961) and not(ipv4_is_private(RemoteIP)))
   | project Timestamp, DeviceName, Signal = "network", Detail = strcat(RemoteUrl, " ", RemoteIP, ":", tostring(RemotePort)))
| summarize Signals = make_set(Signal), Events = count(), Examples = make_set(Detail, 5), FirstSeen = min(Timestamp), LastSeen = max(Timestamp) by DeviceName
| sort by LastSeen desc
Lower-fidelity parts (3)
  • the `test.list` file name — a generic name, and the 100 KB size test Talos describes is not applied. Remove: delete the `// optional: test.list` line and the `or FileName =~ "test.list"` line below it.
  • the eight Talos command-and-control addresses — published 2025-10 and likely rotated since. Remove: delete the `// optional: addresses` line and the `or RemoteIP in (...)` line below it.
  • the C2 ports 1224, 1418, 1476, 1478 and 5961 on a public address — ordinary services use these ports, so it matches on port alone. Remove: delete the `// optional: these ports` line and the `or (RemotePort in (...) ...)` line below it.

Splunk SPL

Written for this site, from indicators reported by Cisco Talos, NTT Security Japan

Raw EDR events with the Endpoint fields, one row per host; the URL-path match needs a source that records the request path.

```index=endpoint is a placeholder for EDR process, file and network events carrying the Endpoint data model fields```
index=endpoint earliest=-30d
| eval proc=lower(process_name), parent=lower(parent_process_name), cmd=lower(process), path=replace(lower(file_path), "\\\\", "/"), web=lower(coalesce(url, query))
| eval signal=case(
    match(path, "windows-cache|test/fixtures/eval/"), "file",
    match(proc, "^(node|electron)(\\.exe)?$") AND match(path, "/(1\\.tmp|2\\.jpeg)$|/appdata/local/1\\.db$"), "file",
    ```optional: test.list is a generic file name```
    match(path, "/test\\.list$"), "file",
    match(parent, "^(node|electron)(\\.exe)?$") AND (match(cmd, "get-clipboard") OR match(proc, "^(pbpaste|xclip)$")), "process",
    match(cmd, "node-nvm-ssh|bitbucket\\.org/dev-chess/chess-frontend"), "process",
    match(web, "(alchemy|chainlink|moralis)-api-v3\\.cloud|/api/service/makelog|/(client|payload|brow)/14/144"), "network",
    ```optional: addresses dated 2025-10, they rotate```
    in(dest_ip, "23.227.202.244", "172.86.88.188", "138.201.50.5", "135.181.123.177", "144.172.96.35", "144.172.112.50", "172.86.73.46", "172.86.113.12"), "network",
    ```optional: these ports are shared with ordinary services```
    in(tostring(dest_port), "1224", "1418", "1476", "1478", "5961") AND NOT (cidrmatch("10.0.0.0/8", dest_ip) OR cidrmatch("172.16.0.0/12", dest_ip) OR cidrmatch("192.168.0.0/16", dest_ip) OR cidrmatch("127.0.0.0/8", dest_ip)), "network",
    true(), null())
| where isnotnull(signal)
| eval detail=coalesce(path, cmd, web)
| stats count AS events values(signal) AS signals values(detail) AS examples min(_time) AS first_seen max(_time) AS last_seen BY host
| convert ctime(first_seen) ctime(last_seen)
| sort - last_seen
Lower-fidelity parts (3)
  • the `test.list` file name — a generic name, and the 100 KB size test Talos describes is not applied. Remove: delete the ```optional: test.list``` comment and the `match(path, "/test\\.list$")` line below it.
  • the eight Talos command-and-control addresses — published 2025-10 and likely rotated since. Remove: delete the ```optional: addresses``` comment and the `in(dest_ip, ...)` line below it.
  • the C2 ports 1224, 1418, 1476, 1478 and 5961 on a public address — ordinary services use these ports, so it matches on port alone. Remove: delete the ```optional: these ports``` comment and the `in(tostring(dest_port), ...)` line below it.

Sigma

Written for this site, from indicators reported by Cisco Talos, NTT Security Japan

One rule per telemetry type because a Sigma rule binds to one logsource, and the URL-path match has no endpoint event so it is left to KQL, SPL and ES|QL.

title: OtterCookie Keylog, Screenshot and Staging Files Written
id: f1a251cc-22f6-5f5c-b05a-6cd45f7030db
status: experimental
description: >
  Files OtterCookie leaves on disk: the windows-cache folder of the keylogging
  and screenshot module, 1.tmp and 2.jpeg or the v4 1.db written by node or
  electron, and the delivery staging paths inside the take-home package.
references:
  - https://blog.talosintelligence.com/beavertail-and-ottercookie/
  - https://jp.security.ntt/insights_resources/tech_blog/en-waterplum-ottercookie/
logsource:
  category: file_event
detection:
  selection_folder:
    TargetFilename|contains:
      - 'windows-cache'
      - 'test/fixtures/eval/'
      - 'test\fixtures\eval\'
  selection_node_files:
    Image|endswith:
      - '\node.exe'
      - '\electron.exe'
      - '/node'
      - '/electron'
    TargetFilename|endswith:
      - '\1.tmp'
      - '/1.tmp'
      - '\2.jpeg'
      - '/2.jpeg'
      - '\AppData\Local\1.db'
  # optional: test.list is a generic name, the 100 KB size test cannot be written in Sigma
  selection_test_list:
    TargetFilename|endswith:
      - '\test.list'
      - '/test.list'
  condition: 1 of selection_*
falsepositives:
  - Applications that legitimately cache under a directory named windows-cache
level: high
---
title: OtterCookie Native Clipboard Read or Delivery Command by a Node Process
id: 0c5f6d4b-6a4e-5a53-8f2c-3b1f9e6a7d10
status: experimental
description: >
  node or electron reading the clipboard through Get-Clipboard, pbpaste or
  xclip, and any command line that names the node-nvm-ssh package or the
  reported Bitbucket delivery repository.
references:
  - https://blog.talosintelligence.com/beavertail-and-ottercookie/
logsource:
  category: process_creation
detection:
  parent_node:
    ParentImage|endswith:
      - '\node.exe'
      - '\electron.exe'
      - '/node'
      - '/electron'
  clipboard_read:
    - CommandLine|contains: 'Get-Clipboard'
    - Image|endswith:
        - '/pbpaste'
        - '/xclip'
  delivery:
    CommandLine|contains:
      - 'node-nvm-ssh'
      - 'bitbucket.org/dev-chess/chess-frontend'
  condition: (parent_node and clipboard_read) or delivery
falsepositives:
  - Clipboard utilities that call Get-Clipboard, pbpaste or xclip from node on purpose
level: high
---
title: OtterCookie Command-and-Control Connection
id: 9d2e7b1a-4c58-5e0f-a1b3-6f8c2d4e9a21
status: experimental
description: >
  A connection to the v4 command-and-control domains, to the addresses Talos
  published or to the ports its samples used, on a public address.
references:
  - https://blog.talosintelligence.com/beavertail-and-ottercookie/
  - https://jp.security.ntt/insights_resources/tech_blog/en-waterplum-ottercookie/
logsource:
  category: network_connection
detection:
  selection_domain:
    DestinationHostname|endswith:
      - 'alchemy-api-v3.cloud'
      - 'chainlink-api-v3.cloud'
      - 'moralis-api-v3.cloud'
  # optional: addresses dated 2025-10, they rotate
  selection_ip:
    DestinationIp:
      - '23.227.202.244'
      - '172.86.88.188'
      - '138.201.50.5'
      - '135.181.123.177'
      - '144.172.96.35'
      - '144.172.112.50'
      - '172.86.73.46'
      - '172.86.113.12'
  # optional: ports are shared with ordinary services
  selection_port:
    DestinationPort:
      - 1224
      - 1418
      - 1476
      - 1478
      - 5961
  filter_private:
    DestinationIp|cidr:
      - '10.0.0.0/8'
      - '172.16.0.0/12'
      - '192.168.0.0/16'
      - '127.0.0.0/8'
  condition: selection_domain or selection_ip or (selection_port and not filter_private)
falsepositives:
  - Unrelated services that use the listed ports
level: medium
Lower-fidelity parts (3)
  • the `test.list` file name — a generic name, and the 100 KB size test Talos describes is not applied. Remove: delete the `# optional: test.list` comment and the `selection_test_list` block; `1 of selection_*` needs no change.
  • the eight Talos command-and-control addresses — published 2025-10 and likely rotated since. Remove: delete the `# optional: addresses` comment and the `selection_ip` block, then remove `or selection_ip` from the third rule's condition.
  • the C2 ports 1224, 1418, 1476, 1478 and 5961 on a public address — ordinary services use these ports, so it matches on port alone. Remove: delete the `# optional: ports` comment, the `selection_port` and `filter_private` blocks, then remove `or (selection_port and not filter_private)` from the third rule's condition.

CrowdStrike LogScale

Written for this site, from indicators reported by Cisco Talos, NTT Security Japan

Covers the process and network lines only, because Falcon logs no event for writes of non-executable files such as 1.tmp and 2.jpeg and no request path.

// Falcon has no event for writes of non-executable files, so the file Match line is not expressible here
(
  #event_simpleName=/^(Synthetic)?ProcessRollup2$/
  and (
        (ParentBaseFileName=/^(node|electron)(\.exe)?$/i and (CommandLine=/get-clipboard/i or ImageFileName=/\/(pbpaste|xclip)$/i))
     or CommandLine=/node-nvm-ssh|bitbucket\.org\/dev-chess\/chess-frontend/i
  )
)
or (#event_simpleName=DnsRequest and DomainName=/(alchemy|chainlink|moralis)-api-v3\.cloud$/i)
// optional: addresses dated 2025-10, they rotate
or (#event_simpleName=NetworkConnectIP4 and in(RemoteAddressIP4, values=["23.227.202.244", "172.86.88.188", "138.201.50.5", "135.181.123.177", "144.172.96.35", "144.172.112.50", "172.86.73.46", "172.86.113.12"]))
// optional: these ports are shared with ordinary services
or (#event_simpleName=NetworkConnectIP4 and in(RemotePort, values=[1224, 1418, 1476, 1478, 5961]) and RemoteAddressIP4!=/^(10\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[01])\.|127\.)/)
| groupBy([ComputerName, aid], function=[count(as=events), collect([CommandLine, DomainName, RemoteAddressIP4]), min(@timestamp, as=first_seen), max(@timestamp, as=last_seen)])
| sort(last_seen, order=desc)
Lower-fidelity parts (2)
  • the eight Talos command-and-control addresses — published 2025-10 and likely rotated since. Remove: delete the `// optional: addresses` line and the `or (... in(RemoteAddressIP4 ...))` line below it.
  • the C2 ports 1224, 1418, 1476, 1478 and 5961 on a public address — ordinary services use these ports, so it matches on port alone. Remove: delete the `// optional: these ports` line and the `or (... in(RemotePort ...))` line below it.

Elastic

Written for this site, from indicators reported by Cisco Talos, NTT Security Japan

Each signal is a boolean column so the three low-fidelity ones can be dropped without touching the core conditions.

FROM logs-*
| WHERE @timestamp > NOW() - 30 days
| EVAL path = REPLACE(TO_LOWER(file.path), "\\\\", "/"), cmd = TO_LOWER(process.command_line), proc = TO_LOWER(process.name), parent = TO_LOWER(process.parent.name), web = TO_LOWER(COALESCE(url.original, destination.domain, dns.question.name))
| EVAL f_file = path LIKE "*windows-cache*" OR path LIKE "*test/fixtures/eval/*" OR (proc IN ("node", "node.exe", "electron", "electron.exe") AND (path LIKE "*/1.tmp" OR path LIKE "*/2.jpeg" OR path LIKE "*/appdata/local/1.db"))
| EVAL f_proc = (parent IN ("node", "node.exe", "electron", "electron.exe") AND (cmd LIKE "*get-clipboard*" OR proc IN ("pbpaste", "xclip"))) OR cmd LIKE "*node-nvm-ssh*" OR cmd LIKE "*bitbucket.org/dev-chess/chess-frontend*"
| EVAL f_net = web LIKE "*alchemy-api-v3.cloud*" OR web LIKE "*chainlink-api-v3.cloud*" OR web LIKE "*moralis-api-v3.cloud*" OR web LIKE "*/api/service/makelog*" OR web LIKE "*/client/14/144*" OR web LIKE "*/payload/14/144*" OR web LIKE "*/brow/14/144*"
| EVAL f_list = path LIKE "*/test.list"
| EVAL f_ip = TO_STRING(destination.ip) IN ("23.227.202.244", "172.86.88.188", "138.201.50.5", "135.181.123.177", "144.172.96.35", "144.172.112.50", "172.86.73.46", "172.86.113.12")
| EVAL f_port = destination.port IN (1224, 1418, 1476, 1478, 5961) AND NOT CIDR_MATCH(destination.ip, "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "127.0.0.0/8")
| WHERE f_file OR f_proc OR f_net OR f_list OR f_ip OR f_port
| EVAL signal = CASE(f_file, "file", f_proc, "process", f_net, "network", "low fidelity")
| STATS events = COUNT(*), signals = VALUES(signal), paths = VALUES(path), commands = VALUES(cmd), first_seen = MIN(@timestamp), last_seen = MAX(@timestamp) BY host.name
| SORT last_seen DESC
| LIMIT 100
Lower-fidelity parts (3)
  • the `test.list` file name — a generic name, and the 100 KB size test Talos describes is not applied. Remove: delete the `| EVAL f_list` line and `OR f_list` from the `| WHERE` line.
  • the eight Talos command-and-control addresses — published 2025-10 and likely rotated since. Remove: delete the `| EVAL f_ip` line and `OR f_ip` from the `| WHERE` line.
  • the C2 ports 1224, 1418, 1476, 1478 and 5961 on a public address — ordinary services use these ports, so it matches on port alone. Remove: delete the `| EVAL f_port` line and `OR f_port` from the `| WHERE` line.

Cortex XQL

Written for this site, from indicators reported by Cisco Talos, NTT Security Japan

Counts matching events per host and telemetry type; XDR network events carry no request path, so that match is not expressible here.

config case_sensitive = false
| dataset = xdr_data
| filter (event_type = ENUM.FILE and (
            action_file_path contains "windows-cache"
         or action_file_path contains "test/fixtures/eval/"
         or action_file_path contains "test\\fixtures\\eval\\"
         or (actor_process_image_name in ("node.exe", "node", "electron.exe", "electron") and (action_file_name in ("1.tmp", "2.jpeg") or action_file_path contains "AppData\\Local\\1.db"))
         // optional: test.list is a generic file name
         or action_file_name = "test.list"
         ))
    or (event_type = ENUM.PROCESS and (
            (actor_process_image_name in ("node.exe", "node", "electron.exe", "electron") and (action_process_image_command_line contains "Get-Clipboard" or action_process_image_name in ("pbpaste", "xclip")))
         or action_process_image_command_line contains "node-nvm-ssh"
         or action_process_image_command_line contains "bitbucket.org/dev-chess/chess-frontend"))
    or (event_type = ENUM.NETWORK and (
            action_external_hostname ~= "(alchemy|chainlink|moralis)-api-v3[.]cloud$"
         or dns_query_name ~= "(alchemy|chainlink|moralis)-api-v3[.]cloud$"
         // optional: addresses dated 2025-10, they rotate
         or action_remote_ip in ("23.227.202.244", "172.86.88.188", "138.201.50.5", "135.181.123.177", "144.172.96.35", "144.172.112.50", "172.86.73.46", "172.86.113.12")
         // optional: these ports are shared with ordinary services
         or (action_remote_port in (1224, 1418, 1476, 1478, 5961) and not (action_remote_ip ~= "^(10[.]|192[.]168[.]|172[.](1[6-9]|2[0-9]|3[01])[.]|127[.])"))
         ))
| alter signal = if(event_type = ENUM.FILE, "file", if(event_type = ENUM.PROCESS, "process", "network"))
| comp count() as events, min(_time) as first_seen, max(_time) as last_seen by agent_hostname, signal
| sort desc last_seen
Lower-fidelity parts (3)
  • the `test.list` file name — a generic name, and the 100 KB size test Talos describes is not applied. Remove: delete the `// optional: test.list` line and the `or action_file_name = "test.list"` line below it.
  • the eight Talos command-and-control addresses — published 2025-10 and likely rotated since. Remove: delete the `// optional: addresses` line and the `or action_remote_ip in (...)` line below it.
  • the C2 ports 1224, 1418, 1476, 1478 and 5961 on a public address — ordinary services use these ports, so it matches on port alone. Remove: delete the `// optional: these ports` line and the `or (action_remote_port in (...) ...)` line below it.

osquery

Written for this site, from indicators reported by Cisco Talos

Lists projects that hold node-nvm-ssh, which is enough on its own, and optionally those holding all three keylogger and screenshot dependencies.

SELECT directory, count(DISTINCT name) AS matched, group_concat(DISTINCT name) AS packages
FROM npm_packages
WHERE name = 'node-nvm-ssh'
   -- optional: legitimate packages that matter only together in an assignment's dependency tree
   OR name IN ('node-global-key-listener', 'screenshot-desktop', 'sharp')
GROUP BY directory
HAVING packages LIKE '%node-nvm-ssh%'
   -- optional: all three keylogging and screenshot dependencies in one directory
   OR matched >= 3;
Lower-fidelity parts (1)
  • the three legitimate dependencies node-global-key-listener, screenshot-desktop and sharp — each is an ordinary package and they matter only together in a tree that arrived from an interview assignment. Remove: delete the two `-- optional:` lines and the `OR name IN (...)` and `OR matched >= 3` lines below them, leaving `WHERE name = 'node-nvm-ssh'` and the first HAVING condition.

False positives and tuning

The addresses Talos published date from October 2025 and will have rotated; the URI paths and on-disk artefacts outlive them and are the better half of this hunt.

Talos notes the variant tracked since October 2025 uses much heavier obfuscation, so treat string matching here as a floor rather than coverage.

The windows-cache folder name is not reserved and legitimate software could use it.

The three Node dependencies are legitimate packages; they matter only together in a dependency tree that arrived from an interview assignment.

Sources

Tuning required before use This hunt is a starting point. Adapt the queries to your schema, validate them against your baseline and tune them for the false positives noted above. Indicators go stale as operators rotate hosts.