New Research: Supply Chain Attack on Axios Pulls Malicious Dependency from npm.Details →
Socket
Book a DemoSign in
Socket
Blog
Research

TeamPCP Compromises Telnyx Python SDK to Deliver Credential-Stealing Malware

Malicious versions of the Telnyx Python SDK on PyPI delivered credential-stealing malware via a multi-stage supply chain attack.

TeamPCP Compromises Telnyx Python SDK to Deliver Credential-Stealing Malware

Socket Research Team

March 27, 2026

Socket has identified a supply chain attack affecting the telnyx Python package on PyPI.

The telnyx library is the official Python SDK for the Telnyx communications platform, providing developers with programmatic access to APIs for voice calls, SMS/MMS messaging, WhatsApp, fax, IoT connectivity, and SIP trunking. It is commonly used in backend systems to integrate real-time communications and telephony into applications.

Because the library is used to authenticate and send requests directly to Telnyx APIs, it is often deployed in environments that handle API keys, phone infrastructure, and customer communications, increasing the potential impact of a compromise.

Versions 4.87.1 and 4.87.2 were uploaded to PyPI as malicious releases containing embedded credential-harvesting malware. These versions should not be used.

PyPI has currently quarantined the releases, and users are advised to downgrade to version 4.87.0 or earlier immediately.

Socket’s scanner detected the malicious behavior, alongside independent confirmation from researchers at Aikido and Wiz.

Impact and Usage Context

The package sees tens of thousands of downloads per month, indicating active use in production systems. However, download counts likely underestimate actual deployment, as many organizations rely on internal mirrors and pinned dependencies.

Because this SDK is commonly used in systems responsible for messaging and voice infrastructure, it is often deployed in environments that handle API credentials, customer communications, and operational workflows.

Technical Analysis#

Attack Chain Overview

Our analysis reveals a three-stage runtime attack chain on Linux/macOS consisting of delivery via audio steganography, in-memory execution of a data harvester, and encrypted exfiltration. The entire chain is designed to operate within a self-destructing temporary directory and leave near-zero forensic artifacts on the host. The function name audioimport() and the archive identifier tpcp strongly suggest this payload was distributed as a trojanized Python package on PyPI, likely masquerading as an audio processing or media utility library. The exfiltration archive is named tpcp.tar.gz, and the prefix tpcp directly maps to TeamPCP, a threat actor group linked to an ongoing wave of supply-chain compromises targeting open-source package registries.

Supply Chain Entry Point

All malicious code was injected into a single file: src/telnyx/_client.py. This is a deliberate placement choice. The _client.py module is the core HTTP client that every Telnyx API call flows through, meaning it is imported the moment any application runs import telnyx. The threat actor did not use a postinstall hook in setup.py or pyproject.toml to trigger execution. Instead, both attack functions fire at module import time via top-level calls :

setup()       # Windows attack path
FetchAudio()  # Linux attack path

Postinstall hooks are one of the most heavily monitored signals in PyPI's malware detection pipeline, and security scanners specifically flag packages that execute code during pip install. By moving execution into the library's import path, the threat actor sidesteps that entire class of detection. The malicious code only fires when the library is actually used in a Python process, not during installation. This also means the payload executes in the context of the application's runtime environment, with access to whatever credentials, environment variables, and filesystem permissions the host application has.

