BeaverTail to InvisibleFerret: The Contagious Interview Malware Chain

WindowsmacOS

TTP Hunt high BeaverTailContagious InterviewInvisibleFerret

The JavaScript stealer Contagious Interview delivers and the Python backdoor it installs, hunted as one chain: hidden .pyp, .npl and .n2 paths, the /pdown C2 layout, ports 1224, 3000 and 3306, silent AnyDesk and Telegram exfiltration.

What this hunt looks for

BeaverTail is the obfuscated JavaScript infostealer Contagious Interview embeds in npm packages, take-home repositories and trojanised conferencing apps. It enumerates wallet browser extensions and steals browser credentials, then fetches a Python distribution from its controller and installs InvisibleFerret, a cross-platform backdoor with main, payload, browser and AnyDesk modules.

  • BeaverTail requests /pdown on port 1224 and unpacks Python into a hidden .pyp folder, or p.zi in the temporary directory
  • it writes the loader as .npl in the home directory and runs it
  • components arrive from /payload, /bow and /adc into .n2, with a helper DLL saved as store.node.

Later reporting extends this: Unit 42's Qt-compiled variant ends python.exe with taskkill, GitLab records port 3000 and .npc and .myvars, Google's EtherHiding report adds a JavaScript variant beaconing on 3306 to Telegram, and Trend Micro reports InvisibleFerret recompiled with Cython into .pyd and .so modules. Python running from .pyp is the strongest signal.

Technique
Obfuscated JavaScript infostealer (MITRE ATT&CK T1027) targeting browser cryptocurrency-wallet extensions (T1005, T1217), ingress of a Python runtime and backdoor over an application-layer channel (T1105, T1071.001), Python execution from a hidden profile folder (T1059.006), remote access through silently installed AnyDesk (T1219.002) and exfiltration over Telegram (T1567). MITRE ATT&CK Software S1246 and S1245.
Data sources
EDR network telemetry, File scanning, Proxy logs, EDR process telemetry
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
File path %HOMEPATH%\.pyp\ (or %TEMP%\p.zi) Where BeaverTail stores the Python runtime it fetches from /pdown (Unit 42, 2023).
File path ~/.npl InvisibleFerret loader, saved under the user's home directory and executed with the downloaded Python (Unit 42, 2023).
File path ~/.n2/pay, ~/.n2/bow, ~/.n2/adc InvisibleFerret components fetched from /payload, /bow and /adc (Unit 42, 2023).
File path %USERPROFILE%\store.node Helper DLL fetched from /node/<version> (Unit 42, 2023).
File path ~/.npc, ~/.myvars, ~/.linvidia, .vscode/argv.exe, p8.zi September 2025 variant: entry point, password-exfiltration file, Linux payload, a renamed 7-Zip, and a password-protected archive whose password is ppp (GitLab).
Port 1224 Command-and-control port for the whole chain since 2023 (Unit 42); 95.164.17.24:1224 (Datadog) and 23.227.202.244:1224 (Talos) are observed hosts.
Port 3000 Controller port in the September 2025 variant, at 172.86.93.139 (GitLab).
Port 3306 Beacon port of the JavaScript InvisibleFerret variant delivered through EtherHiding (Google Threat Intelligence Group).
URI path /keys, /uploads, /node/<version>, /pdown, /client/<campaign_id>, /payload/<campaign_id>, /bow/<campaign_id>, /adc/<campaign_id> The controller's directory layout (Unit 42, 2023); Datadog observed /client/3/<campaign id> with campaign identifiers 726 and 525.
npm package passports-js, bcrypts-js, blockscan-api Packages carrying BeaverTail in Datadog's reporting.
Browser extension ID nkbihfbeogaeaoehlefnkodbefgpgknn MetaMask for Chrome, among the wallet extensions BeaverTail enumerates; Unit 42's October 2024 report lists thirteen.
Behaviour AnyDesk installed silently by a Python process The AnyDesk module installs the legitimate client to provide hands-on-keyboard access (Unit 42, Datadog).
Behaviour taskkill or killall against python.exe before installation The Qt-compiled BeaverTail ends competing Python processes first (Unit 42, 2024).
Domain nvidiasdk.fly[.]dev, businesshire[.]top Payload host and recruiting lure site of the September 2025 variant; requests carry a numeric user-agent (203 Windows, 204 macOS, 208 Linux) as an execution guardrail (GitLab).
Behaviour Exfiltration to a private Telegram chat JavaScript InvisibleFerret variant (Google Threat Intelligence Group).

