CanisterWorm: npm Publisher Compromise Deploys Backdoor Across 29+ Packages
CanisterWorm is a worm-enabled supply chain attack that compromises legitimate npm publisher accounts to distribute a Python backdoor. The malware establishes user-level Linux persistence via systemd and utilizes an Internet Computer Protocol (ICP) canister as a dead-drop C2 to continuously fetch and execute secondary payloads, while simultaneously harvesting npm tokens to propagate itself to other packages.
Authors: Socket Threat Research Team
Source:Socket
- domaintdtqy-oyaaa-aaaae-af2dq-cai[.]raw[.]icp0[.]ioICP canister domain used for C2 communication.
- filename/tmp/pglogLocation where the second-stage binary payload is downloaded, marked executable, and run.
- filename~/.config/systemd/user/pgmon.serviceUser-level systemd service file created to establish persistence for the Python backdoor.
- filename~/.local/share/pgmon/service.pyThe Python backdoor script dropped by the malicious npm package loader.
- urlhxxps://tdtqy-oyaaa-aaaae-af2dq-cai[.]raw[.]icp0[.]io/Dead-drop C2 URL (ICP canister) polled by the Python implant to fetch secondary payload URLs.
Key Takeaways
- Threat actors compromised legitimate npm publisher accounts to deploy a worm-enabled backdoor across 29+ packages in @emilgroup and @teale.io namespaces.
- The malware uses a Python implant that polls an Internet Computer Protocol (ICP) canister as a dead-drop C2 to fetch and execute secondary payloads.
- Persistence is achieved on Linux systems via user-level systemd services (systemd --user) named 'pgmon'.
- A propagation script (deploy.js) harvests npm tokens to autonomously republish the malicious payload to other accessible packages using the '--tag latest' flag.
Affected Systems
- Linux hosts
- Developer workstations
- CI/CD runners
- Build systems running npm
Attack Chain
The attack begins when a developer or CI system installs a compromised npm package. A postinstall hook executes 'node index.js', which drops a Python backdoor (service.py) and establishes persistence via a user-level systemd service named 'pgmon'. The Python implant continuously polls an ICP canister dead-drop C2 to retrieve URLs for secondary payloads, which are downloaded to '/tmp/pglog' and executed. Concurrently, a worm component (deploy.js) harvests local npm tokens to republish the malicious code to other accessible packages.
Detection Availability
- YARA Rules: No
- Sigma Rules: No
- Snort/Suricata Rules: No
- KQL Queries: No
- Splunk SPL Queries: No
- EQL Queries: No
- Other Detection Logic: No
The article does not provide specific detection rules such as YARA or Sigma, but outlines behavioral indicators and file paths for hunting.
Detection Engineering Assessment
EDR Visibility: High — EDR solutions should easily detect anomalous systemd service creation, execution of binaries from /tmp, and Node.js spawning systemctl or Python processes. Network Visibility: Medium — Traffic to the ICP canister domain can be monitored, but it is hosted on legitimate Web3 infrastructure (icp0.io), which may blend with normal traffic if ICP is used legitimately in the environment. Detection Difficulty: Moderate — While the persistence mechanisms and file paths are hardcoded and easily detectable, the initial vector relies on legitimate npm package installations, which are noisy in developer environments.
Required Log Sources
- Process Creation (Event ID 4688 / Sysmon Event ID 1)
- File Creation (Sysmon Event ID 11)
- Network Connections (Sysmon Event ID 3)
Hunting Hypotheses
| Hypothesis | Telemetry | ATT&CK Stage | FP Risk |
|---|---|---|---|
| Look for Node.js or npm processes spawning systemctl commands with the '--user' flag, indicating potential user-level persistence establishment. | Process execution logs (e.g., Sysmon Event ID 1, EDR process telemetry) | Persistence | Low to Medium |
| Search for Python processes making regular outbound network connections to '*.icp0.io' domains, followed by file creation in the /tmp directory. | Network connection logs, File creation logs | Command and Control | Low |
| Identify execution of binaries directly from the /tmp directory, specifically files named 'pglog'. | Process execution logs | Execution | Low |
Control Gaps
- Lack of strict egress filtering for CI/CD environments
- Insufficient monitoring of user-level systemd services
Key Behavioral Indicators
- Creation of a systemd service named 'pgmon'
- File creation of '/tmp/pglog' and '/tmp/.pg_state'
- npm publish commands executed autonomously from CI/CD or developer workstations
False Positive Assessment
- Low. The specific combination of the 'pgmon' service, '/tmp/pglog' execution, and ICP canister C2 is highly distinctive to this malware family.
Recommendations
Immediate Mitigation
- Identify and remove any installed versions of the compromised @emilgroup and @teale.io packages.
- Check for and disable the 'pgmon' systemd service on developer workstations and CI runners.
- Delete malicious artifacts including ~/.local/share/pgmon/service.py, /tmp/pglog, and /tmp/.pg_state.
- Revoke and rotate any npm publishing tokens that may have been exposed on affected machines.
Infrastructure Hardening
- Implement strict egress network filtering on CI/CD runners to prevent unauthorized C2 communication.
- Restrict the ability of npm packages to run postinstall scripts by default using 'npm config set ignore-scripts true'.
User Protection
- Deploy EDR solutions to developer workstations to monitor for anomalous process execution and persistence mechanisms.
- Audit developer environments for unauthorized user-level systemd services.
Security Awareness
- Educate developers on the risks of supply chain attacks and the importance of reviewing package dependencies and postinstall behaviors.
MITRE ATT&CK Mapping
- T1195.002 - Supply Chain Compromise: Compromise Software Supply Chain
- T1059.006 - Command and Scripting Interpreter: Python
- T1543.002 - Create or Modify System Process: Systemd Service
- T1105 - Ingress Tool Transfer
- T1552.004 - Unsecured Credentials: Private Keys
- T1102 - Web Service
Additional IOCs
- File Paths:
/tmp/.pg_state- File used by the implant to track previously downloaded payload URLs to avoid re-downloading.
- Command Lines:
- Purpose: Execute the initial loader script during the npm package installation phase. | Tools:
node,npm| Stage: Execution |node index.js - Purpose: Establish persistence by reloading the user systemd daemon, enabling, and starting the malicious service. | Tools:
systemctl,systemd| Stage: Persistence |systemctl --user daemon-reload - Purpose: Republish compromised packages to propagate the worm to other accessible namespaces. | Tools:
npm| Stage: Lateral Movement |npm publish --access public --tag latest
- Purpose: Execute the initial loader script during the npm package installation phase. | Tools:
- Other:
pgmon- Hardcoded systemd service name used for persistence.tdtqy-oyaaa-aaaae-af2dq-cai- ICP canister ID used as the dead-drop C2.deploy.js- Propagation script used to harvest npm tokens and republish packages.