警惕!针对国内某大厂开发人员的定向投毒安全事件
原创
suntiger
suntiger
二进制空间安全
2026年8月11日 09:00
北京
在小说阅读器读本章
去阅读
将二进制空间安全设为\"星标⭐️\"
第一时间收到文章更新
事件背景
近日,安全研究人员披露一起针对阿里系开发工具用户的npm供应链投毒事件。攻击者发布了18个恶意包,名字刻意模仿 @ali 作用域下的内部私有包,例如无作用域的 lib-mtop 对应内部的 @ali/lib-mtop。开发者一旦在能访问私有仓库的环境里执行 npm install,依赖解析会按预期拉取内部包,但额外的恶意依赖也会被一并装进项目。
这不是常见的误装拼写错误包那么简单。恶意逻辑被拆散到多层依赖里,顶层包多数只是空壳诱饵,真正的加载器分散在 smart-config-manager、cloud-config-fetcher 等中间层和底层包中。最终落地的是一套跨平台远程控制后门,能在Windows、Linux、macOS 上分别采取不同的持久化手段,并向钉钉、悟空、Qoder 等协作工具注入恶意代码。
安装过程往往一切正常,日志里看不出异常,危害却要等到终端告警或协作软件行为异常时才浮出水面。排查时需联动锁文件深层依赖、开发机持久化痕迹与 npm 源配置,三者缺一项都容易漏掉隐蔽载荷。
攻击架构
整个攻击可以看成诱饵层、桥接层、执行层三段式结构。诱饵层大约10个顶层包,名称对齐 @ali 私有包,自身几乎无业务逻辑,作用是在 package.json 里声明对 smart-config-manager 的依赖,触发整条恶意子树的安装。
桥接层由 smart-config-manager 承担,它把诱饵包连接到真正含加载逻辑的底层包,例如 cloud-config-fetcher、fast-transform-pipeline。单个底层包看起来依赖关系并无异常,组合在一起才构成完整的加载链。这种化整为零的设计,刻意绕开只看顶层依赖的简易扫描。
执行层的核心机制是:某个底层包从GitHub仓库拉取规则引擎配置,再借助 Node.js 内置的 vm 模块在沙箱里执行动态脚本,按操作系统分支下载第二阶段载荷。下载域名 aone-cli-next.oss-cn-beijing.aliyuncs.com 伪装成阿里OSS地址,便于在流量侧混入正常业务。
仿冒包触发安装、桥接层展开子树、底层包拼接 loader,三个阶段按从左到右顺序衔接。攻击者把拉规则、判系统、下载荷拆进多个包的postinstall脚本和require 链,而不是集中写入单个文件,传统关键字扫描因此更容易漏报。
维护者账号 ch4ce 在 npmjs 上已显示不存在,但历史版本仍可被锁文件引用。lib-mtop 最早于2023年11月以空包形式出现,2026年3月至4月连续推送 v1.0.1、v1.0.2、v1.0.3 三个恶意版本。目前尚不确定是账号被接管还是维护者主动投毒,对企业防守而言,处置流程相同:凡命中名单版本,一律按失陷处理。
攻击细节分析
规则引擎跑起来之后,载荷获取与持久化按操作系统分流,行为差异明显,也决定了取证时应检查的位置不同。
在 Windows 上,恶意代码会尝试结束Alilang企业安全与办公客户端进程,并用篡改后的版本替换其核心文件。Alilang承担 VPN、终端安全、办公入口等职能,被替换意味着攻击者可以在员工日常必开的软件里长期驻留,而不必再依赖 npm 安装脚本。
在 Linux 上,二进制载荷下载到 /tmp 目录,以detached方式启动后从磁盘删除,典型无文件落地手法。内存里跑的进程若不与npm生命周期绑定,卸载 node\_modules无法清除。
在 macOS 上,攻击者向 ~/.zshrc 写入恶意后台脚本,并注册10分钟触发一次的Launch Agent。某安全团队在开发者笔记本上见到的现象是:终端每次打开都会多一行来源不明的source命令,而npm缓存里已找不到对应包目录。
最终载荷是一个功能完整的远程控制后门,支持远程命令执行、任意文件上传下载、主机侦察、载荷暂存与内网横向移动。更棘手的是持久化方式:后门会向钉钉、悟空、Qoder 等常见企业协作应用注入恶意代码,把日常办公软件变成二次加载点。这意味着即使重装了 Node 环境,只要协作客户端仍被污染,攻击者就能卷土重来。
研究人员在源码里看到中文注释,GitHub 提交时间戳多为UTC+08:00,结合目标工具链,普遍判断为中文攻击者发起的定向工业间谍行动。公开披露的完整恶意包名单如下,任何一条出现在锁文件或 node\_modules 里,都应视为高风险:
| 包名 | 层级角色 |
| lib-mtop | 仿冒 @ali/lib-mtop 的诱饵包 |
| aone-kit / aone-kit-cli / aone-sandbox | 仿冒 Aone 工具链的空壳包 |
| local-config-parser | 表面合法的JSON解析器,实际串联恶意依赖 |
| smart-config-manager | 桥接层,连接诱饵与loader |
| cloud-config-fetcher / fast-transform-pipeline | 含loader片段的底层包 |
| aone-cloud-cli / colder-cli / def-open-client 等 | 扩展诱饵与载荷投递 |
| feedback-ai-sdk / flight-compare-analyzer / lwp-web-client等 | 同一攻击活动的配套包 |
| open-worker-cli/test-skill-zip/uniapi-bridge | 同一攻击活动的配套包 |
中招排查需覆盖lock文件全量依赖,npm list 只能看到顶层包,深层传递依赖必须靠完整lock文件检索。导出 package-lock.json 或 pnpm-lock.yaml,对照上表包名及 ch4ce 维护者字段;开发机与CI构建节点同步做终端检查,Windows核对Alilang文件哈希,macOS检查Launch Agent 与 ~/.zshrc,Linux检查 /tmp 历史异常进程及 cron、systemd 用户级服务。lock文件或node\_modules 中出现上述任一包名,按已失陷处理,在干净机器上轮换密钥,再对受影响主机完成取证。
投毒污染检测
以下代码可对投毒污染的代码进行检测:
#!/usr/bin/env python3# -*- coding: utf-8 -*-\"\"\"Scan package-lock.json / package.json for Alibaba npm RAT campaign indicators.\"\"\"
from __future__ import annotations
import argparseimport jsonimport reimport sysfrom dataclasses import asdict, dataclass, fieldfrom pathlib import Pathfrom typing import Any
@dataclassclass Finding: severity: str category: str package: str detail: str
@dataclassclass ScanReport: target: str findings: list[Finding] = field(default_factory=list) packages_scanned: int = 0
@property def overall(self) -> str: if any(f.severity in (\"critical\", \"warning\") for f in self.findings): return \"FAIL\" return \"PASS\"
def to_dict(self) -> dict[str, Any]: return { \"target\": self.target, \"packages_scanned\": self.packages_scanned, \"overall\": self.overall, \"finding_count\": len(self.findings), \"findings\": [asdict(f) for f in self.findings], }
def load_rules(rules_path: Path) -> dict[str, Any]: with rules_path.open(encoding=\"utf-8\") as fh: return json.load(fh)
def collect_package_names(manifest: dict[str, Any]) -> set[str]: names: set[str] = set() for section in (\"dependencies\", \"devDependencies\"): for name in (manifest.get(section) or {}): names.add(name) names.add(name.split(\"/\")[-1]) lock = manifest.get(\"packages\") or {} if isinstance(lock, dict): for key in lock: if key.startswith(\"node_modules/\"): pkg = key.split(\"node_modules/\")[-1] names.add(pkg) names.add(pkg.split(\"/\")[-1]) return names
CAMPAIGN_TAG = \"Alibaba-NPM-2026\"
def threat_detail(category: str, **ctx: str) -> str: base = ctx.get(\"base\", \"\") scope = ctx.get(\"scope\", \"\") if category == \"known_malicious_package\": return ( f\"Trojan/JS.SupplyChain.{base}!AlibabaRAT \" f\"[行为:供应链投毒/跨平台后门, 活动:{CAMPAIGN_TAG}, 级别:高危]\" ) if category == \"scope_impersonation\": return ( f\"HEUR:Trojan/NPM.Typosquat.{base}!ScopeConfusion \" f\"[仿冒:{scope}, 风险:作用域混淆/私有包名冲突, 活动:{CAMPAIGN_TAG}]\" ) return f\"HEUR:Trojan/JS.Generic!Unknown [活动:{CAMPAIGN_TAG}]\"
def scan_package_names(names: set[str], rules: dict[str, Any]) -> list[Finding]: findings: list[Finding] = [] malicious = set(rules.get(\"malicious_packages\", [])) for pkg in sorted(names): base = pkg.split(\"/\")[-1] if base in malicious: findings.append(Finding( severity=\"critical\", category=\"known_malicious_package\", package=pkg, detail=threat_detail(\"known_malicious_package\", base=base), )) for scope in rules.get(\"impersonated_scopes\", []): private_name = scope.split(\"/\")[-1] for pkg in names: if pkg.split(\"/\")[-1] == private_name and not pkg.startswith(\"@ali/\"): findings.append(Finding( severity=\"warning\", category=\"scope_impersonation\", package=pkg, detail=threat_detail(\"scope_impersonation\", base=pkg.split(\"/\")[-1], scope=scope), )) return findings
def scan_target(target: Path, rules: dict[str, Any]) -> ScanReport: report = ScanReport(target=str(target)) names: set[str] = set() for fname in (\"package.json\", \"package-lock.json\"): path = target / fname if path.is_file(): names |= collect_package_names(json.loads(path.read_text(encoding=\"utf-8\"))) report.packages_scanned = len(names) report.findings = scan_package_names(names, rules) return report
def main() -> int: parser = argparse.ArgumentParser() parser.add_argument(\"--project\", type=Path, default=Path(\".\")) parser.add_argument(\"--rules\", type=Path, default=Path(__file__).with_name(\"known_packages.json\")) parser.add_argument(\"--strict\", action=\"store_true\") args = parser.parse_args() report = scan_target(args.project.resolve(), load_rules(args.rules.resolve())) print(json.dumps(report.to_dict(), ensure_ascii=False, indent=2)) return 1 if args.strict and report.overall == \"FAIL\" else 0
if __name__ == \"__main__\": sys.exit(main())
扫描的典型输出如下:
{ \"target\": \".../demo_project\", \"packages_scanned\": 3, \"overall\": \"FAIL\", \"finding_count\": 3, \"findings\": [ { \"severity\": \"critical\", \"category\": \"known_malicious_package\", \"package\": \"lib-mtop\", \"detail\": \"Trojan/JS.SupplyChain.lib-mtop!AlibabaRAT [行为:供应链投毒/跨平台后门, 活动:Alibaba-NPM-2026, 级别:高危]\" } ]}
总结
npm供应链投毒正在从单个恶意包演进为多层依赖拆分、跨平台持久化、办公软件注入的组合拳。这次针对阿里开发工具用户的攻击说明,攻击者已经熟悉企业内部包命名习惯,并会把恶意逻辑藏进传递依赖,专门对付只看顶层依赖清单的团队。
日常防护要点包括:package.json写全@ali 作用域; lock文件变更合并前必审; npm安装走内网私服代理; 终端与协作软件按已失陷前提定期核查。lock文件或开发机命中上述恶意包时,该环境生产密钥须立即停用,完成取证与隔离后轮换凭据,再在干净基线上重建开发环境。
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。










评论