Logic

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

  1. Collect: process, file and network events on Windows and macOS endpoints, plus the npm package inventory
  2. Match: command line names .pyp, .npl, .n2 or .npc, or a .npl, .npc, .myvars or .linvidia file, or a file under .pyp or .n2, is written
  3. Match: or Python starts AnyDesk with --silent or --install, or an npm package named passports-js, bcrypts-js or blockscan-api is present
  4. Match: or, lower fidelity: node or python reaches port 1224, 3000 or 3306 or api.telegram.org, a lure host, or taskkill ends python
  5. Report: devices with the signal kinds hit, most kinds first

Queries

Defender / Sentinel KQL

Written for this site, from indicators reported by Unit 42, Datadog Security Labs, GitLab, Google Threat Intelligence Group

Any process or file signal in a hidden .pyp, .npl or .n2 location is near-conclusive, and a device with several signal kinds should be isolated first.

union
    // optional: lower fidelity, ports 3000 and 3306 are common development and database ports and node or python legitimately reaches Telegram; delete this line and the branch below it
    (DeviceNetworkEvents
        | where Timestamp > ago(30d)
        | where (InitiatingProcessFileName in~ ("node.exe", "node", "python.exe", "pythonw.exe", "python", "python3") and ((RemotePort in (1224, 3000, 3306) and not(ipv4_is_private(RemoteIP))) or RemoteUrl has "api.telegram.org"))
            or RemoteUrl has_any ("nvidiasdk.fly.dev", "businesshire.top")
        | project Timestamp, DeviceId, DeviceName, Signal = "network", Detail = strcat(InitiatingProcessFileName, " -> ", RemoteIP, ":", RemotePort, " ", RemoteUrl)),
    (DeviceFileEvents
        | where Timestamp > ago(30d)
        | where FileName in~ (".npl", ".npc", ".myvars", ".linvidia")
            or FolderPath matches regex @"(?i)[\\/](\.(pyp|n2)|node_modules[\\/](passports-js|bcrypts-js|blockscan-api))([\\/]|$)"
            // optional: lower fidelity, generic file names that can occur outside the campaign; delete this line and the next
            or FileName in~ ("store.node", "p.zi", "p8.zi") or (FileName =~ "argv.exe" and FolderPath contains ".vscode")
        | project Timestamp, DeviceId, DeviceName, Signal = "file", Detail = strcat(FolderPath, "/", FileName)),
    (DeviceProcessEvents
        | where Timestamp > ago(30d)
        | where FolderPath matches regex @"(?i)[\\/]\.(pyp|n2)[\\/]" or ProcessCommandLine matches regex @"(?i)[\\/]\.((pyp|n2)[\\/]|npl|npc)"
            or (InitiatingProcessFileName in~ ("python.exe", "pythonw.exe", "python", "python3") and ((FileName in~ ("AnyDesk.exe", "AnyDesk") and ProcessCommandLine has_any ("--silent", "--install")) or ProcessCommandLine contains "AnyDesk.app"))
            // optional: lower fidelity, administrators also end Python jobs with taskkill or killall; delete this line and the next
            or (FileName in~ ("taskkill.exe", "taskkill", "killall", "pkill") and ProcessCommandLine contains "python")
        | project Timestamp, DeviceId, DeviceName, Signal = "process", Detail = ProcessCommandLine)
| summarize Hits = count(), Signals = make_set(Signal), FirstSeen = min(Timestamp), LastSeen = max(Timestamp), Details = make_set(Detail, 5) by DeviceId, DeviceName
| order by array_length(Signals) desc, LastSeen desc
Lower-fidelity parts (3)
  • the network branch (ports 1224, 3000 and 3306, api.telegram.org, nvidiasdk.fly.dev, businesshire.top) — 3000 and 3306 are ordinary development and database ports, node and python legitimately reach Telegram, and the hosts rotate. Remove: delete the // optional: comment line and the (DeviceNetworkEvents ...) branch beneath it
  • the store.node, p.zi, p8.zi and .vscode argv.exe file names — generic names that can occur outside the campaign. Remove: delete the // optional: comment line and the line beneath it
  • the taskkill, killall or pkill against python match — administrators and developers also end Python jobs. Remove: delete the // optional: comment line and the line beneath it

