By clicking “Accept”, you agree to the storing of cookies on your device to enhance site navigation, analyze site usage, and assist in our marketing efforts. View our Privacy Policy for more information.
18px_cookie
e-remove
Blog
Glossary
Customer Story
Video
eBook / Report
Solution Brief

TeamPCP Strikes Again: Telnyx Compromised Three Days After LiteLLM

Written by
Kiran Raj
Kiran Raj
Rachana Misal
Rachana Misal
Published on
March 27, 2026
Updated on
March 27, 2026

Two PyPI releases of telnyx were backdoored with a multi-stage attack that hides its payload inside WAV audio files, steals everything from SSH keys to Kubernetes secrets, and exfiltrates it all to a bare-IP C2 server. The same RSA key from the litellm compromise ties this directly to TeamPCP.

Summary

Less than three days after TeamPCP’s compromise of litellm, the same threat actor struck again - this time compromising telnyx.

On March 27, 2026, Endor Labs identified that telnyx versions 4.87.1 and 4.87.2 on PyPI contain malicious code that does not exist in the upstream GitHub repository. We reported this to the telnyx maintainers through a GitHub issue within three hours of the malicious versions going live.

Telnyx is a telecommunications API SDK with over 3.8 million total downloads and 790,000 monthly downloads. Both versions ship a backdoored telnyx/_client.py that fires two platform-specific attack chains the moment you run import telnyx: on Windows, it drops a persistent binary disguised as msbuild.exe in the Startup folder; on Linux/macOS, it downloads a credential harvester hidden inside a WAV audio file.

Version 4.87.1 was the attacker's first attempt. A single-character typo, Setup() instead of setup(), caused a NameError that killed the entire payload on import. Neither the Windows nor Linux attack paths ever ran. Sixteen minutes later, the attacker pushed 4.87.2 with the fix. The full attack chain went live.

The attack is attributed to TeamPCP: The RSA-4096 public key embedded in the telnyx payload is byte-for-byte identical to the key used in the litellm compromise three days earlier, and the exfiltration pattern — AES-256-CBC encryption, RSA OAEP key wrapping, bundled as tpcp.tar.gz — matches exactly.

Version 4.87.0 is the last known-clean release.

What changed since litellm:

  • WAV steganography replaces inline base64 blobs - payload hides inside audio frames
  • Raw IP C2 (83.142.209.203:8080) replaces domain impersonation (models.litellm.cloud)
  • Windows targeting added for the first time - msbuild.exe in Startup folder
  • Thin dropper architecture - 87% smaller package footprint, real payload fetched at runtime from the C2. K8s lateral movement and persistence are still present, just moved off-disk
  • Persistence renamed - audiomon.service replaces sysmon.service, /tmp/.initd_state replaces /tmp/.pg_state

Affected Packages

Package Name Version(s) Publication Date Malicious Code Functional? Status
telnyx 4.87.1 2026-03-27 03:51 UTC No — NameError on import kills payload Quarantined
telnyx 4.87.2 2026-03-27 04:07 UTC Yes — full attack chain confirmed Quarantined

Last known-clean version: telnyx@4.87.0 (published 2026-03-26 19:27 UTC, verified clean by Endor Labs).

Campaign Timeline

Date Target Attack Vector Impact
Feb 28 Aqua Trivy pull_request_target Pwn Request → stole PAT Full repo takeover. Incomplete remediation left residual access.
Mar 19 Aqua Trivy (2nd) Residual access → imposter commits → tag hijacking Backdoored binaries. Credential stealer on CI/CD runners.
Mar 20 npm (45+ packages) Stolen npm tokens → self-propagating CanisterWorm 28+ scopes compromised in under 60 seconds.
Mar 22 Docker Hub Stolen Docker Hub credentials Malicious trivy:0.69.5–0.69.6 images.
Mar 23 Checkmarx KICS / OpenVSX Compromised service accounts → tag hijacking IDE extensions + GitHub Actions backdoored.
Mar 24 litellm PyPI Stolen PyPI credentials 332-line credential harvester + K8s lateral movement + persistent backdoor. Packages quarantined.
Mar 27 telnyx PyPI (this report) Stolen PyPI token (likely from litellm credential harvest) WAV steganography + full credential harvester + K8s lateral movement + Windows persistence. Packages quarantined.

