场景:2026年9月初,某头部券商内部npm私有镜像站被投毒,一个伪装成lodash-utils的恶意包在72小时内感染了37个业务项目。安全团队从一条异常的CI构建告警开始溯源。
攻击 / 事件时间线
初始投毒:镜像站沦陷
攻击者通过钓鱼获取了运维人员的GitLab会话Token,登录内部npm镜像站后台,上传了一个名为lodash-utils的恶意包(版本1.0.3),其postinstall脚本中嵌入了Base64编码的Node.js反向Shell。该包被设置为lodash的依赖,利用npm的依赖解析机制自动拉取。
GitLab Token钓鱼npm publishBase64编码
横向扩散:CI/CD自动感染
恶意包被拉取后,postinstall脚本执行,从C2(185.xxx.xxx.xxx:443)下载第二阶段载荷node-agent.js,该脚本会读取环境变量中的CI_JOB_TOKEN和NPM_TOKEN,并回传至C2。攻击者利用这些Token进一步向私有镜像站推送更多恶意版本,形成循环感染。
postinstall脚本C2服务器环境变量窃取
持久化与数据窃取
第二阶段载荷在CI Runner上创建了systemd服务node-updater.service,实现持久化。同时,攻击者通过npx执行npm audit伪装命令,实际运行curl将.npmrc、.env等敏感文件外传至C2。72小时内,共37个项目的构建日志中出现异常外联。
systemdcurlnpm audit伪装
蓝队发现:CI告警触发
SOC收到CI流水线告警:某项目构建时间异常增加至12分钟(正常2分钟),且构建日志中出现185.xxx.xxx.xxx的外联记录。蓝队通过Sysmon日志和Zeek流量分析,定位到node-agent.js的下载行为,并提取样本进行YARA规则匹配。
SysmonZeekYARA
溯源反制:C2画像与清除
蓝队通过C2的TLS指纹(JA3)和域名注册信息,关联到攻击者使用的Vultr VPS,并发现其复用了同一套Cobalt Strike证书。随后,蓝队封禁C2 IP,清除恶意包,重置所有CI Token,并对37个项目进行全量依赖审计。
JA3指纹Cobalt Strike证书依赖审计
蓝队视角 · 发现与处置
SOC在收到CI构建超时告警后,立即联动DevOps团队暂停所有流水线。通过分析构建日志,发现postinstall脚本的异常外联行为,提取样本后使用YARA规则(匹配node-agent.js中的CI_JOB_TOKEN关键字)在内部镜像站扫描,确认了12个恶意版本。蓝队随后重置所有CI/CD Token,强制轮换npm凭据,并在镜像站启用包签名验证。整个处置耗时4小时,未造成生产环境数据泄露。
涉及关键技术 / 工具
npm postinstall脚本利用CI/CD环境变量窃取JA3指纹与C2溯源YARA规则匹配
防护经验总结
- 在私有npm镜像站启用
npm audit signatures和--ignore-scripts策略,禁止postinstall脚本自动执行。 - CI/CD环境中的
CI_JOB_TOKEN和NPM_TOKEN应设置为最小权限,并定期轮换,避免长期有效。 - 部署
Sysmon和Zeek对CI Runner进行进程与外联监控,设置异常外联告警(如非白名单IP的443端口连接)。
#供应链攻击#npm投毒#CI/CD安全#溯源反制