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
Last known-clean version: telnyx@4.87.0 (published 2026-03-26 19:27 UTC, verified clean by Endor Labs).
Campaign Timeline
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:
- Generate a random 32-byte AES session key (openssl rand)
- Encrypt the collected data with AES-256-CBC (openssl enc)
- Encrypt the AES key with the hardcoded RSA-4096 public key using OAEP padding (openssl pkeyutl)
- 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.
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
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:
Check for persistence artifacts:
On Linux:
On Windows:
Check for attacker pods in Kubernetes:
If compromise is confirmed:
- Downgrade: pip install telnyx==4.87.0
- 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
- Remove Windows persistence: Delete msbuild.exe and msbuild.exe.lock from %APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup\
- Block C2: 83.142.209.203 in firewall/IDS
- Rotate ALL credentials - SSH keys, cloud credentials, API keys, database passwords, K8s tokens, .env values, npm tokens, CI/CD secrets
- 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)
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!



What's next?
When you're ready to take the next step in securing your software supply chain, here are 3 ways Endor Labs can help:
.jpg)