Splunk SPL

Written for this site, from indicators reported by Unit 42, Datadog Security Labs, GitLab, Google Threat Intelligence Group

The same process and file signals over the Endpoint data model, with the network and proxy branches over EDR and proxy indexes.

```Placeholders: the Endpoint data model carries process and file events from Windows and macOS; index=network is an EDR or flow source with process_name; index=proxy is the proxy log.```
| tstats count max(_time) AS _time FROM datamodel=Endpoint.Processes WHERE earliest=-30d
    (Processes.process="*/.pyp/*" OR Processes.process="*\\.pyp\\*" OR Processes.process="*/.n2/*" OR Processes.process="*\\.n2\\*" OR Processes.process="*/.npl*" OR Processes.process="*\\.npl*" OR Processes.process="*/.npc*" OR Processes.process="*\\.npc*"
    OR (Processes.parent_process_name IN ("python.exe", "pythonw.exe", "python", "python3") AND ((Processes.process_name IN ("AnyDesk.exe","AnyDesk") AND (Processes.process="*--silent*" OR Processes.process="*--install*")) OR Processes.process="*AnyDesk.app*"))
    ```optional: lower fidelity, administrators also end Python jobs with taskkill or killall; delete this line and the next```
    OR (Processes.process_name IN ("taskkill.exe","taskkill","killall","pkill") AND Processes.process="*python*")
    )
    BY Processes.dest, Processes.process
| rename Processes.dest AS host, Processes.process AS detail
| eval signal="process"
| append
    [| tstats count max(_time) AS _time FROM datamodel=Endpoint.Filesystem WHERE earliest=-30d
        (Filesystem.file_name IN (".npl",".npc",".myvars",".linvidia") OR Filesystem.file_path="*/.pyp/*" OR Filesystem.file_path="*\\.pyp\\*" OR Filesystem.file_path="*/.n2/*" OR Filesystem.file_path="*\\.n2\\*" OR Filesystem.file_path="*node_modules*passports-js*" OR Filesystem.file_path="*node_modules*bcrypts-js*" OR Filesystem.file_path="*node_modules*blockscan-api*"
        ```optional: lower fidelity, generic file names that can occur outside the campaign; delete this line and the next```
        OR Filesystem.file_name IN ("store.node","p.zi","p8.zi") OR (Filesystem.file_name="argv.exe" AND Filesystem.file_path="*.vscode*")
        )
        BY Filesystem.dest, Filesystem.file_path
     | rename Filesystem.dest AS host, Filesystem.file_path AS detail
     | eval signal="file"]
```optional: lower fidelity, ports 3000 and 3306 are common development and database ports and node or python legitimately reaches Telegram; delete this line and the two append blocks below it```
| append
    [search index=network earliest=-30d process_name IN ("node.exe", "node", "python.exe", "pythonw.exe", "python", "python3") (dest_port IN (1224, 3000, 3306) OR url="*api.telegram.org*")
     | where NOT cidrmatch("10.0.0.0/8", dest_ip) AND NOT cidrmatch("172.16.0.0/12", dest_ip) AND NOT cidrmatch("192.168.0.0/16", dest_ip)
     | stats count max(_time) AS _time BY dest, process_name, dest_ip, dest_port
     | eval host=dest, detail=process_name." -> ".dest_ip.":".dest_port, signal="network"]
| append
    [search index=proxy earliest=-30d dest_port IN (1224, 3000, 3306)
     | where match(url, "^https?://[^/]+:[0-9]+/(pdown|keys|uploads|node/[^/]+|client/[0-9]+|payload/[0-9]+|bow/[0-9]+|adc/[0-9]+)")
     | stats count max(_time) AS _time BY src, url
     | eval host=src, detail=url, signal="network"]
