If you use the Telnyx Python SDK and you installed it today, stop what you’re doing. Two versions pushed to PyPI in the early hours of March 27 – 4.87.1 and 4.87.2 are malicious. The malware runs the moment you type import telnyx. No setup hooks, no prompts, no warning. It just runs.
Downgrade to telnyx==4.87.0 immediately, then keep reading.
What Happened
At 03:51 UTC this morning, someone pushed two poisoned versions of the Telnyx SDK to PyPI. The package gets about 742,000 downloads a month – contact centers, voice platforms, comms-heavy SaaS. These aren’t hobbyist installs.
The group behind it is TeamPCP. They’ve been running the same playbook for three weeks: compromise a trusted security tool, drain its credentials, use those credentials to push malware into whatever that tool had access to, collect new credentials from the next wave of victims, repeat.
The Telnyx attack is their fifth public move in nine days.

What’s different this time and worth paying attention to is how the payload gets delivered. TeamPCP doesn’t fetch a raw binary or a Python script. They fetch a .wav file. A structurally valid audio file. The malware is hidden inside the audio frame data using XOR obfuscation. It passes MIME-type checks. It’ll slip through URL filters that allow .wav downloads. You won’t catch it with standard static analysis unless you know specifically to decode the WAV frames.
They first used this technique five days ago in a Kubernetes wiper. It went from experiment to production PyPI attack in less than a week.
Who Did This, and Why Does It Keep Happening?
The group behind this is TeamPCP. This is their fifth public attack in nine days, and it’s not random targeting. They’ve been running a deliberate credential harvesting chain since March 19, they started compromising one tool, steal its secrets, use those secrets to push malware into whatever that tool had access to, collect new credentials from the next wave of victims, and repeat.
March 19 – Trivy (CVE-2026-33634, CVSS 9.4)
Aqua Security’s open source vulnerability scanner got backdoored. Every CI/CD pipeline running Trivy without version pinning had its secrets exfiltrated. API tokens, cloud credentials, package registry keys – all of it. TeamPCP also renamed 44 Aqua Security GitHub repos with the prefix tpcp-docs- and changed their descriptions to “TeamPCP Owns Aqua Security.” Subtle.
March 20 – CanisterWorm across npm
Using stolen npm tokens from Trivy victims, TeamPCP deployed CanisterWorm: a worm that takes a single stolen token, enumerates every package that token can publish, bumps the version, and injects malicious code across the entire scope. Sixty seconds. Forty-six-plus packages compromised, including @EmilGroup and @opengov.
March 22 – WAV steganography appears
Researchers spotted TeamPCP using the WAV frame trick for the first time in a Kubernetes wiper variant. It looked like an experiment. It wasn’t.
March 23 – Checkmarx
kics-github-action and ast-github-action were compromised, along with two OpenVSX extensions. The C2 domain was checkmarx[.]zone deliberately chosen to look like the real company. Thirty-five git tags hijacked in under four hours. Cleaned up three hours later.
March 24 – LiteLLM
LiteLLM is a proxy that sits in front of your OpenAI, Anthropic, AWS Bedrock, and GCP VertexAI keys. Basically a master keyring for an organization’s entire AI stack. Versions 1.82.7 and 1.82.8 were published using credentials from LiteLLM’s own CI/CD pipeline, which was running unpinned Trivy. PyPI quarantined them after about three hours. Roughly 95 million downloads a month the window was brief, the exposure wasn’t.
March 27 – Telnyx
Two malicious versions pushed overnight, no corresponding GitHub tags or releases. They didn’t come from Telnyx’s own build process.
How They Got In
The Telnyx GitHub repository shows no signs of compromise at all. Every recent push was from stainless-app[bot], the automated SDK generation platform that manages the pipeline. No force pushes, no unknown contributors, no sketchy PRs.
The last legitimate PyPI publish was v4.87.0 on March 26, through the proper publish-pypi.yml GitHub Actions workflow. No workflow run exists for 4.87.1 or 4.87.2. The attacker uploaded the malicious wheels directly to PyPI – manually, from their own machine.
How do we know? The upload tool fingerprint gives it away. The legitimate Telnyx CI pipeline uses rye publish. The PyPI metadata for 4.87.2 shows the upload client as twine/6.2.0 CPython/3.14.3. That mismatch means someone used a stolen API token to push these versions outside of any automated process.
Telnyx doesn’t use PyPI’s trusted publisher (OIDC) feature, which would have bound uploads to a specific GitHub repository and workflow. Without that, a stolen token is a master key. Anyone holding it can push any version from anywhere.
The most likely explanation: the PYPI_TOKEN was stolen in a prior credential harvesting operation, possibly through the same chain that started with Trivy. If your CI/CD pipeline ran Trivy anytime after March 19, your secrets may have already been exfiltrated.
What the Malware Actually Does
The injected code lives in telnyx/_client.py. That file is part of the package’s normal initialization, so import telnyx is all it takes. Two functions get called at module scope with zero exceptions handling around them:
setup()FetchAudio()
Version 4.87.1 had a bug in Setup() (capital S) vs setup() (lowercase) that caused a NameError and actually prevented either attack path from running. Version 4.87.2 fixed the typo. Treat both versions as malicious regardless.
Windows: Startup Persistence
The setup() function only runs on Windows (os.name == 'nt'). It:
- Downloads
hangup.wavfrom83[.]142[.]209[.]203:8080 - Reads the WAV file’s audio frames
- Base64-decodes the frame data
- Takes the first 8 bytes as an XOR key and decrypts the rest
- Writes the resulting PE executable to the Windows Startup folder as
msbuild.exe– named to blend in with a legitimate Microsoft tool - Launches it silently using
creationflags=0x08000000(no console window)
A .lock file enforces a 12-hour cooldown between re-drops, keeping C2 traffic low enough to avoid timing-based detection. The lock file is hidden using attrib +h.
Linux/macOS: Three-stage infostealer
FetchAudio() runs on everything that isn’t Windows. It decodes a 4,436-character base64 blob hardcoded at line 459 of _client.py and runs it in a detached subprocess (start_new_session=True), so credential theft continues even if the importing process exits.
That decoded payload is an 83-line credential harvester. Here’s what it does:
1: Downloads ringtone.wav from the same C2 server. Extracts a second-stage harvester script from the audio frames using the same XOR-over-base64 technique. Pipes that script into a fresh Python interpreter and captures its output i.e. your credentials to a temp file.
2: Encrypts the stolen data. Generates a random 32-byte AES session key, encrypts the collected credentials with AES-256-CBC, then wraps the session key itself with the attacker’s RSA-4096 public key using OAEP padding. Both encrypted files go into a tarball.
3: POSTs the tarball to 83[.]142[.]209[.]203:8080 with the header X-Filename: tpcp.tar.gz.
The hybrid encryption means that even if you intercept the traffic, you can’t read what was stolen. You’d need the attacker’s RSA private key.
The WAV trick
This is the part that changes things for defenders.
TeamPCP isn’t downloading raw binaries or Python scripts. They’re downloading .wav files which are basically structurally valid audio containers. The payload is stuffed inside the audio frame data and is base64-encoded with XOR-obfuscation. The WAV header is legitimate. The file passes MIME-type checks. It clears URL filters that allow audio downloads. Standard static analysis tools won’t catch it unless someone specifically knows what to look for in the audio frames.
The extraction logic is simple:
with wave.open(wf, '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))])
The code reads the frames. Base64-decode them. First 8 bytes become the XOR key. The rest is the payload. That’s it.
What makes this particularly annoying from a defense standpoint is that a communications SDK downloading an audio file is completely normal traffic. There’s nothing to flag. This is what makes it difficult to detect with traditional techniques. TeamPCP tested this technique on March 22 and deployed it at scale five days later. Expect it to stick around.
How do we know that This Is Definitely TeamPCP?
Three things tie this attack to the same group behind the LiteLLM compromise:
- The RSA-4096 public key matches byte for byte. The key embedded in the decoded
_ppayload is identical to the one in LiteLLM 1.82.8’s Stage 1 orchestrator. Only whoever holds the private key can decrypt the exfiltrated data. - The exfil header is identical. Both attacks use
X-Filename: tpcp.tar.gz. “TPCP” is TeamPCP’s own initials. They’re not hiding the attribution. - The encryption scheme is the same. Both attacks use the exact same sequence:
openssl rand, thenopenssl enc -aes-256-cbc, thenopenssl pkeyutl -encryptwith OAEP padding.
Only one change was noticed – the LiteLLM attack used a compromised domain for C2 but this one uses a raw IP. Also, the WAV delivery mechanism is new in compared to all their previous attacks.
Indicators of Compromise
Indicators of Compromise
| Category | Indicator |
|---|---|
| Malicious package | telnyx==4.87.1 |
| Malicious package | telnyx==4.87.2 |
| SHA256 (4.87.1) | 7321caa303fe96ded0492c747d2f353c4f7d17185656fe292ab0a59e2bd0b8d9 |
| SHA256 (4.87.2) | cd08115806662469bbedec4b03f8427b97c8a4b3bc1442dc18b72b4e19395fe3 |
| C2 server | 83[.]142[.]209[.]203:8080 |
| Windows payload URL | hxxp://83[.]142[.]209[.]203:8080/hangup.wav |
| Linux/Mac payload URL | hxxp://83[.]142[.]209[.]203:8080/ringtone.wav |
| Exfil endpoint | hxxp://83[.]142[.]209[.]203:8080/ (POST) |
| Exfil header | X-Filename: tpcp.tar.gz |
| Windows persistence | %APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup\msbuild.exe |
| Windows lock file | %APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup\msbuild.exe.lock |
| Injection file | src/telnyx/_client.py (line 459) |
What You Need to Do Now
Fix the package first
pip install telnyx==4.87.0
Then purge 4.87.1 and 4.87.2 from every virtualenv, Docker image, and pipeline. Check your requirements.txt, pyproject.toml, and Pipfile.lock for unpinned telnyx. If you have a private mirror (Artifactory, Nexus, etc.), pull the malicious versions from there too.
If it was installed, assume the machine is compromised
Don’t try to clean the environment. Rebuild from a known good image.
Then rotate everything:
- API keys and service tokens
- SSH keys in
~/.ssh/or held byssh-agent - Cloud credentials (AWS IAM, GCP service accounts, Azure service principals)
- Anything stored in a secrets manager that was accessible from that machine and audit the access logs
On Windows: check for msbuild.exe in your Startup folder. If it’s there, delete it and the .lock file. Then search egress logs and the filesystem for tpcp.tar.gz.
Detection rules to add
- Block
83.142.209[.]203at your firewall and proxy - SIEM alert: HTTP requests with header
X-Filename: tpcp.tar.gz - Alert on Python processes spawning child processes via
sys.executablewith stdin piping - Alert on
msbuild.exelaunching from the Startup folder (unless you have a legitimate use for it there) - Run
pip-auditor check your SBOM againsttelnyx==4.87.1andtelnyx==4.87.2
Longer term
Pin your dependencies to exact versions in production. Use pip install --require-hashes. Set up PyPI trusted publisher (OIDC) for any packages you maintain. It makes stolen tokens useless outside the specific GitHub workflow they’re tied to. If you ran Trivy in CI/CD and haven’t audited your secrets since March 19, do it now.
The Bigger Picture
Credential chaining is what makes this campaign work. Each victim organization hands TeamPCP new keys, and those keys open new targets. The difficulty doesn’t go up as the attack scales – if anything it gets easier because the credential pool keeps growing.
Here’s the uncomfortable part: if your organization used Trivy, Checkmarx tools, LiteLLM, and Telnyx in the same CI/CD pipeline, you may have contributed credentials to this pool more than once without knowing it.
The WAV delivery technique will probably show up again. It has showed up before too. Most detection tooling watches for binary downloads, Python script fetches, and raw shellcode. An audio file download from a communications SDK is exactly the traffic that doesn’t trip any of those checks. TeamPCP ran a test on March 22 and liked the results enough to ship it at scale five days later.
Also, the import-time execution problem isn’t going away. pip audit and safety check package names against CVE databases. They don’t execute anything. Source review of your full dependency tree is the only way to catch this kind of injection, and almost nobody does that systematically on third-party packages.
The safe version of the Telnyx SDK is 4.87.0. Stay there until the maintainers confirm the compromised credentials are rotated and push a verified clean release.








