Teardown: the Telnyx PyPI compromise

A stolen PyPI token, and a credential stealer fetched inside a WAV file on import.

What happened on 27 March 2026, what ran on import, and where Chainsaw would and would not have stopped it.

Questions and answers

What happened?

On 27 March 2026, two malicious versions of the telnyx Python SDK, 4.87.1 and 4.87.2, were published to PyPI with a credential stealer that ran on import telnyx. The GitHub advisory from the Telnyx maintainers gives the publish times as 03:51 UTC for 4.87.1 and 04:07 UTC for 4.87.2; Trend Micro reports both appearing at 03:51. The Telnyx notice, its GitHub advisory and Trend Micro give 10:13 UTC for PyPI’s quarantine of both versions, an exposure window of about six hours. StepSecurity counted roughly 742,000 downloads of the package in the 30 days before the attack.

Telnyx says its platform, APIs and infrastructure were not compromised and that no customer data was accessed; the incident was limited to the PyPI distribution channel. The GitHub repository never held the malicious code, so builds from source and installs pinned to 4.87.0 or earlier were not affected. The exposure was an unpinned pip install telnyx during the window, directly or as a dependency of another package.

Researchers attribute the attack to TeamPCP, the group behind the Trivy and LiteLLM compromises earlier that month. The PyPA advisory notes the same encryption key seen in earlier TeamPCP activity, and StepSecurity, the Telnyx advisory and Trend Micro point to the same RSA-4096 public key and tpcp.tar.gz archive name used against LiteLLM three days before.

How did the attacker get in?

With a stolen PyPI API token, used to upload both versions straight to PyPI outside the project’s GitHub release pipeline. The Telnyx advisory says the attacker obtained the token and that no malicious commits exist in the repository. Endor Labs and Trend Micro note that the project had not configured PyPI trusted publishing, so a long-lived token worked from anywhere.

Sources disagree on where the token came from:

  • The PyPA advisory (PYSEC-2026-3) says the token was exposed through an exploited Trivy dependency.
  • Endor Labs believes the most likely vector is the LiteLLM compromise itself.
  • Telnyx’s notice says it is still investigating how the publishing credentials were obtained, and Trend Micro says it could not determine how TeamPCP got them.

What changed in the package?

The only file the malicious telnyx releases changed was telnyx/_client.py, the client module every use of the SDK imports. The GitHub issue reporting the March 2026 attack and the Telnyx advisory count about 74 added lines:

  • New imports, including subprocess, tempfile, base64 and wave.
  • A base64 decode helper and a long base64 string, which StepSecurity measured at 4,436 characters and Trend Micro at 4,428.
  • Two functions called at module scope at the end of the file: setup() for Windows and FetchAudio() for Linux and macOS.

4.87.1 called Setup() with a capital S, which does not exist and raises a NameError. Sources disagree on how much that broke. The PyPA advisory, the Telnyx advisory and Endor Labs say the typo stopped the malware in 4.87.1 altogether. StepSecurity says FetchAudio() still ran on Linux and macOS in both versions and only the Windows path failed. All agree 4.87.2, uploaded about 16 minutes later with the casing fixed, ran the full chain.

What ran on import?

When import telnyx loaded the malicious March 2026 releases on Linux and macOS, FetchAudio() started a detached Python process that decoded the base64 string and ran the result; on Windows, setup() did the work in the importing process. StepSecurity saw connections to the attacker’s server only when the package was imported, not during pip install. The real payloads were hidden inside WAV audio files on 83.142.209[.]203:8080, not in the package.

  • Fetched a WAV file: ringtone.wav on Linux and macOS, hangup.wav on Windows. StepSecurity and Trend Micro describe the decoding: read the audio frames with Python’s wave module, base64-decode them, take the first 8 bytes as an XOR key and XOR-decode the rest.
  • Linux and macOS, harvested credentials: the decoded Python harvester was piped into a new interpreter without being written to disk. Endor Labs lists SSH keys, AWS, GCP, Azure and Kubernetes config, Docker, npm, git and Vault credentials, database configs, shell histories, crypto wallets, TLS keys, .env files and /etc/shadow, plus live calls to the AWS metadata service, Secrets Manager and SSM Parameter Store.
  • Sent the data out: the results were encrypted with AES-256-CBC, the key wrapped with RSA-4096, packed as tpcp.tar.gz and POSTed to the same server with the header X-Filename: tpcp.tar.gz.
  • Windows, stayed on the host: the decoded binary was written to the user’s Startup folder as msbuild.exe and run with no window, with a hidden msbuild.exe.lock file to skip reinstalling for 12 hours.