Technical Analysis

Infection Chain

All of the malicious code lives in one file: telnyx/_client.py. The file is 7,825 lines long. The legitimate SDK code ends around line 7,758. The attacker inserted malicious imports at the top (lines 4-10, 16), a base64 decoder helper (lines 41-42), a 4,428-character base64 payload blob (line 459), and two attack functions with their trigger calls at the bottom (lines 7,761-7,825). None of this exists in the clean GitHub source at tag v4.87.0.

Both attack paths fire at import time. The real SDK classes (Telnyx, AsyncTelnyx) still work perfectly. The malware runs alongside the legitimate package, not instead of it. That is what makes it dangerous: everything looks normal.

Figure 1: The module-level trigger calls at the end of _client.py (lines 7,819–7,825). setup() runs the Windows path, FetchAudio() runs the Linux/macOS path.

Figure 2: Side-by-side diff of the file header. Left: clean v4.87.0 from GitHub (4 lines of imports). Right: malicious v4.87.2 from PyPI (11 lines of imports).

Malicious Behavior

The attack splits into two platform-specific entry points, and on Linux/macOS it chains through five stages: WAV download, credential harvesting, cloud exploitation, Kubernetes lateral movement, and encrypted exfiltration with persistent C2 polling.

Linux/macOS Entry Point: FetchAudio()

On non-Windows systems, FetchAudio() spawns a detached subprocess (start_new_session=True) that decodes the base64 payload stored in _p and runs it. Because the process is detached, it survives even if the parent Python process exits. This is a deliberate sandbox evasion trick. During our initial dynamic analysis, we missed the entire C2 communication because the sandbox exited before the detached process reached out. We only caught the full attack after adding a 60-second post-import delay.

Figure 3: The FetchAudio() function (lines 7,806–7,817). The start_new_session=True flag decouples the malicious subprocess from the parent process.

Stage 1: WAV Steganography Loader

The decoded payload is an 85-line script built around a single function: audioimport(). It fetches a WAV audio file from http://83.142.209.203:8080/ringtone.wav, pulls the real payload out of the audio frames, and runs it.

The WAV file (ringtone.wav, 23,724 bytes in our network capture) is a structurally valid audio file. It passes MIME-type checks, content filters, and casual inspection. But the audio frame data is not audio. It is base64-encoded, XOR-encrypted Python code. The extraction works like this: read WAV frames, base64 decode, take the first 8 bytes as an XOR key, XOR-decrypt the rest. Out comes the credential harvester. To anything watching the network, it looks like the application downloaded a ringtone.

Figure 4: The deobfuscated audioimport() function (decoded from the base64 _p variable in _client.py). The credential harvester is not in this code - it is fetched from the C2 inside a WAV file at runtime.

Stage 2: Credential Harvesting

What comes out of the WAV file is a 332-line Python credential harvester. It is thorough. Dynamic analysis confirmed it runs immediately and starts reading files and environment variables across the system.

Targets include:

  • SSH: ~/.ssh/id_rsa, id_ed25519, id_ecdsa, authorized_keys, config - for all users
  • Cloud: ~/.aws/credentials, ~/.config/gcloud/, ~/.azure/, ~/.kube/config
  • Dev tools: ~/.docker/config.json, ~/.npmrc, ~/.git-credentials, ~/.vault-token
  • Databases: .pgpass, my.cnf, redis.conf, .mongorc.js
  • Crypto wallets: Bitcoin, Ethereum keystores, Solana keypairs, Cardano keys
  • TLS/SSL: All .pem, .key, .p12, .pfx files, LetsEncrypt certificates
  • Env files: .env, .env.local, .env.production, .env.development - searched 6 directories deep
  • Shell histories: bash, zsh, MySQL, PostgreSQL, Redis CLI
  • System: /etc/passwd, /etc/shadow, authentication logs