The injected code is structured as follows:

  • Lines 4-12: New imports (subprocess, tempfile, os, base64, sys, wave, urllib.request) are added alongside the legitimate SDK imports. These are all standard library modules, so they introduce no new external dependencies and do not alter the package's requirements.txt or metadata. A casual diff review could easily miss them among the existing imports.
  • Line 41-42: A small helper function _d(s) that wraps base64.b64decode(s).decode('utf-8'). This is used throughout the malicious code to keep base64-encoded strings out of plaintext, adding a thin layer of obfuscation against simple string-matching scanners.
  • Line 459: A massive base64-encoded blob assigned to the variable _p. When decoded, this blob is the complete Linux attack payload: the Python script that performs steganographic download, fileless harvester execution, hybrid encryption, and exfiltration. Embedding the entire Linux payload as a base64 string inside a legitimate SDK file means static analysis tools must decode and analyze the blob to detect anything suspicious. The variable name _p is intentionally nondescript, consistent with internal/private variable naming conventions in Python, and unlikely to draw attention during code review.
  • Lines 7760-7818: The setup() function containing the complete Windows attack path.
  • Lines 7806-7817: The FetchAudio() function, which spawns a detached Python process (start_new_session=True) to decode and exec() the _p payload. The use of start_new_session=True is a deliberate orphaning technique: the child process is detached from the parent's process group and session, meaning it continues running independently even if the parent process exits. This ensures the Linux attack completes even if the importing application is short-lived (a CLI tool, a script, a Lambda function that terminates after a single invocation).

Version Progression

The version history reveals an threat actor who is actively monitoring, testing, and iterating on their implant:

Version 4.87.0 is the clean baseline. It contains the legitimate Telnyx SDK code with no malicious modifications, and corresponds to the last authentic release published by the Telnyx maintainers.

Version 4.87.1 is the initial weaponized release. All of the malicious infrastructure described above was injected in this version. However, it shipped with a bug: the Windows attack path defined the function as setup() (lowercase) but the top-level invocation called Setup() (capital S). In Python, function names are case-sensitive, so this produced a NameError at import time on Windows hosts. Because the call is wrapped in the same fail-silent pattern used throughout the malware (bare except blocks), the error was swallowed and the Windows attack simply never executed. The Linux path was unaffected and fully operational in this version.

Version 4.87.2 was published shortly after, containing exactly one change: Setup() was corrected to setup(). No other code was modified. This is a bugfix release by the threat actor, for the threat actor. It confirms several things about the threat actor's operational posture. First, they have a testing or monitoring pipeline. They detected that the Windows path was not executing, diagnosed the root cause as a casing typo, and pushed a targeted fix. Second, they have sustained access to the publishing credentials. The ability to push a follow-up release means the compromised token or account was not a one-time leak but an ongoing access vector. Third, they prioritize the Windows attack path enough to risk a second malicious publish (which increases exposure to detection) solely to activate it. The Windows path's persistence mechanism (Startup folder) provides long-term access that the one-shot Linux path does not, making it strategically valuable enough to warrant the additional risk.

Neither 4.87.1 nor 4.87.2 has a corresponding tag, release, or commit in the official Telnyx GitHub repository, which is one of the clearest indicators that these versions were not published through the project's normal release pipeline.

Platform Targeting

The malware does not target a single platform. It implements dual, OS-specific attack paths with fundamentally different strategies tailored to each operating system's characteristics:

On Windows, the setup() function begins with the inverse guard (if os.name != 'nt': return), exiting immediately on Linux/macOS before proceeding to download hangup.wav from the C2, extracts a PE binary from the audio frames using the same steganographic technique (base64 decode followed by 8-byte XOR), and drops it as msbuild.exe in the Windows Startup folder (%APPDATA%\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\). The binary is named to masquerade as Microsoft's legitimate Build Engine, a tool commonly present on developer workstations. It is executed immediately via subprocess.Popen with the CREATE_NO_WINDOW flag (0x08000000), suppressing any visible console window. A .lock file with a 12-hour cooldown prevents re-execution on repeated imports within the same half-day period. The Startup folder placement ensures the implant survives reboots and persists across sessions without requiring elevated privileges.

