🎯 Contexte
Publié le 20 mars 2026 par Aikido Security (Charlie Eriksen), cet article documente la détection le 20 mars 2026 à 20h45 UTC d’une campagne de compromission massive de packages NPM, baptisée CanisterWorm, attribuée au groupe TeamPCP. Cette attaque fait suite à une compromission de l’outil Trivy moins de 24 heures auparavant, documentée par Wiz.
📦 Packages compromis
- 28 packages dans le scope
@EmilGroup - 16 packages dans le scope
@opengov @teale.io/eslint-config@airtm/uuid-base32@pypestream/floating-ui-dom
🏗️ Architecture en trois étapes
- Stage 1 – Loader Node.js (postinstall) : Un hook
postinstalldansindex.jsdécode un payload base64 (script Python), crée un service systemd utilisateurpgmon.serviceavecRestart=always, et le démarre immédiatement. Aucun accès root requis. - Stage 2 – Backdoor Python persistante : Le script
service.pyattend 5 minutes (évasion sandbox), puis interroge toutes les ~50 minutes un canister ICP (tdtqy-oyaaa-aaaae-af2dq-cai.raw.icp0.io) pour obtenir une URL de payload. Il télécharge le binaire vers/tmp/pglog, l’exécute en processus détaché, et sauvegarde l’URL dans/tmp/.pg_state. Un kill switch est intégré : si l’URL contientyoutube.com, le payload est ignoré. - Stage 3 – Ver de propagation (
deploy.js) : Outil initialement manuel utilisant des tokens NPM volés pour énumérer tous les packages publiables d’un compte, incrémenter la version patch, préserver le README original, et republier avec--tag latest.
🐛 Évolution vers l’auto-propagation
Environ une heure après la vague initiale, une mise à jour de @teale.io/eslint-config (versions 1.8.11 et 1.8.12) a introduit la fonction findNpmTokens() qui :
- Scrape
~/.npmrc,.npmrclocal et/etc/npmrc - Inspecte les variables d’environnement (
NPM_TOKEN,NPM_TOKENS,*NPM*TOKEN*) - Interroge
npm config geten subprocess - Lance automatiquement
deploy.jsen processus détaché avec les tokens récoltés
Cette évolution transforme chaque développeur ou pipeline CI/CD installant le package en vecteur de propagation involontaire.
🔑 Caractéristiques notables
- C2 décentralisé via ICP : premier usage documenté d’un canister Internet Computer comme dead-drop C2, résistant aux takedowns
- Rotation de payload à distance : le contrôleur du canister peut changer l’URL à tout moment
- Masquage PostgreSQL : artefacts nommés
pgmon,pglog,.pg_state - Quatre vagues documentées avec hashes SHA256 distincts pour
index.jsetdeploy.js - Message laissé dans le code source par le threat actor s’adressant directement à l’auteur de l’article
📊 Type d’article
Analyse technique détaillée avec extraction complète des IOCs, codes sources malveillants annotés et chronologie des vagues d’attaque, destinée aux équipes CTI et sécurité supply chain.
🧠 TTPs et IOCs détectés
Acteurs de menace
- TeamPCP (unknown)
TTP
- T1195.002 — Supply Chain Compromise: Compromise Software Supply Chain (Initial Access)
- T1059.006 — Command and Scripting Interpreter: Python (Execution)
- T1059.004 — Command and Scripting Interpreter: Unix Shell (Execution)
- T1543.001 — Create or Modify System Process: Launch Agent (Persistence)
- T1053.003 — Scheduled Task/Job: Cron (Persistence)
- T1027 — Obfuscated Files or Information (Defense Evasion)
- T1036 — Masquerading (Defense Evasion)
- T1497.003 — Virtualization/Sandbox Evasion: Time Based Evasion (Defense Evasion)
- T1552.001 — Unsecured Credentials: Credentials In Files (Credential Access)
- T1552.007 — Unsecured Credentials: Container API (Credential Access)
- T1071.001 — Application Layer Protocol: Web Protocols (Command and Control)
- T1102 — Web Service (Command and Control)
- T1105 — Ingress Tool Transfer (Command and Control)
- T1543 — Create or Modify System Process (Persistence)
- T1078 — Valid Accounts (Initial Access)
IOC
- Domaines :
tdtqy-oyaaa-aaaae-af2dq-cai.raw.icp0.io - URLs :
https://tdtqy-oyaaa-aaaae-af2dq-cai.raw.icp0.io/ - SHA256 :
e9b1e069efc778c1e77fb3f5fcc3bd3580bbc810604cbf4347897ddb4b8c163b - SHA256 :
61ff00a81b19624adaad425b9129ba2f312f4ab76fb5ddc2c628a5037d31a4ba - SHA256 :
0c0d206d5e68c0cf64d57ffa8bc5b1dad54f2dda52f24e96e02e237498cb9c3a - SHA256 :
c37c0ae9641d2e5329fcdee847a756bf1140fdb7f0b7c78a40fdc39055e7d926 - SHA256 :
f398f06eefcd3558c38820a397e3193856e4e6e7c67f81ecc8e533275284b152 - SHA256 :
7df6cef7ab9aae2ea08f2f872f6456b5d51d896ddda907a238cd6668ccdc4bb7 - SHA256 :
5e2ba7c4c53fa6e0cef58011acdd50682cf83fb7b989712d2fcf1b5173bad956 - Fichiers :
service.py - Fichiers :
pgmon.service - Fichiers :
pglog - Fichiers :
deploy.js - Fichiers :
index.js - Chemins :
~/.local/share/pgmon/service.py - Chemins :
~/.config/systemd/user/pgmon.service - Chemins :
/tmp/pglog - Chemins :
/tmp/.pg_state
Malware / Outils
- CanisterWorm (other)
- deploy.js (tool)
- service.py (backdoor)
- pgmon (backdoor)
🔗 Source originale : https://www.aikido.dev/blog/teampcp-deploys-worm-npm-trivy-compromise