Figure 5: Credential harvester target paths and categories (from the deobfuscated 332-line harvester delivered via WAV steganography).

Stage 3: Cloud Exploitation

The harvester does not just read files and move on. If it finds AWS credentials, it actively uses them to call cloud APIs:

  • AWS IMDS: Requests an IMDSv2 token, enumerates IAM roles, retrieves temporary security credentials
  • AWS Secrets Manager: Calls ListSecrets then GetSecretValue for each secret
  • AWS SSM Parameter Store: Calls DescribeParameters to enumerate stored configuration

It rolls its own AWS SigV4 signing routine to authenticate these calls - the same implementation we found in the litellm inner script.

Stage 4: Kubernetes Lateral Movement

If a Kubernetes service account token exists, the malware goes after the entire cluster:

  • Secret enumeration: Dumps all secrets across all namespaces
  • Privileged pod deployment: Deploys a privileged pod to every node in kube-system, each mounting the host root filesystem at /host with hostPID, hostNetwork, and privileged: True. The pods chroot into the host to install the persistence implant directly on the node.

Figure 6: The privileged pod manifest from the deobfuscated credential harvester. Pods are named node-setup-{node_name} in kube-system.

Stage 5: Encryption and Exfiltration

Once collection finishes, everything gets encrypted and shipped out:

  1. Generate a random 32-byte AES session key (openssl rand)
  2. Encrypt the collected data with AES-256-CBC (openssl enc)
  3. Encrypt the AES key with the hardcoded RSA-4096 public key using OAEP padding (openssl pkeyutl)
  4. Bundle into tpcp.tar.gz and POST to http://83.142.209.203:8080/ with header X-Filename: tpcp.tar.gz

Windows Attack Path: setup()

The setup() function only runs on Windows. It builds a path to the Startup folder using base64-encoded components decoded at runtime:

  • APPDATA + Microsoft\Windows\Start Menu\Programs\Startup + msbuild.exe

Before downloading, it checks a hidden lock file (msbuild.exe.lock, hidden with attrib +h). If the binary already exists or the lock file is less than 12 hours old, it skips. Otherwise it fetches http://83.142.209.203:8080/hangup.wav, extracts a native Windows executable from the audio frames using the same steganography technique, drops it into the Startup folder, and launches it with CREATE_NO_WINDOW so no console window appears.

Figure 7: The Windows setup() function (lines 7,761–7,804) with decoded base64 annotations.

Persistence

The malware does not just steal and leave. It installs persistence on both platforms.

Linux: The WAV-delivered harvester installs ~/.config/audiomon/audiomon.py and creates audiomon.service ("Audio Controller") as a systemd user service. The implant polls http://83.142.209.203:8080/raw every 45–55 minutes (jittered, centered on 50 min) for new WAV payloads to download and execute. State is tracked in /tmp/.initd_state. The kill switch - "youtube.com" not in response - is identical to litellm.

Windows: msbuild.exe in the Startup folder executes on every login. The hidden .lock file with a 12-hour cooldown prevents re-downloading.

Figure 8: The audiomon.service (right) unit file and polling loop (from the deobfuscated persistence implant), side-by-side with litellm's sysmon.service (left)

How Was the Account Compromised?

There are no GitHub commits for these versions. No tags. No releases. The attacker bypassed telnyx's automated release pipeline entirely and published using a stolen PyPI API token.

No OIDC Trusted Publishers. telnyx uses a long-lived API token rather than PyPI Trusted Publishers. Anyone with the token can publish from anywhere.

Credential cascade from litellm. We believe the most likely vector is the litellm compromise itself. TeamPCP's harvester swept environment variables, .env files, and shell histories from every system that imported litellm. If any developer or CI pipeline had both litellm installed and access to the telnyx PyPI token, that token was already in TeamPCP's hands. The three-day gap fits the time needed to sift through stolen credentials and pick the next target.

