Back Endorlabs Trojanized Microsoft SDK: durabletask 1.4.1 through 1.4.3 Deliver Credential-Stealing Malware
Malicious PyPI package durabletask 1.4.1-1.4.3 steals AWS, Azure, and GCP credentials on import. 417k monthly downloads affected.
On May 19, 2026, Endor Labs detected three trojanized versions of durabletask , the official Python SDK for Microsoft's Azure Durable Functions. Versions 1.4.1, 1.4.2, and 1.4.3 of durabletask all contain malicious code that runs on import.
Azure Durable Functions is a Microsoft serverless service used to build long-running workflows in the cloud, including business process automation, document pipelines, and AI agent backends. The package is downloaded roughly 417,000 times a month, and the malicious code runs automatically the moment the package is imported, with no error messages and no visible signs of compromise.
Three things stand out this incident:
The malicious block is the same in both files. It sits at the top, before any legitimate package code:
The initial injection is only thirteen lines. The rest of the attack runs through the downloaded payload.
Python runs module-level code the moment a module is imported. This block is at the top level of __init__.py , so it executes as soon as any code does import durabletask. No function call needed, no configuration, nothing. In Azure Functions, the import happens at cold start before any user code runs.
The payload appears in both __init__.py and task.py . If something imports from durabletask.task directly rather than the top-level package, the code in task.py fires on its own. Both common import paths trigger the download.
Two details in the implementation make this hard to catch at runtime.
start_new_session =True detaches the spawned process from its parent. The payload keeps running even after the Python process that triggered the import exits.
except: pass catches everything without re-raising. If the download fails, if Python is not on the path, if the DNS lookup times out, the package still imports cleanly. No error, no log entry, nothing for a developer to notice.
The platform.system() == "Linux" check skips Windows and macOS. CI runners, cloud VMs, containers, and Azure Functions hosts are predominantly Linux. Developer laptops are not targeted, which delays detection since engineers running pip install locally on a Mac see nothing unusual.
The second-stage payload is fetched from check.git-service.com . The domain is built to look like routine git tooling traffic in network logs. The file lands at /tmp/managed.pyz with a filename that does not stand out in a directory listing. The .pyz format is a Python zipapp: the source is not immediately readable and the server-side file can be swapped out at any time without touching the PyPI package.
rope.pyz is a Python zipapp containing a multi-module malware suite. Based on our payload analysis, it does three things:
Purpose-built collectors target each of the following:
Each collector targets the specific paths and formats used by that platform. This is not a generic scraper grabbing environment variables at random. In a typical CI environment, that means deployment keys, cloud role credentials, and pipeline secrets are all in scope.
Collected credentials are sent to attacker-controlled infrastructure. Tokens, config files, and anything else accessible from the compromised process get shipped off the box using several exfiltration alternative. Primary exfiltration . After the payload aggregates credentials and secrets from the environment, it tries to ship everything to a fixed HTTPS endpoint baked into its configuration—disguised as an innocuous “public version” API on a domain themed around Git hosting. The stolen data is compressed, encrypted with a random AES-256-GCM key, and that key is wrapped with RSA-OAEP using an operator public key embedded in the binary. The client sends a small JSON body (ciphertext envelope plus encrypted key) in a single POST with Content-Type: application/json. From the victim’s perspective it looks like a routine API call; from the operator’s side, only someone holding the matching private key can recover the plaintext bundle.
Backup mothership from GitHub commits . If that primary POST fails—or if an earlier “health check” style fetch cannot reach the operator’s infrastructure—the malware falls back to a public dead drop on GitHub. It queries the commit API with q=FIRESCALE (newest commits first, up to 30 results), then parses each matching commit message for a pattern: a base64-encoded URL and a base64-encoded signature separated by a dot. Before trusting any candidate URL, it verifies an RSA-SHA256 signature over the base64 URL string using the same embedded public key used for encrypting exfil data. The first commit message that passes verification becomes the new mothership URL, and the malware retries the identical encrypted POST against that host. That design lets the operator rotate C2 without redeploying the payload: they only need to publish a new signed commit message somewhere searchable on GitHub.
GitHub repository fallback . When both the hardcoded endpoint and the signed-commit resolver fail, the malware pivots to abusing credentials it already collected. It scans the aggregated JSON for GitHub personal access tokens (ghp_… or fine-grained github_pat_… patterns), then uses each token in turn to talk to the official GitHub REST API. For a working token it creates a new public repository with a randomly generated folklore-themed name, uploads a file named results.json containing the same RSA-wrapped, AES-encrypted package (this time as base64 in the GitHub “create file” API), and treats that upload as success. The exfil path is no longer a custom C2 server but the victim’s own GitHub account—turning stolen developer credentials into a last-resort channel that blends into normal GitHub activity and may evade simple domain blocklists aimed only at the primary domain.
The payload includes a module called roulette.py , a wiper that runs only on hosts that fingerprint as Israeli or Iranian (timezone/localtime/LANG/locale checks for those regions) and only when a random roll lands on 2, otherwise proceeds to deploy the persistence payload instead. If the check passes, it runs:
That attempts to delete everything on the filesystem. The module name suggests the authors knew the locale check would not be perfect and accepted some risk of triggering it on unintended hosts.
The second-stage payload includes Python modules for lateral movement to AWS ECS and Kubernetes pods. On AWS, propagation assumes credentials available from the environment or the EC2 instance metadata role. It flattens discovered SSM-managed instances (each tied to a profile and region), skips the current instance, requires PingStatus Online, ignores Windows PlatformType, and stops after SendCommand succeeds for at most five targets.
Each target receives AWS-RunShellScript via the regional SSM API with a generated bash payload that downloads rope*.pyz from configurable URLs (primary then secondary, see below), runs python3 under nohup, removes the file, and respects a ~/.cache/.sys-update-check marker so the same host does not repeat the bootstrap blindly. The initiating host records propagation state in that marker file with a stable fingerprint derived from machine identifiers.
On Kubernetes, propagation relies on kubectl being present or installed under /tmp, then lists Running pods cluster-wide. For up to five pods it picks the first container name and runs kubectl exec … -- sh -c with a shell script that mirrors the download-and-run pattern. It skips the pod whose name matches the current HOSTNAME, and uses a separate one-shot marker ~/.cache/.sys-update-check-k8s so the workload only drives kubectl exec fan-out once per filesystem view. Success or failure per pod is captured in the propagation.targets structure returned alongside the usual secret-collection results.
Start by confirming whether you are running an affected version:
You can also scan the installed package files directly:
Check network logs for outbound connections to the C2 domain:
Pin to the clean version:
Block the attacker domain at your egress proxy and DNS:
On any Linux machine that had durabletask>=1.4.1 installed and ran an import, assume the following are compromised:
When you're ready to take the step in securing your software supply chain, here are 3 ways Endor Labs can help:
The full story
This article is one source in a clustered incident — the cluster page carries the summary, timeline and every other outlet covering it.