| stats count AS hits min(_time) AS first_seen max(_time) AS last_seen values(signal) AS signals values(detail) AS details BY host
| eval signal_count=mvcount(signals)
| convert ctime(first_seen) ctime(last_seen)
| sort - signal_count, - last_seen
Lower-fidelity parts (3)
  • the network branch (ports 1224, 3000 and 3306, api.telegram.org, nvidiasdk.fly.dev, businesshire.top) — 3000 and 3306 are ordinary development and database ports, node and python legitimately reach Telegram, and the hosts rotate. Remove: delete the optional comment line and the two append blocks beneath it
  • the store.node, p.zi, p8.zi and .vscode argv.exe file names — generic names that can occur outside the campaign. Remove: delete the optional comment line and the line beneath it
  • the taskkill, killall or pkill against python match — administrators and developers also end Python jobs. Remove: delete the optional comment line and the line beneath it

Sigma

Written for this site, from indicators reported by Unit 42, Datadog Security Labs, GitLab, Google Threat Intelligence Group

Four rules that apply to Windows and macOS process, file, network and proxy telemetry.

title: BeaverTail or InvisibleFerret Hidden Path, Silent AnyDesk Install or Python Termination
id: f6c5b4a7-9acd-4b8f-8eb6-7cd3a0cf6b06
status: experimental
description: >
  Detects the BeaverTail to InvisibleFerret handoff Unit 42 and GitLab
  documented: a process running from or naming the hidden .pyp, .npl, .n2 and
  .npc paths, and a Python process silently installing AnyDesk, on Windows
  and macOS.
references:
  - https://unit42.paloaltonetworks.com/two-campaigns-by-north-korea-bad-actors-target-job-hunters/
  - https://securitylabs.datadoghq.com/articles/tenacious-pungsan-dprk-threat-actor-contagious-interview/
  - https://gitlab-com.gitlab.io/gl-security/security-tech-notes/threat-intelligence-tech-notes/north-korean-malware-sept-2025/
logsource:
  category: process_creation
detection:
  selection_image:
    Image|contains:
      - '\.pyp\'
      - '/.pyp/'
  selection_cmdline:
    CommandLine|contains:
      - '\.pyp\'
      - '\.npl'
      - '\.n2\'
      - '\.npc'
      - '/.pyp/'
      - '/.npl'
      - '/.n2/'
      - '/.npc'
  selection_anydesk_silent:
    ParentImage|endswith:
      - '\python.exe'
      - '\pythonw.exe'
      - '/python'
      - '/python3'
    Image|endswith:
      - '\AnyDesk.exe'
      - '/AnyDesk'
    CommandLine|contains:
      - '--silent'
      - '--install'
  selection_anydesk_app:
    ParentImage|endswith:
      - '\python.exe'
      - '\pythonw.exe'
      - '/python'
      - '/python3'
    CommandLine|contains: 'AnyDesk.app'
  # optional: lower fidelity, administrators also end Python jobs with taskkill or killall; delete this line and the selection below it
  selection_kill:
    Image|endswith:
      - '\taskkill.exe'
      - '/killall'
      - '/pkill'
    CommandLine|contains: 'python'
  condition: 1 of selection_*
falsepositives:
  - IT-managed silent AnyDesk deployment started by a Python-based tool (selection_anydesk_*), and administrators ending Python jobs (selection_kill)
level: high
---
title: BeaverTail or InvisibleFerret File Written
id: f6c5b4a7-9acd-4b8f-8eb6-7cd3a0cf6b07
status: experimental
description: >
  Detects the files the chain writes: the hidden .npl, .npc, .myvars and
  .linvidia files, anything under .pyp or .n2, the helper and archive names,
  and the three npm packages Datadog reports as carrying BeaverTail.
references:
  - https://unit42.paloaltonetworks.com/two-campaigns-by-north-korea-bad-actors-target-job-hunters/
  - https://securitylabs.datadoghq.com/articles/tenacious-pungsan-dprk-threat-actor-contagious-interview/
logsource:
  category: file_event