Sources disagree on Linux persistence and Kubernetes. The Telnyx advisory and Endor Labs describe a ~/.config/audiomon/audiomon.py implant run as the audiomon systemd user service, polling the server for new WAV payloads, and privileged node-setup-* pods in kube-system when Kubernetes access is found. StepSecurity and Trend Micro describe no Linux persistence.

Would Chainsaw have stopped it?

Yes, for installs that go through the Chainsaw proxy: both 4.87.1 and 4.87.2 are refused on first download, on day 0, before any malware feed lists them. This comes from our re-run of 58 attacks from 2026 against production Chainsaw (v0.22.118) on 19 October 2026:

  • A default rule refuses both versions on day 0. The default rule “Block import-time execution of fetched or decoded code” refuses packages whose import runs code in clearly malicious shapes. In both versions, FetchAudio() is called when the module loads and starts a child Python interpreter on a base64-decoded payload, which is one of those shapes, so the first request fails.
  • The rule has to be in your policy. It is part of the default policy set. An organisation whose policies do not include it gets the hold and the feed below, not the day-0 refusal.
  • The release hold and the feed sit behind it. Chainsaw holds every new PyPI version for 48 hours by default. The known-malicious feed lists both versions on the day of the attack, well inside the hold, so they stay refused after the hold ends.
  • Chainsaw does not see the WAV payloads. They come from the attacker’s server when the package is imported, not from PyPI, so Chainsaw never inspects them. The refusal does not depend on them: the loader in _client.py is enough.
  • Installs that bypass the proxy get the malware. A machine or CI runner pointed straight at pypi.org is not protected. That is why Chainsaw also gates CI, pins laptops through MDM and blocks direct registry traffic at the network.

What should you do?

The advice for the telnyx PyPI compromise is to treat any machine, container build or CI runner that installed telnyx 4.87.1 or 4.87.2 from PyPI on 27 March 2026 as compromised, as Telnyx, the PyPA advisory and the researchers recommend. Once a hit is confirmed, rotating every credential the host could reach comes first, because the Linux and macOS harvester targeted SSH keys, cloud credentials and Kubernetes config.

  1. Check what you installed. Run pip show telnyx, check pip cache list telnyx, and search lockfiles, Docker builds and CI logs for the two versions.
  2. Downgrade to 4.87.0. Run pip install telnyx==4.87.0 and pin it in your requirements.
  3. Look for persistence. On Windows, check the Startup folder for msbuild.exe and msbuild.exe.lock. On Linux and macOS, check for ~/.config/audiomon/, the audiomon systemd user service and /tmp/.initd_state. In Kubernetes, look for node-setup-* pods in kube-system.
  4. Rotate every credential the host could reach: SSH keys, AWS, GCP and Azure credentials, Kubernetes tokens, Docker registry and database credentials, API keys in .env files and environment variables, and Telnyx API keys. Rebuild affected CI runners rather than only rotating their secrets.
  5. Search network logs for 83.142.209[.]203, port 8080, and for outbound requests carrying X-Filename: tpcp.tar.gz.

For the next one:

  1. Pin dependencies with hashes, for example with pip install --require-hashes.
  2. Publish with PyPI trusted publishing, not a long-lived API token. A stolen token worked here because nothing tied uploads to the project’s release pipeline.
  3. Watch for releases with no matching GitHub tag or release. Both malicious versions had none.
  4. Pin GitHub Actions to full commit SHAs, as the reporting issue and Endor Labs recommend.
  5. Hold brand-new releases before installing them. The exposure window here was about six hours, shorter than a one-day hold.

Sources

One teardown per notable attack

Get the next teardown by email

What happened, the install-path angle most coverage misses, and the rule that refuses it.