Evolution of TTPs: From litellm to telnyx

Three days is not a lot of time, but TeamPCP used it well. The core fingerprints are the same actor. The delivery, evasion, and operational details have all changed.

TTP litellm (March 24) telnyx (March 27)
Payload in package 34,460 chars base64 — full harvester inline 4,428 chars base64 — thin dropper only
Payload delivery Harvester embedded inline Harvester inside WAV files on C2
C2 models.litellm.cloud, checkmarx.zone (HTTPS) 83.142.209.203:8080 (HTTP)
Platform Linux/macOS only Windows + Linux/macOS
Windows persistence None msbuild.exe in Startup + 12h lock
Linux persistence sysmon.service, /tmp/.pg_state audiomon.service, /tmp/.initd_state
Persistence timing Fixed 5-min initial, 50-min poll Jittered 3–7 min initial, 45–55 min poll
K8s lateral movement Inline in package In WAV-delivered payload
Obfuscation base64 base64 + WAV steganography + XOR
OPSEC Clean execution Setup() typo in first version
.pth injection Present in v1.82.8 Dropped

The biggest change is architectural. The litellm payload was a monolith - one decode reveals everything. The telnyx payload is a thin dropper. No credential paths, no AWS calls, no K8s code in the package. The real harvester lives on the C2. This means static analysis is blind, the payload is updatable without republishing, and forensics break down if the C2 goes dark.

C2 Infrastructure

Endpoint Purpose
GET /ringtone.wav Linux/macOS payload (WAV steganography)
GET /hangup.wav Windows payload (WAV steganography)
POST / Exfiltration receiver (tpcp.tar.gz)
GET /raw Persistence polling (new payload URLs)

Attribution

The attack can be clearly attributed to TeamPCP, the RSA-4096 key being the smoking gun. The one embedded in the telnyx payload is byte-for-byte identical to the key used in the litellm compromise:

  • PEM SHA-256: 4eceb569b4330565b93058465beab0e6d5ea09cfba8e7f29d7be1b5a2abd958a
  • DER SHA-256: bc40e5e2c438032bac4dec2ad61eedd4e7c162a8b42004774f6e4330d8137ba8

The keyspace is 2^4096. The probability of two independent actors landing on the same key is zero in any practical sense. And unlike other indicators, this one cannot be faked in a useful way: if someone planted TeamPCP's public key as a false flag, they would not be able to decrypt the data they steal. The key is both the attribution and the proof.

The exfiltration pattern is also identical: AES-256-CBC encryption, RSA OAEP key wrapping, bundled as tpcp.tar.gz, POSTed with an X-Filename: tpcp.tar.gz header.

Figure 9: Side-by-side diff of the RSA-4096 public key - telnyx (left) vs litellm (right), from deobfuscated payloads (decoded_outer_payload.py and decoded_payload.py). The keys are byte-for-byte identical.

Mitigation

Recommended Actions for telnyx Users

  • Verify you are not running version 4.87.1 or 4.87.2 — downgrade to 4.87.0
  • On Linux, check for ~/.config/audiomon/ and audiomon.service
  • On Windows, check Startup folder for msbuild.exe
  • In Kubernetes, check kube-system for node-setup-* pods
  • Check network logs for 83.142.209.203
  • Treat any match as a full-environment compromise — rotate all credentials
  • For maintainers: Enable PyPI Trusted Publishers (OIDC) and rotate all API toke

Detection Commands

Check if the compromised version is installed:

pip show telnyx 2>/dev/null | grep -i version pip freeze 2>/dev/null | grep telnyx

Check for persistence artifacts:

On Linux:

ls -la ~/.config/audiomon/audiomon.py ls -la ~/.config/systemd/user/audiomon.service ls -la /tmp/.initd_state

On Windows:

dir "%APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup\msbuild.exe"

Check for attacker pods in Kubernetes:

kubectl get pods -n kube-system | grep node-setup

