场景:某中型SaaS企业,10月下旬前端团队升级依赖包后,CI/CD流水线异常外联;安全团队从一条出站告警切入,还原出一条从恶意npm包到构建机失守的完整感染链。
攻击 / 事件时间线
初始访问:投毒包落地
攻击者仿冒内部高频依赖名发布 @corp-ui/icons-pro,版本号与正版仅差一个补丁位,利用 typosquatting 诱导。包内 postinstall 脚本在 npm install 时静默执行,落地 /tmp/.cache/.node 二进制,并通过 curl 拉取二次载荷。构建机无外网直连,但攻击者复用了企业已放行的镜像站域名做 DNS 隧道回连。
npmpostinstall脚本DNS隧道
执行与驻留:构建机失守
二次载荷为轻量 Go 木马,落地后写入 ~/.config/systemd/user/ 用户级服务实现开机自启,规避 root 权限审计。随后枚举环境变量中的 CI_JOB_TOKEN、NPM_TOKEN 等凭据,打包经 DNS 查询子域外传。全程无落地可疑域名,仅见大量 *.cdn-metrics.io 解析。
Go木马systemd user serviceDNS exfil
横向:凭据复用打穿制品库
利用窃取的 NPM_TOKEN 登录企业私有 Nexus,发现其与 GitLab 共用 LDAP。攻击者以构建机身份向私有仓库推送带后门的内部包,并触发一次流水线构建,将恶意制品打入测试环境镜像。横向未用漏洞,纯凭据复用,Living off the Land 特征明显。
NexusGitLab CI凭据复用
清痕:四小时收尾
从首条可见命令到清痕结束约 4 小时 07 分。攻击者删除 ~/.npm/_logs、清空 shell history,并卸载用户级 systemd 服务。但清理遗漏了 journalctl --user 日志与镜像站访问日志,成为后续溯源的关键锚点。
journalctl镜像站日志
蓝队视角 · 发现与处置
SOC 首先收到构建机对 cdn-metrics.io 的高频 DNS 告警,研判发现解析子域长度异常、熵值偏高,判定为隧道外传。随即隔离构建机,回溯 npm 安装日志锁定投毒包,并在 Nexus 审计日志中发现异常推送。蓝队同步吊销 NPM_TOKEN、轮换 LDAP 凭据、冻结相关流水线,用 YARA 规则扫描全量构建机,最终在 3 台节点发现同源木马并完成清除。
涉及关键技术 / 工具
postinstall 脚本投毒与 typosquattingDNS 隧道数据外传与子域熵检测CI 凭据复用横向至私有制品库
防护经验总结
- 对 CI 构建机禁用
postinstall 自动执行(npm config set ignore-scripts true),并对新增依赖做名称相似度校验 - 对构建机出站 DNS 做白名单与子域熵值监控,发现长随机子域即告警,阻断隧道外传通道
- CI Token 最小权限化并按流水线隔离,禁止与 LDAP/制品库共用凭据,定期轮换并审计异常推送
#供应链攻击#npm投毒#DNS隧道#CI/CD安全