Telnyx PyPI Compromise: What Garnet Saw at Runtime
TeamPCP published backdoored Telnyx SDK versions to PyPI, reusing the same RSA key and exfiltration headers from the LiteLLM breach. We detonated versions 4.87.1 and 4.87.2 inside a Garnet-monitored runner and captured a base64 payload spawn, C2 egress, and process orphaning.
Garnet TeamThreat Research
4 min read
On March 27, 2026, TeamPCP published two backdoored versions of the Telnyx Python SDK to PyPI — the latest strike in a weeks-long supply chain campaign that has already hit Trivy, Checkmarx KICS, and LiteLLM.
The Telnyx SDK provides voice, messaging, and networking APIs, with roughly 742k monthly downloads. TeamPCP uploaded versions 4.87.1 and 4.87.2 at 03:51 UTC, injecting a backdoor into _client.py that fires at import time. According to Aikido and Socket, the same RSA public key and exfiltration headers from the LiteLLM breach were reused, and the payload concealed later stages within WAV audio files via steganography. PyPI quarantined both versions the same day.
What Garnet observed
Method: detonation of both compromised telnyx versions inside a GitHub Actions runner instrumented with Garnet's eBPF sensor. The workflow extracted the malicious packages from Datadog's malicious-software-packages-dataset, installed telnyx from clean source, overlaid the backdoored _client.py, and triggered import telnyx.
Process lineage
Run 23662517211 · jadoonf/pypi-analysis-feed
Analyse local PyPI archives
No lineage snapshot for this run — add data/profiles/23662517211.json or API credentials.
Process lineage for run 23662517211: jadoonf/pypi-analysis-feed, Analyse local PyPI archives.
Version 4.87.1 crashed immediately — the malware author defined the entry function as setup() but called it as Setup() at line 7823, producing a NameError before any payload could execute. Garnet fired code_on_the_fly and execution_from_unusual_directory on the import, but no child process or network activity followed.
Version 4.87.2 is where the attack succeeds. The _client.py backdoor fires at import time and spawns a child process that decodes and executes a base64 payload. Garnet captured the truncated blob in the command args — partial decoding reveals import subprocess, import tempfile, import os, import base64, import s... (truncated at Garnet's field-length limit). These imports are consistent with a network-capable payload that stages files to disk. The observed network behavior confirms what the code does next: python3.11(2659) opens a TCP connection to 83.142.209.203:8080.
Garnet fires three signals on the child process: code_on_the_fly (dynamic code execution via base64 decode), execution_from_unusual_directory (running from hostedtoolcache), and shell_spawned_by_language_interpreter (Python spawning a subprocess). The flow data shows 6 entries from 10.1.0.106:50584 → 83.142.209.203:8080/TCP, all sharing a single source port — one persistent connection observed across Garnet's polling intervals. Status: "ongoing" (TCP established, not just a SYN).
Then the parent exits. python3.11(2657) (the import process) terminates, but the malicious child python3.11(2659) continues running, reparented to systemd(1). Garnet sees this directly in the ancestry shift: at 18:55:47Z the process sits under the full Runner.Worker chain; by 18:55:55Z it reports to systemd. This is a textbook persistence technique for CI/CD — the workflow step appears to complete, but the payload lives on.
After ~8 seconds, python3.11(2659) exits. Garnet recorded no file writes, no credential file access, and no additional outbound flows from this process — the C2 was reachable but was not serving payloads at replay time. External reporting documents that the intended next stages were WAV steganography download, XOR decode, credential harvesting, AES-256-CBC + RSA-4096 encryption, and tpcp.tar.gz exfiltration — none of which produced Garnet events in this replay.
Garnet produced 100 total events across the run. After pruning CI/CD noise — LD_LIBRARY_PATH from actions/setup-python, hidden_elf_execution from rustc under .rustup/toolchains/, apt-get GPG verification, 7z archive extraction, and legitimate PyPI/GitHub CDN flows — 10 true positive events remain, all correlating to a single process: python3.11(2659).
Real-world impact
The telnyx package has 742k monthly downloads. The _client.py backdoor fires on any import telnyx, making every downstream consumer a potential victim. Version 4.87.1's NameError crash means only 4.87.2 installations would have executed the full payload — but both versions were live on PyPI for the same window. The reuse of the same RSA key and tpcp.tar.gz exfiltration pattern from the LiteLLM compromise confirms a single actor operating across multiple PyPI packages in rapid succession.
Garnet detected every stage of the attack that executed: import-time trigger, base64 child spawn, C2 TCP connection, and process orphaning to systemd. The behavioral signals fire regardless of whether the C2 is live or the exfiltration completes — and the absence of file writes and credential access events in Garnet's telemetry is itself the confirmation that the payload stalled at the connection stage.
Explore the run yourself in the profile above, or start profiling your own CI runs with Garnet.
What Garnet SawSupply ChainPyPITelnyxPythoneBPFRuntime Detection