If compromise is confirmed:

  1. Downgrade: pip install telnyx==4.87.0
  2. Remove Linux persistence: systemctl --user stop audiomon.service && systemctl --user disable audiomon.service && rm -rf ~/.config/audiomon/ ~/.config/systemd/user/audiomon.service /tmp/.initd_state && systemctl --user daemon-reload
  3. Remove Windows persistence: Delete msbuild.exe and msbuild.exe.lock from %APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup\
  4. Block C2: 83.142.209.203 in firewall/IDS
  5. Rotate ALL credentials - SSH keys, cloud credentials, API keys, database passwords, K8s tokens, .env values, npm tokens, CI/CD secrets
  6. Check network logs for connections to 83.142.209.203:8080

Long-term: Prevention

  • Pin dependencies with hashes (pip install --require-hashes)
  • For PyPI package maintainers: Enable PyPI Trusted Publishers (OIDC) to eliminate long-lived tokens
  • SHA-pin GitHub Actions to full commit hashes
  • Use network-isolated builds where only registry traffic is permitted
  • Monitor for unexpected version bumps without corresponding GitHub releases

Indicators of Compromise (IoCs)

IoC Type Status
telnyx==4.87.1 PyPI Package (backdoored, non-functional) Quarantined
telnyx==4.87.2 PyPI Package (backdoored, functional) Quarantined
7321caa303fe96ded0492c747d2f353c4f7d17185656fe292ab0a59e2bd0b8d9 SHA-256 (4.87.1 wheel) Active
cd08115806662469bbedec4b03f8427b97c8a4b3bc1442dc18b72b4e19395fe3 SHA-256 (4.87.2 wheel) Active
23b1ec58649170650110ecad96e5a9490d98146e105226a16d898fbe108139e5 SHA-256 (_client.py v4.87.1) Active
ab4c4aebb52027bf3d2f6b2dcef593a1a2cff415774ea4711f7d6e0aa1451d4e SHA-256 (_client.py v4.87.2) Active
83.142.209.203:8080 C2 Server Live
http://83.142.209.203:8080/ringtone.wav Linux/macOS payload Live
http://83.142.209.203:8080/hangup.wav Windows payload Live
http://83.142.209.203:8080/ Exfiltration endpoint Live
http://83.142.209.203:8080/raw Persistence polling Live
~/.config/audiomon/audiomon.py Linux persistence implant Filesystem
~/.config/systemd/user/audiomon.service Linux persistence service Filesystem
/tmp/.initd_state Implant state file Filesystem
%APPDATA%\...\Startup\msbuild.exe Windows persistence binary Filesystem
%APPDATA%\...\Startup\msbuild.exe.lock Hidden lock file Filesystem
node-setup-* in kube-system Attacker K8s pods Kubernetes
tpcp.tar.gz / X-Filename: tpcp.tar.gz Exfiltration archive + header Network
4eceb569b4330565b93058465beab0e6d5ea09cfba8e7f29d7be1b5a2abd958a RSA-4096 key hash (PEM) Attribution
bc40e5e2c438032bac4dec2ad61eedd4e7c162a8b42004774f6e4330d8137ba8 RSA-4096 key hash (DER) Attribution

Conclusion

The telnyx compromise confirms what the litellm attack foreshadowed: TeamPCP is working through stolen credential caches, and each compromised package gives them the keys to the next one. Three days between litellm and telnyx is the pace of a group sorting through exfiltrated data and picking high-value targets.

The evolution is not accidental. They made the package footprint 87% smaller, hid the real payload inside WAV audio files, added Windows targeting, and renamed every persistence artifact to dodge detection rules written after our litellm disclosure. But the full capability is still there - the 332-line credential harvester, the Kubernetes lateral movement, and the systemd persistence all live in the WAV-delivered payload. They just moved off-disk where static analysis cannot see them.

Stay alert for upcoming attacks, we will try to catch them as early as possible!

Find out More

The Challenge

The Solution

The Impact

Welcome to the resistance
Oops! Something went wrong while submitting the form.

Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.