On Linux and macOS, the FetchAudio() function guards with (if os.name == 'nt': return), exiting on Windows, before taking the opposite approach. Rather than dropping a persistent binary, it spawns a detached Python process that decodes the _p payload and executes it in-memory. The payload downloads ringtone.wav, extracts the harvester script, runs it filelessly via stdin piping, encrypts the collected data with AES-256-CBC and RSA-4096, and exfiltrates the bundle to the C2. The entire operation is ephemeral: no files persist, no services are installed, no cron jobs are created. The Linux payload includes a redundant platform guard: FetchAudio() checks os.name == 'nt' before spawning the detached process, and the decoded _p payload (audioimport()) performs the identical check again on entry. This defense-in-depth ensures the Linux harvester never executes on Windows even if the _p blob is somehow triggered outside its intended code path, preventing failures in an environment where openssl, tar, and curl may not be available. The strategic split is clear. Windows gets persistence: a binary in the Startup folder that survives reboots, providing the threat actor with long-term, repeatable access. Linux/macOS gets smash-and-grab: a single, high-speed data harvesting operation that collects everything of value and exfiltrates it immediately, then vanishes. This likely reflects the threat actor's assessment of their target environments. Windows developer machines tend to be long-lived, personal workstations where persistent access has compounding value. Linux and macOS environments are more likely to be ephemeral (CI runners, containers, cloud VMs) or hardened (developer machines with more aggressive monitoring), making persistence both harder to maintain and less valuable. A one-shot exfiltration that captures credentials, keys, and tokens provides immediate value regardless of whether the environment survives. Developers installing Python packages from PyPI are disproportionately likely to be running macOS or Linux, and CI runners are overwhelmingly Linux-based.

Stage 1: Steganographic Payload Delivery