detection:
  selection_name:
    TargetFilename|endswith:
      - '\.npl'
      - '\.npc'
      - '\.myvars'
      - '\.linvidia'
      - '/.npl'
      - '/.npc'
      - '/.myvars'
      - '/.linvidia'
  selection_dir:
    TargetFilename|contains:
      - '\.pyp\'
      - '\.n2\'
      - '/.pyp/'
      - '/.n2/'
      - '/node_modules/passports-js/'
      - '/node_modules/bcrypts-js/'
      - '/node_modules/blockscan-api/'
      - '\node_modules\passports-js\'
      - '\node_modules\bcrypts-js\'
      - '\node_modules\blockscan-api\'
  # optional: lower fidelity, generic file names that can occur outside the campaign; delete this line and the selection below it
  selection_generic:
    TargetFilename|endswith:
      - '\store.node'
      - '/store.node'
      - '\p.zi'
      - '/p.zi'
      - '\p8.zi'
      - '/p8.zi'
      - '.vscode\argv.exe'
      - '.vscode/argv.exe'
  condition: 1 of selection_*
level: high
---
# optional: lower fidelity, ports 3000 and 3306 are common development and database ports and node or python legitimately reaches Telegram; delete this line and every document below it
title: Interpreter Connecting to a BeaverTail Port or Lure Host
id: f6c5b4a7-9acd-4b8f-8eb6-7cd3a0cf6b08
status: experimental
description: >
  Detects node or Python connecting to the BeaverTail command-and-control
  ports 1224, 3000 and 3306, restricted to public addresses by the exclusions.
references:
  - https://unit42.paloaltonetworks.com/two-campaigns-by-north-korea-bad-actors-target-job-hunters/
  - https://cloud.google.com/blog/topics/threat-intelligence/dprk-adopts-etherhiding
logsource:
  category: network_connection
detection:
  selection:
    Image|endswith:
      - '\node.exe'
      - '\python.exe'
      - '\pythonw.exe'
      - '/node'
      - '/python'
      - '/python3'
    DestinationPort:
      - 1224
      - 3000
      - 3306
  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 and not filter_private
level: medium
---
title: Request for the BeaverTail Controller Directory Layout
id: f6c5b4a7-9acd-4b8f-8eb6-7cd3a0cf6b09
status: experimental
description: >
  Detects proxy requests for the controller paths Unit 42 and Datadog report
  (/pdown, /keys, /uploads, /node, /client, /payload, /bow, /adc) on the
  documented ports.
references:
  - https://unit42.paloaltonetworks.com/two-campaigns-by-north-korea-bad-actors-target-job-hunters/
  - https://securitylabs.datadoghq.com/articles/tenacious-pungsan-dprk-threat-actor-contagious-interview/
logsource:
  category: proxy
detection:
  selection:
    c-uri|re: '^https?://[^/]+:(1224|3000|3306)/(pdown|keys|uploads|node/[^/]+|client/[0-9]+|payload/[0-9]+|bow/[0-9]+|adc/[0-9]+)'
  condition: selection
level: high
Lower-fidelity parts (3)
  • the network branch (ports 1224, 3000 and 3306, api.telegram.org, nvidiasdk.fly.dev, businesshire.top) — 3000 and 3306 are ordinary development and database ports, node and python legitimately reach Telegram, and the hosts rotate. Remove: delete the # optional: comment line and the two documents beneath it
  • the store.node, p.zi, p8.zi and .vscode argv.exe file names — generic names that can occur outside the campaign. Remove: delete the # optional: comment line and the selection_generic block beneath it
  • the taskkill, killall or pkill against python match — administrators and developers also end Python jobs. Remove: delete the # optional: comment line and the selection_kill block beneath it

CrowdStrike LogScale

Written for this site, from indicators reported by Unit 42, Datadog Security Labs, GitLab, Google Threat Intelligence Group

The process, AnyDesk and DNS signals over Falcon events; Falcon file and port signals are not used, so staged files surface through the process signals.

