应用安全公司 Apiiro 披露,2026 年 5 月至 10 月间,npm 上出现 19 个伪装成 dotenv 库的软件包,每个包都附带一张藏有 PowerShell 命令的 JPEG 照片。开发者调用 require() 加载这些包时命令即被触发,在 Windows 上下载并安装信息窃取木马 Epsilon Stealer。
Apiiro 将这场攻击活动命名为 Darkroom,相关软件包已被 npm 下架。该公司估计真实安装量在几十到几百之间,并已向 URLhaus、Cloudflare 和 GitHub 提交相关地址与账号。
攻击如何运作
Darkroom 的执行链条分为几步:GitHub 上的假 exploit 仓库先运行 npm install 安装某个包,代码在加载该包时从一张 JPEG 照片中取出编码过的 PowerShell 命令,再由这条命令从免费的 Cloudflare 隧道下载信息窃取木马。整个过程在安装阶段不会触发任何动作,安装脚本保持干净,恶意代码只在第一次调用 require() 时启动。
这 19 个包中,5 月中旬出现的是 env-threads,其余 18 个在 9 月 14 日至 10 月 6 日之间发布,目前全部已被 npm 下架。Apiiro 还发现 3 个更早的包,时间可追溯到 2025 年 12 月,结构与 dotenv 克隆代码都与这批一致。
Apiiro 是在排查 Shai-Hulud 蠕虫新变种时发现这批包的:他们把 OSV 在 9 月 10 日至 10 月 9 日间发布的 1,532 条恶意软件包记录按代码和共有指标分组,其中一组对不上任何已有报告,最初 18 个包,后来增至 19 个,全部由 Amazon Inspector 标记。
命令藏在照片何处
所有包附带的是同一张 stest.jpg,大小 287 KB。包括 5 月那个包在内,每个文件直到第 286,604 字节都逐字节一致,隐藏内容从该位置开始。命令位于追加在文件末尾的 APP13 段中,紧挨着结束 JPEG 的标记;APP13 本是存放元数据的区段,图片查看器不会读取,因此照片能正常打开,显示的仍是原图。
require() 被调用时,一个名为 dispatchAnalytics() 的函数启动,按各段长度扫描文件,停在第一个 APP13 处,把读到的文本当作命令。这段 base64 文本被写入临时文件夹里的 VBScript 脚本,文件名形如 relay.vbs,由 wscript.exe 启动并自行删除;脚本以隐藏窗口并带 -NoProfile -NonInteractive -EncodedCommand 选项启动 PowerShell。
每个包里还有一个无人加载的 decode.js,是 loader 的可读源码,沿用了作者自己的命名。对分析者而言,这相当于直接提供了阅读载荷的入口。
诱饵仓库与投放
安装者本应是那些假 exploit 的使用者。Apiiro 在同一个 GitHub 账号下找到五个仓库,该账号创建于 2018 年,沉寂多年后在 2026 年 9 月重新活跃,因此推测是被买下或从原主手中盗走。这些仓库分别冒充 Discord 的 exploit(其中一个获得 59 颗星)、用户名抢注工具、两个在线博彩平台以及一个加密货币钱包破解器。
每个仓库都带一个 setup.bat,先运行 npm install,再执行 node index.js,而 index.js 开头正是对某个 Darkroom 包的 require()。提交历史记录了行动节奏:9 月 22 日关联两个测试包,9 月 24 日在发布 better-dotenv3 四分钟后就让第一个带真实载荷的包上线,两周内被引用的包更换了 8 次。10 月 7 日 dotenv-runtime 被下架后,五个仓库在安装阶段即报错。
Apiiro 称同样的手法自 2025 年 12 月起就在另外 4 个 GitHub 账号上运转,涉及 Telegram 礼物抢购工具和博彩平台的假 exploit,每个账号的包都在几分钟内被替换。npm 在 5 月 13 日下架最后两个包后,不到三小时 env-threads 就出现了。
还有第六种诱饵针对编程助手用户:10 月 4 日同一账号发布 webcontrol-mcp,这是一个用于操控浏览器的 MCP 服务器,服务器代码本身干净,但 README 推荐从 release 页面下载 16 MB 的 helper-setup.exe,并要求即使 SmartScreen 提示风险也照常运行。该文件未见于 VirusTotal,Apiiro 既没有下载也没有分析它。
照片下载了什么
照片里的命令会从 Cloudflare 隧道下载一个可执行文件,以隐藏方式启动并存进用户的临时文件夹(%LOCALAPPDATA%\Temp),文件名为 lahost.exe 或 hello.exe。整场行动使用了四条 Cloudflare quick 隧道,各自使用时段不同;quick 隧道免费、无需账号,每次启动分配随机名称,因此不会有域名出现在新注册网站的名单里。
Apiiro 没有连接这些隧道,但公开记录提供了线索:VirusTotal 上名为 hardwood-studio 的隧道被记录分发过一个 85 MB 的 winhost.exe,MalwareBazaar 将其归类为 Epsilon Stealer。对 9 月和 10 月六个构建版本的沙箱运行显示,每个版本都会从各自的隧道下载一个每小时执行一次的持久化脚本,并把结果发往第二条隧道。5 月的 hello.exe 则是一个 188 KB 的 downloader,自带 epsilon.exe、一个会替换剪贴板中已复制钱包地址的 clipboard hijacker 和一段 shellcode。
攻击者是在真实软件包上测试行动的,并在公开场合出了错。有五个包的载荷是 osu! 官方安装程序,这个无害且启动即可辨认的程序被当作测试用,其中两个在第一个真实载荷出现前两天就进入了诱饵仓库。9 月底的四个包里,照片中的命令用 AES-256 加密,每个包密钥不同,但实际运行的 loader 从未更新,直接把加密块交给 PowerShell,后者无法解析便停了下来;9 月 26 日至 30 日,诱饵仓库恰恰指向这些坏掉的包。Apiiro 用 decode.js 中的密钥离线解开了加密块,显示同一条隧道上出现了新的路径,随后攻击者回到明文命令,每一批换一个新 npm 账号。
影响范围与自查
Apiiro 估计真实安装量在几十到几百之间,因为 npm 对这 19 个包统计到的 4,651 次下载大多来自镜像和扫描器,一个只上线 41 秒的包就记录了 50 次下载。只有 dotenv-async 的曲线像真人所为,第一天 171 次下载,之后每天约 50 次;它也是存活最久的包,在 npm 上待了六天,五个诱饵仓库全都挂在它身上。
Apiiro 已向 URLhaus 提交五个第二阶段地址,向 Cloudflare 举报四条隧道,并向 GitHub 提交两份举报:10 月 9 日针对那个有五个仓库的账号,10 月 11 日针对此前的三个账号和 MCP 诱饵。该公司同时提示了分析的局限:安装量是基于 npm 公开统计的估算,把攻击归于单一攻击者并与 5 月的假 axios 包挂钩依据的是其收集的线索,尚无第三方调查证实;分析属于静态分析,Epsilon Stealer 在机器上的行为来自 VirusTotal、MalwareBazaar 和沙箱运行,而非对受害者的观察。
自查可分三步:在 package-lock.json、yarn.lock、pnpm-lock.yaml 中搜索清单里的 25 个包名;在 Windows 上检查 %TEMP% 中是否存在 relay.vbs,以及 %LOCALAPPDATA%\Temp 中是否有 lahost.exe、hello.exe 或 mshost.exe;一旦发现,就把机器视为已被攻陷,退出浏览器所有会话,更改已保存的密码和 Discord 令牌,并转移该电脑上钱包里的资金。
Apiiro 建议把软件包里的图片当作代码对待,因为一个配置库没有理由带一张 JPEG;凡是被同时会触及 child process 的代码所读取的二进制文件,以及图像数据之后跟着一长串 base64 的 APP13,都应标记出来。在开发者机器和持续集成流水线上,除非确实在用,最好拦截或至少标记发往 .trycloudflare.com 的连接。npm install --ignore-scripts 无法防住这类攻击,因为它只拦截安装阶段的脚本,而载荷在 require() 时启动。
要点
- 19 个假 dotenv 包于 2026 年 5 月至 10 月发布
- 载荷在首次调用 require() 时触发,安装阶段无异常
- 命令藏在 JPEG 末尾的 APP13 段中
- 最终在 Windows 上安装 Epsilon Stealer
- Apiiro 估计真实安装量为几十到几百次

正在读取登录状态…
暂无讨论,说说你的看法吧