Rather than embedding the second-stage harvester directly in the package (which would be trivially flagged by static analysis tools and PyPI's malware scanners), the threat actor employs audio steganography as a retrieval mechanism. The script downloads ringtone.wav from the C2 at 83[.]142[.]209[.]203:8080 using a spoofed Mozilla/5.0 User-Agent header, a minimal but effective measure to bypass naive request filtering that blocks non-browser User-Agents. The choice of a .wav file is not arbitrary. WAV is a raw, uncompressed audio format whose frame data is essentially an opaque byte stream. Unlike MP3 or OGG, WAV frames undergo no lossy compression that would corrupt embedded data. And unlike executable or archive formats, audio files are unlikely to trigger content-type-based network inspection or endpoint detection rules. To a network monitor or a proxy log, the download appears to be a benign audio file fetch.

The extraction logic is concise but effective:

  • The raw audio frames are read via Python's wave module
  • The frame bytes are base64-decoded, producing a binary blob
  • The first 8 bytes are split off as an XOR key; the remainder is the encrypted second-stage payload
  • Each byte of the payload is XOR'd against the key in a rotating fashion (data[i] ^ key[i % 8])

The XOR cipher here is not intended to provide cryptographic security. It is an obfuscation layer designed to prevent the embedded payload from appearing as recognizable Python source code or matching known malware signatures within the WAV file. An 8-byte XOR key provides enough entropy to break simple pattern matching while remaining trivial for the malware to reverse at runtime. This is a pragmatic tradeoff: the threat actor accepts weak encryption for the delivery stage because the WAV file is hosted on infrastructure they control and is already obscured by the steganographic container. The real cryptographic protection is reserved for the exfiltration stage, where intercepted data could compromise the operation.

Stage 2: Fileless Harvester Execution

The decrypted payload, a complete Python script, is executed without ever being written to disk as a file. The malware spawns a child Python interpreter with sys.executable (ensuring it uses the same Python installation that is running the dropper) and passes "-" as the script argument, which instructs CPython to read source code from standard input. The decrypted payload bytes are fed directly via the input parameter of subprocess.run(). This is a deliberate fileless execution technique. The second-stage script exists only in memory, in the pipe buffer between the parent and child processes, and is never written to disk. This defeats file-based scanning (antivirus, YARA rules on the filesystem), makes post-incident forensic recovery of the harvester significantly harder, and ensures that the temporary directory cleanup at the end of execution leaves no trace of what code was actually run.

The stdout of the child process is redirected to a file named simply c within the temporary directory. This design reveals the second stage's purpose: it is a data harvester that writes stolen information to stdout. The dropper does not care what the harvester collects or how. It treats the second stage as a black box that produces an opaque data blob. This separation of concerns is operationally significant: the threat actor can update, swap, or customize the harvester (targeting SSH keys, browser credentials, cloud tokens, cryptocurrency wallets, environment variables, etc.) simply by updating the WAV file on the C2 server, without modifying or re-publishing the dropper package. The dropper itself remains static and clean-looking, reducing the risk of detection through package diffing or update analysis.

An important detail: if the download fails, the WAV parsing fails, or the harvester execution fails, the bare except: return silently aborts the entire operation. The malware does not retry, does not log errors, and does not leave partial artifacts. This fail-silent design prioritizes stealth over reliability. The threat actor accepts that some infections will silently fail rather than risk generating error messages, tracebacks, or crash logs that could alert the victim or appear in monitoring systems. It also means the malware does not persist: if the C2 is down at install time, the infection simply does not occur, and the victim's system remains clean. This is consistent with a mass-targeting, low-and-slow campaign where the threat actor expects many failed attempts and optimizes for the subset that succeed undetected.

Stage 3: Hybrid Encryption and Exfiltration

After the harvester completes, the dropper performs a size check on the collected data (line 61). If the file is empty or missing, indicating the harvester found nothing worth stealing or failed silently, the malware aborts without attempting exfiltration. This is a minor but operationally sound decision: it avoids sending empty archives to the C2, which would create noise in the threat actor's collection pipeline and generate unnecessary network traffic that could trigger anomaly detection.

When data is present, the dropper implements a textbook hybrid encryption (envelope encryption) scheme using OpenSSL command-line tools:

  1. Session key generation: A cryptographically random 256-bit key is generated for this specific exfiltration event. Every infected host produces a unique session key, meaning compromise of one victim's exfiltrated data does not expose any other victim's data.
  2. Symmetric encryption: The collected data is encrypted with AES-256-CBC, using the random session key as the passphrase material (fed through PBKDF2 key derivation). AES-256-CBC provides strong confidentiality for arbitrarily large data volumes.
  3. Asymmetric key wrapping : The 32-byte session key is encrypted with a hardcoded 4096-bit RSA public key using OAEP padding. Only the threat actor, who possesses the corresponding private key, can recover the session key and therefore decrypt the stolen data. This encryption architecture is the most operationally sophisticated element of the malware. The choice to use the system's openssl binary rather than a Python cryptography library (like cryptography or pycryptodome) is deliberate: it avoids adding suspicious dependencies to the malicious package's requirements. A PyPI package that declares a dependency on pycryptodome while claiming to be a simple audio utility would raise immediate red flags. By relying on OpenSSL, which is pre-installed on every macOS and Linux system, the dropper maintains a clean dependency footprint.

The use of RSA-4096 with OAEP padding for key wrapping is notable. OAEP is the modern, IND-CCA2-secure padding mode; the threat actor specifically avoided the older PKCS#1 v1.5 padding, which is vulnerable to Bleichenbacher-style padding oracle attacks. This indicates a non-trivial level of cryptographic awareness. The threat actor understands not just how to use encryption, but how to use it correctly. For defenders, this means the exfiltrated data is cryptographically unrecoverable without the threat actor's private key. There is no implementation shortcut to exploit.

The final bundle (tpcp.tar.gz) containing both payload.enc and session.key.enc is exfiltrated via a raw HTTP POST to the C2's root endpoint, using curl with output suppression (-s -o /dev/null). The custom header X-Filename: tpcp.tar.gz likely serves as a campaign tag or routing directive on the C2 server, allowing the threat actor to sort incoming loot by campaign, package, or target group. The use of curl over Python's urllib for exfiltration, despite urllib being used for the initial download, is likely a reliability choice: curl handles edge cases (redirects, chunked encoding, connection reuse) more robustly for large uploads, and its -s flag provides cleaner output suppression than Python's equivalent.

C2 Infrastructure

Both attack paths connect to the same command-and-control server at 83[.]142[.]209[.]203 on port 8080 over plain HTTP. The C2 exposes three known endpoints:

The naming convention is worth noting. Both payload files use telephony-themed names (ringtone, hangup) that are thematically consistent with the Telnyx communications SDK being the delivery vehicle. Whether this is a deliberate attempt to make the C2 traffic blend in with the compromised package's domain, or simply the threat actor's internal naming preference, is unclear. Either way, it suggests the threat actor was aware of what they were compromising, not just blindly injecting a generic payload into an arbitrary package.

The use of plain HTTP (no TLS) for all C2 communications is an operational weakness. It means the payload downloads, the steganographic WAV files, and the exfiltrated data bundles are all visible to network inspection in transit. However, the hybrid encryption on the exfiltration path partially mitigates this: even if the tpcp.tar.gz upload is captured by a network monitor, the contents are AES-256-CBC encrypted with an RSA-4096 wrapped session key, rendering the stolen data unrecoverable without the threat actor's private key. The payload downloads are less protected; a network defender who captures ringtone.wav or hangup.wav in transit could extract and analyze the second-stage payloads by reversing the base64 + XOR steganography.

Anti-Forensics and Operational Security

The malware's operational security posture is consistent throughout:

  • Self-cleaning workspace: All operations occur inside a tempfile.TemporaryDirectory() context manager. When the function exits, whether by success, failure, or exception, Python's context manager protocol guarantees the directory and all its contents are recursively deleted. No encrypted files, no session keys, no harvester output, and no WAV files survive on disk.
  • Universal error suppression: Every subprocess.run() call redirects stderr to DEVNULL. Both exception handlers use bare except with no logging. The malware is designed to be completely invisible during normal operation. No error dialogs, no stack traces, no log entries.
  • No persistence mechanism: Unlike the Windows path, the Linux/macOS attack does not install a persistence mechanism (no cron jobs, no launch agents, no systemd services). It is a one-shot, import-time payload. It fires once when the library is imported and never runs again. This is a deliberate operational choice: persistence mechanisms are among the most commonly detected indicators of compromise. By sacrificing persistence on Unix systems, the threat actor maximizes the probability that the initial data theft goes undetected. The implicit assumption is that credentials and keys stolen at import time retain their value long enough to be exploited before the victim rotates them.
  • Asymmetric encryption as a one-way gate: Even if network traffic is captured and the C2 server is seized, the encrypted archives are useless without the RSA private key. The public key is embedded in the malware; the private key presumably exists only on infrastructure the threat actor controls. This creates a cryptographic one-way gate that protects the threat actor's collected data even in the event of full infrastructure compromise by defenders.

Outlook and Recommendations#

For Developers

  • Audit your Python environments and requirements.txt files for telnyx==4.87.1 or telnyx==4.87.2. Remove and replace with the clean 4.87.0 release immediately if found.
  • Treat all credentials as exfiltrated. Any API key, token, SSH key, or secret accessible in the environment where the compromised package was imported should be considered compromised. Rotate all credentials without waiting for confirmation of data loss.
  • On Windows hosts, inspect the following directory for msbuild.exe, msbuild.exe.lock, or msbuild.exe.tmp:
%APPDATA%\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\
  • The presence of any of these files confirms the Windows persistence mechanism was deployed. Terminate the process, delete the files, and treat the machine as compromised.
  • On Linux/macOS, the payload is ephemeral and leaves no persistent artifacts. However, the absence of files does not mean the host was unaffected. If the package was imported at any point while the malicious versions were live, assume data was collected and exfiltrated.
  • Check CI/CD pipelines and build environments. If any automated job installed or imported telnyx during the exposure window, rotate all credentials available in that build context including cloud provider tokens, deployment keys, and service account secrets.
  • If you operate internal PyPI mirrors or dependency caches, verify that the quarantined versions have been purged and cannot be resolved by downstream consumers.

For Security Teams

  • Block 83[.]142[.]209[.]203 at the network perimeter and review historical logs for connections to this IP on port 8080. Any match confirms the payload was triggered.
  • The threat actor bypassed postinstall hooks entirely, instead triggering execution via module import-time code in a deeply nested SDK file (src/telnyx/_client.py). This sidesteps the most common detection heuristic for malicious PyPI packages. Scanners that only flag lifecycle hooks will miss this pattern.
  • Watch for anomalous standard library imports such as subprocess, tempfile, wave, and urllib.request appearing alongside SDK-specific imports in core client modules. These are out of place in an HTTP client wrapper and are a strong signal worth investigating.
  • Account for steganographic delivery. Payloads hidden in WAV audio frames are designed to bypass content-type-based network inspection. Detection rules should account for the possibility that executable or script content is embedded in media files fetched over HTTP.
  • Monitor for version releases without corresponding source control tags or commits. This discrepancy is a reliable indicator of compromised publishing credentials and provides an early warning signal independent of code-level analysis.
  • This compromise is part of a broader, ongoing campaign by TeamPCP across multiple package registries. Organizations should track TeamPCP IOCs and TTPs across their full dependency surface, not just PyPI.

Indicators of Compromise (IOCs)#

Malicious Packages

Network Indicators

  • 83[.]142[.]209[.]203:8080
  • http://83[.]142[.]209[.]203:8080/hangup.wav
  • http://83[.]142[.]209[.]203:8080/ringtone.wav
  • http://83[.]142[.]209[.]203:8080/

Hardcoded Credentials

  • Public Key: MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAvahaZDo8mucujrT15ry+ 08qNLwm3kxzFSMj84M16lmIEeQA8u1X8DGK0EmNg7m3J6C3KzFeIzvz0UTgSq6cV pQWpiuQa+UjTkWmC8RDDXO8G/opLGQnuQVvgsZWuT31j/Qop6rtocYsayGzCFrMV 2/ElW1UE20tZWY+5jXonnMdWBmYwzYb5iwymbLtekGEydyLalNzGAPxZgAxgkbSE mSHLau61fChgT9MlnPhCtdXkQRMrI3kZZ4MDPuEEJTSqLr+D3ngr3237G14SRRQB IqIjly5OoFkqJxeNPSGJlt3Ino0qO7fy7LO0Tp9bFvXTOI5c+1lhgo0lScAu1ucA b6Hua+xRQ6s//PzdMgWT3R1aK+TqMHJZTZa8HY0KaiFeVQ3YitWuiZ3ilwCtwhT5 TlS9cBYph8U2Ek4K20qmp1dbFmxm3kS1yQg8MmrBRxOYyjSTQtveSeIlxrbpJhaU Z7eneYC4G/Wl3raZfFwoHtmpFXDxA7HaBUArznP55LD/rZd6gq7lTDrSy5uMXbVt 6ZnKd0IwHbLkYlX0oLeCNF6YOGhgyX9JsgrBxT0eHeGRqOzEZ7rCfCavDISbR5xK J4VRwlUSVsQ8UXt6zIHqg4CKbrVB+WMsRo/FWu6RtcQHdmGPngy+Nvg5USAVljyk rn3JMF0xZyXNRpQ/fZZxl40CAwEAAQ==

Windows Indicators

  • %APPDATA%\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\msbuild.exe
  • %APPDATA%\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\msbuild.exe.lock
  • %APPDATA%\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\msbuild.exe.tmp

Subscribe to our newsletter

Get notified when we publish new security blog posts!

Try it now

Ready to block malicious and vulnerable dependencies?

Install GitHub App
Book a Demo

Questions? Call us at (844) SOCKET-0

Related posts

Back to all posts