(#event_simpleName=/^(ProcessRollup2|SyntheticProcessRollup2)$/ (ImageFileName=/[\\\/]\.(pyp|n2)[\\\/]/i or CommandLine=/[\\\/]\.((pyp|n2)[\\\/]|npl|npc)/i))
or (#event_simpleName=/^(ProcessRollup2|SyntheticProcessRollup2)$/ ParentBaseFileName=/^pythonw?(\.exe|3)?$/i FileName=/^AnyDesk(\.exe)?$/i CommandLine=/--(silent|install)/i)
    // optional: lower fidelity, administrators also end Python jobs with taskkill or killall; delete this line and the next
    or (#event_simpleName=/^(ProcessRollup2|SyntheticProcessRollup2)$/ FileName=/^(taskkill\.exe|killall|pkill)$/i CommandLine=/python/i)
    // optional: lower fidelity, ports 3000 and 3306 are common development and database ports and node or python legitimately reaches Telegram; delete this line and the next
    or (#event_simpleName=DnsRequest DomainName=/(^|\.)(nvidiasdk\.fly\.dev|businesshire\.top)$/i)
| groupBy([aid, ComputerName], function=[count(as=Hits), count(#event_simpleName, distinct=true, as=SignalKinds), collect([#event_simpleName]), min(@timestamp, as=FirstSeen), max(@timestamp, as=LastSeen)])
| sort(SignalKinds, order=desc)
Lower-fidelity parts (2)
  • the taskkill, killall or pkill against python match — administrators and developers also end Python jobs. Remove: delete the // optional: comment line and the line beneath it
  • the network branch (ports 1224, 3000 and 3306, api.telegram.org, nvidiasdk.fly.dev, businesshire.top) — 3000 and 3306 are ordinary development and database ports, node and python legitimately reach Telegram, and the hosts rotate. Remove: delete the // optional: comment line and the line beneath it

Elastic

Written for this site, from indicators reported by Unit 42, Datadog Security Labs, GitLab, Google Threat Intelligence Group

The same process, file, AnyDesk, npm and network signals over Elastic Defend events, counted per host.

FROM logs-endpoint.events.*
| EVAL cmd = TO_LOWER(process.command_line), exe = TO_LOWER(process.executable), path = TO_LOWER(file.path), fname = TO_LOWER(file.name), name = TO_LOWER(COALESCE(dns.question.name, destination.domain))
| WHERE exe RLIKE ".*[\\\\/]\\.(pyp|n2)[\\\\/].*"
    OR cmd RLIKE ".*[\\\\/]\\.((pyp|n2)[\\\\/]|npl|npc).*"
    OR (TO_LOWER(process.parent.name) IN ("python.exe", "pythonw.exe", "python", "python3") AND ((TO_LOWER(process.name) IN ("anydesk.exe", "anydesk") AND cmd RLIKE ".*--(silent|install).*") OR cmd RLIKE ".*anydesk\\.app.*"))
    OR fname IN (".npl", ".npc", ".myvars", ".linvidia")
    OR path RLIKE ".*[\\\\/](\\.(pyp|n2)|node_modules[\\\\/](passports-js|bcrypts-js|blockscan-api))[\\\\/].*"
    OR fname IN ("store.node", "p.zi", "p8.zi")
    OR (fname == "argv.exe" AND path RLIKE ".*\\.vscode.*")
    OR (TO_LOWER(process.name) IN ("taskkill.exe", "taskkill", "killall", "pkill") AND cmd RLIKE ".*python.*")
    OR (TO_LOWER(process.name) IN ("node.exe", "node", "python.exe", "pythonw.exe", "python", "python3") AND ((destination.port IN (1224, 3000, 3306) 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")) OR name == "api.telegram.org"))
    OR name RLIKE "(.*\\.)?(nvidiasdk\\.fly\\.dev|businesshire\\.top)"
| EVAL signal = CASE(file.path IS NOT NULL, "file", process.command_line IS NOT NULL, "process", "network")
| STATS hits = COUNT(*), signal_kinds = COUNT_DISTINCT(signal), signals = VALUES(signal), last_seen = MAX(@timestamp) BY host.name
| SORT signal_kinds DESC, last_seen DESC
Lower-fidelity parts (3)
  • the network branch (ports 1224, 3000 and 3306, api.telegram.org, nvidiasdk.fly.dev, businesshire.top) — 3000 and 3306 are ordinary development and database ports, node and python legitimately reach Telegram, and the hosts rotate. Remove: delete the last two OR lines beginning (TO_LOWER(process.name) IN ("node.exe" and name RLIKE
  • the store.node, p.zi, p8.zi and .vscode argv.exe file names — generic names that can occur outside the campaign. Remove: delete the two OR lines beginning fname IN ("store.node" and (fname == "argv.exe"
  • the taskkill, killall or pkill against python match — administrators and developers also end Python jobs. Remove: delete the OR line beginning (TO_LOWER(process.name) IN ("taskkill.exe"

Cortex XQL

Written for this site, from indicators reported by Unit 42, Datadog Security Labs, GitLab, Google Threat Intelligence Group

The same process, AnyDesk, file, npm and network signals over Cortex events.

dataset = xdr_data
| alter host = lowercase(coalesce(dns_query_name, action_external_hostname, ""))
| filter (event_type = ENUM.PROCESS and (lowercase(action_process_image_path) ~= "[^a-z0-9][.](pyp|n2)[^a-z0-9]" or lowercase(action_process_image_command_line) ~= "[^a-z0-9][.]((pyp|n2)[^a-z0-9]|npl|npc)"))
    or (event_type = ENUM.PROCESS and lowercase(actor_process_image_name) in ("python.exe", "pythonw.exe", "python", "python3") and ((lowercase(action_process_image_name) in ("anydesk.exe", "anydesk") and lowercase(action_process_image_command_line) ~= "--(silent|install)") or lowercase(action_process_image_command_line) ~= "anydesk[.]app"))
        // optional: lower fidelity, administrators also end Python jobs with taskkill or killall; delete this line and the next
    or (event_type = ENUM.PROCESS and lowercase(action_process_image_name) in ("taskkill.exe", "taskkill", "killall", "pkill") and lowercase(action_process_image_command_line) ~= "python")
    or (event_type = ENUM.FILE and (lowercase(action_file_name) in (".npl", ".npc", ".myvars", ".linvidia") or lowercase(action_file_path) ~= "[^a-z0-9]([.](pyp|n2)|node_modules[^a-z0-9](passports-js|bcrypts-js|blockscan-api))[^a-z0-9]"))
        // optional: lower fidelity, generic file names that can occur outside the campaign; delete this line and the next
    or (event_type = ENUM.FILE and (lowercase(action_file_name) in ("store.node", "p.zi", "p8.zi") or (lowercase(action_file_name) = "argv.exe" and lowercase(action_file_path) ~= "[.]vscode")))
        // optional: lower fidelity, ports 3000 and 3306 are common development and database ports and node or python legitimately reaches Telegram; delete this line and the next
    or (event_type = ENUM.NETWORK and ((lowercase(actor_process_image_name) in ("node.exe", "node", "python.exe", "pythonw.exe", "python", "python3") and (action_remote_port in (1224, 3000, 3306) or host = "api.telegram.org")) or host ~= "(^|[.])(nvidiasdk[.]fly[.]dev|businesshire[.]top)$"))
| comp count() as hits, count_distinct(event_type) as signal_kinds, max(_time) as last_seen by agent_hostname
| sort desc signal_kinds, desc last_seen
Lower-fidelity parts (3)
  • the taskkill, killall or pkill against python match — administrators and developers also end Python jobs. Remove: delete the // optional: comment line and the line beneath it
  • the store.node, p.zi, p8.zi and .vscode argv.exe file names — generic names that can occur outside the campaign. Remove: delete the // optional: comment line and the line beneath it
  • the network branch (ports 1224, 3000 and 3306, api.telegram.org, nvidiasdk.fly.dev, businesshire.top) — 3000 and 3306 are ordinary development and database ports, node and python legitimately reach Telegram, and the hosts rotate. Remove: delete the // optional: comment line and the line beneath it

osquery

Written for this site, from indicators reported by Unit 42, Datadog Security Labs, GitLab, Google Threat Intelligence Group

A point-in-time check of running processes, hidden home-directory entries, npm packages and interpreter sockets on the machine in hand.

SELECT 'process' AS signal, name AS item, path || ' ' || cmdline AS detail
FROM processes
WHERE lower(path) LIKE '%/.pyp/%' OR lower(path) LIKE '%\.pyp\%' OR lower(path) LIKE '%/.n2/%' OR lower(path) LIKE '%\.n2\%'
   OR lower(cmdline) LIKE '%/.npl%' OR lower(cmdline) LIKE '%\.npl%' OR lower(cmdline) LIKE '%/.npc%' OR lower(cmdline) LIKE '%\.npc%'
UNION ALL
SELECT 'file', f.filename, f.path
FROM users u JOIN file f ON f.directory = u.directory
WHERE f.filename IN ('.npl', '.npc', '.myvars', '.n2', '.pyp', '.linvidia', 'store.node')
UNION ALL
SELECT 'npm package', name, path
FROM npm_packages
WHERE name IN ('passports-js', 'bcrypts-js', 'blockscan-api')
-- optional: lower fidelity, ports 3000 and 3306 are common development and database ports; delete this line and the five below it
UNION ALL
SELECT 'network', p.name, s.remote_address || ':' || s.remote_port
FROM process_open_sockets s JOIN processes p ON p.pid = s.pid
WHERE s.remote_port IN (1224, 3000, 3306) AND lower(p.name) IN ('node', 'node.exe', 'python', 'python3', 'python.exe', 'pythonw.exe')
  AND s.remote_address NOT LIKE '10.%' AND s.remote_address NOT LIKE '192.168.%' AND s.remote_address NOT LIKE '127.%' AND s.remote_address NOT LIKE '172.16.%';
Lower-fidelity parts (1)
  • the network branch (ports 1224, 3000 and 3306, api.telegram.org, nvidiasdk.fly.dev, businesshire.top) — 3000 and 3306 are ordinary development and database ports, node and python legitimately reach Telegram, and the hosts rotate. Remove: delete the -- optional: comment line and the UNION ALL, SELECT, FROM and two WHERE lines beneath it, ending at the semicolon

YARA

Written for this site, from indicators reported by Unit 42, Datadog Security Labs, GitLab, Google Threat Intelligence Group

Heuristic content rules for a BeaverTail npm payload and an InvisibleFerret Python module; Trend Micro reports 2026 builds compiled to .pyd and .so, which a Python-source rule will not match.

rule Suspected_BeaverTail_NPM_Payload
{
    meta:
        description = "Heuristic match for BeaverTail-style JS infostealer packaged inside an npm module (postinstall payload, wallet-extension enumeration, hardcoded C2 port pattern reported by Unit 42 and Datadog)"
        reference = "https://unit42.paloaltonetworks.com/north-korean-threat-actors-lure-tech-job-seekers-as-fake-recruiters/"
        confidence = "medium"
    strings:
        $wallet1 = "nkbihfbeogaeaoehlefnkodbefgpgknn" ascii // MetaMask extension ID, commonly targeted
        $postinstall = "postinstall" ascii
        // optional: lower fidelity, ordinary minifiers also emit _0x and the port pattern is generic; delete this line, the $obf line and the final 1 of term
        $exfil = /https?:\/\/[a-z0-9.\-]+:12[0-9]{2}\// ascii
        $obf = "_0x" ascii
    condition:
        filesize < 5MB and
        $postinstall and $wallet1 and 1 of ($exfil, $obf)
}

rule Suspected_InvisibleFerret_Python_Payload
{
    meta:
        description = "Heuristic match for InvisibleFerret-style Python backdoor modules (main/payload/browser/AnyDesk) and its reported C2 URI structure"
        reference = "https://securitylabs.datadoghq.com/articles/tenacious-pungsan-dprk-threat-actor-contagious-interview/"
        confidence = "medium"
    strings:
        $mod1 = "browser_module" ascii
        $mod2 = "anydesk_module" ascii nocase
        $uri1 = "/pdown" ascii
        $uri2 = "/uploads" ascii
        $py = "import base64" ascii
    condition:
        filesize < 3MB and $py and 1 of ($mod1, $mod2) and 1 of ($uri1, $uri2)
}
Lower-fidelity parts (1)
  • the _0x obfuscation prefix and the :12xx exfiltration URL pattern — ordinary minifiers also emit _0x and the port pattern is generic. Remove: in Suspected_BeaverTail_NPM_Payload delete the // optional: comment, the $exfil line, the $obf line and the trailing and 1 of ($exfil, $obf) term of the condition

False positives and tuning

The hidden-folder queries are close to zero-noise: no legitimate Python distribution installs into .pyp and nothing benign writes .npl or .n2 into a home directory.

The port queries are noisy: 3000 is the default for countless development servers and 3306 for MySQL, so they are restricted to script interpreters and public addresses, and 1224 will go stale as hosts rotate.

IT-managed silent AnyDesk deployment matches the silent-install signal, so exclude known RMM or configuration-management parent processes. The taskkill and killall query also matches administrators ending Python jobs.

The YARA rules and URI-path strings are heuristic: tune against your own corpus before blocking, since ordinary minifiers also emit the _0x obfuscation prefix.

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.