一、背景
近期慢雾安全团队收到一位 Fomo 用户的求助。用户反馈,他在 Fomo 网页端查看 MOONLET 代币详情时,点击了页面展示的项目官网入口,进入 voltage.family,并按提示完成“人机验证”,随后发现账户内约 4.8 万美元资产被转走。整个过程中,他没有连接钱包、进行链上授权,也没有输入助记词或私钥。该用户通过 Google 登录 Fomo,称账号未开启二次验证。
接到反馈后,慢雾安全团队对相关页面及脚本展开分析,发现所谓的“人机验证”会引导用户添加并点击恶意书签。书签内嵌 JavaScript 代码,目标是在 Fomo 页面中读取浏览器存储、提取登录令牌,并将数据发送至外部地址。
本文围绕钓鱼页面和脚本还原攻击过程,重点分析恶意书签如何窃取嵌入式钱包的访问凭证。

(钓鱼站点首页伪装成交易产品页面,并设置访问入口)
MistEye 响应
MistEye 是由 SlowMist 自主研发的 Web3 威胁情报与动态安全监控系统,集成了安全监控与情报聚合能力,为用户提供实时的风险预警与资产守护。
MistEye 已第一时间通过情报推送与客户告警通道同步风险。

二、钓鱼分析
钓鱼入口来源
根据用户补充的截图,当时他正在查看 Fomo 上的 MOONLET 代币详情页。红箭头标出了代币名称旁的官网入口,右侧项目介绍区域也有“官网”按钮。结合用户反馈,本次访问钓鱼站点的入口来自该代币详情页展示的外部链接。

(Fomo 的 MOONLET 代币详情页及项目官网入口)
伪造人机验证
用户访问的站点为 voltage.family。进入 /app 页面后,页面展示九个图标,要求用户找到“四条腿的动物”,将其拖到浏览器书签栏,再连续点击该书签三次。拖拽时,页面还会显示操作引导,让用户误以为这是正常验证步骤。

(伪造的人机验证页面)

(页面引导用户将图标拖入浏览器书签栏)
最终添加的书签名为 Triple click to verify。它保存的并非普通网页地址,而是一段以 开头的代码,即 bookmarklet。用户点击这种书签时,代码会在当前页面中执行;攻击者正是借此诱导用户在已登录的 Fomo 页面运行恶意脚本。
这种通过恶意书签在目标页面执行 JavaScript 的攻击方式并不陌生。此前,慢雾安全团队也曾分析过一起 Discord Token 窃取案例,详见《慢雾:揭露浏览器恶意书签如何盗取你的 Discord Token》。
恶意书签投递
页面上的动物图标会随机出现,但用户选哪个图标并不影响最终添加的恶意书签内容。fomo-track.js 读取页面内嵌的 BMCODE,替换访客标识后,将所有 a.mini-bookmark 元素的 href 设置为同一段书签代码(以下为样本原文,已省略外层取值与判空逻辑):

替换动作会在页面 DOM 变化、定时器触发、鼠标按下和拖拽开始时反复执行,以覆盖页面原本的书签链接。正常加载页面时,九个图标均指向这段代码。所谓“识别动物”,实际是在为恶意书签的投递提供借口。

(书签实际内容为 javascript 代码)
验证后的 /app/live 页面继续模仿交易终端。界面注明市场数据为模拟数据,连接钱包按钮仅弹出提示。结合前面的引导过程,这些页面主要是为了把钓鱼站点伪装得更可信。

(验证后展示的模拟交易终端)
本地数据采集
书签代码压缩后有八千多字符。最外层使用 location.hostname.includes("fomo.family") 判断当前页面主机名,符合条件才继续执行。这里用的是字符串包含判断,而不是严格域名匹配,因此凡主机名中包含 fomo.family 的页面都可能满足条件,不能理解为只会在 fomo.family 及其合法子域下执行。
这也说明,单纯打开钓鱼网站并不等于已经执行了后续采集逻辑。脚本需要在符合条件的页面上下文中运行,才能读取该页面可访问的存储。
采集函数 v() 首先遍历 localStorage 和 sessionStorage。它按键名匹配 privy、wallet、share、secret、seed、mnemon、turnkey、dynamic、fomo、mfa 等关键词;键名未命中时,再检查值的前 300 个字符。命中的数据最多保留 4000 个字符,存入对象 o。

随后,脚本调用 indexedDB.databases() 获取当前上下文可访问的数据库,逐一遍历 object store,通过 getAll() 读取记录,并按“库名/表名”保存。该流程设置了 4 秒超时,避免单个采集步骤长时间阻塞。
采集范围同时覆盖登录凭证和钱包相关数据。在嵌入式钱包场景下,这些数据可能涉及钱包访问、恢复和密钥材料;一旦泄露,攻击者可能进一步控制钱包。至于实际泄露了哪些数据,要看脚本真正采集到的内容;仅凭关键词匹配,不能认定脚本已经取得完整私钥。
登录凭证提取
脚本优先查找 privy:token 和 privy:refresh_token,并通过 y() 处理可能包裹在 JSON 中的 token 或 accessToken 字段。如果没有找到固定键名,则逐步放宽匹配范围,查找其他 token 字段,同时排除 id_token、identity 等不属于目标令牌的字段。最后,脚本还会尝试从值中提取以 eyJ 开头的 JWT 片段。该兜底方式只能选出疑似令牌,并不保证结果有效。
TOTP 注册
取得访问令牌后,脚本携带 Bearer 令牌、Privy 应用 ID 和客户端 ID,请求 https://auth.privy.io/api/v1/users/me,并读取响应中的 mfa_methods(或 mfaMethods)字段,以判断账号已绑定的二次验证方式;该数组是否为空,直接决定后续 MFA 分支是否执行。脚本同时包含返回 401 时借助刷新令牌重试请求的处理。
脚本还包含收到 401 后向 /sessions 提交刷新令牌的逻辑:以 refresh_token 为请求体,代码先对响应调用 .json() 解析,再取其中的 token 字段作为新的访问令牌,取得后重新请求 /users/me(在 MFA 事件通道中捕获到 unauthorized/401 时,也走同一条刷新路径重试)。
与 MFA 相关的代码会创建一个 4×4 像素、透明度为 0.01 的 iframe,加载 Privy 的 embedded-wallets 页面,再通过 postMessage 发送事件请求。消息按随机请求 ID 对应,默认等待 6 秒;就绪检查以 100 毫秒为间隔,最多等待 8 秒。
脚本尝试以 TOTP 为方法初始化注册,并从响应中提取 secret 或 toptSecret。若取得密钥,它会在本地按 30 秒时间步长、HMAC-SHA1 和动态截断计算六位验证码,再提交注册请求。该逻辑的意图是将攻击者掌握的 TOTP 密钥用于账户验证。
这里需要区分“发起注册”与“注册成功”。初始化返回密钥,不代表服务端已经完成绑定;脚本将包含 already、exist、enrolled、duplicate 等字样的错误视为可继续处理,也不能证明新密钥已生效。
代码中,这段 MFA 逻辑的触发条件很明确:只有当 /users/me 返回的 mfa_methods 非空,也就是账号已绑定至少一种二次验证方式时,脚本才会创建 iframe、调用 privy:mfa:init-enrollment 获取 TOTP 密钥并提交注册;如果该数组为空,整段 MFA 逻辑都不会执行。用户称其账号原先未开启二次验证,因此这部分很可能没有触发。但这不影响本地数据和凭证的采集、外传,因为 ls 与 idb 是独立通道。另需注意,发起注册不等于注册成功,是否真正绑定仍需服务端记录确认。
登录凭证泄露如何危及钱包资产
本次攻击针对的是用户已登录的 Fomo 页面及其嵌入式钱包访问凭证。恶意书签执行后,会采集 Privy 访问令牌、刷新令牌及钱包相关本地数据,并发送至攻击者控制的地址。这些凭证涉及钱包的访问与恢复流程,泄露后可能被用于接管钱包,进一步导出私钥或发起交易。因此,即使用户没有主动连接钱包、签署链上授权或输入助记词,也可能面临资产被盗的风险。当前展示的脚本主要负责采集和外传,后续具体采用哪条路径完成资产转移,还需结合进一步的调查结果确认。
数据外传
发送前,脚本把采集结果组织为 ls、idb 和 mfa 三部分,分别对应 localStorage、IndexedDB 记录以及 MFA 方法和处理结果。,再进行 base64url 编码。编码后超过 11000 个字符时,脚本先删减 IndexedDB 记录,保留与 privy、share、wallet、key、secret 相关的内容;若仍超限,则将本地存储部分缩减为 privy:token、privy:refresh_token 和 mfa-session-storage 三个关键字段。流程另设 20 秒兜底发送,避免一直等待采集结束。
外传目标为 noisy-heart-5856.zmfkc29.workers.dev。脚本采用两种方式发送:一是在取得 TOTP 初始化密钥后,通过 fetch 提前发送,密钥放在 initsecret 参数中;二是通过 location.href 跳转,把令牌、采集数据以及脚本保留的 TOTP 密钥放入 URL 参数。
- t:访问令牌;
- r:刷新令牌;
- vid:访客标识;
- site:站点标识;
- voltageIs:经 base64url 编码的采集数据;
- intisect:初始化阶段取得的 TOTP 密钥;
- totp:脚本最终保留的 TOTP 密钥。
对该地址进行非破坏性测试时,根目录、未知路径以及携带凭证参数的请求都返回 302,并跳转回 voltage.family;POST /visit 返回 204。该响应符合“接收数据后把用户送回钓鱼站点”的设计,但仅凭响应本身无法确认服务端是否保存了数据。
fomo-track.js 开头还保留了一段注释,描述 tracker → Cloudflare Worker → panel → Telegram 的数据链路,并称复用了旧验证页的中继。这属于样本作者留下的线索,尚未获得后台面板或 Telegram 侧证据支持。
/* voltage tracker -> cloudflare worker -> panel -> telegram
(same relay the old captcha page uses; no keys in this file) */
综合页面和代码,可以还原出诱导添加书签、在目标页面执行脚本、采集存储、提取令牌、尝试 MFA 操作及外传数据的链路。凭证是否被实际重放、后续交易由谁发起,仍需结合服务端日志和链上记录核实。
四、IOC
钓鱼页面与数据外传
hxxps://voltage[.]family/app
hxxps://voltage[.]family/app/live
noisy-heart-5856[.]zmfkc29[.]workers[.]dev
样本 SHA256
index.html
24df225d70e399fa30a2d0761e8645e339d99a0f751af6f9e5e00ab555e2fa07
fomo-track.js
6d771096e442b36b0719a4e07b19c62a5a985005e8a07ab5d50dd6ca59c4ad06
VerifyRoute
d5576bb50cb299d0c48657ed0cfd3f03b4c2313db8b37abcee57f779d70f7f18
LiveTerminal
193b305baf9548a6dded341f3dd613a811e4604c7dfa4447219664f6c43818f8
五、总结
本次分析发现,攻击者将恶意 JavaScript 包装成浏览器书签,并用“人机验证”引导用户在已登录的 Fomo 页面执行。其目标包括 Privy 登录凭证和钱包相关本地数据,风险涉及嵌入式钱包控制权及资产安全。用户没有主动输入私钥或签署链上授权,并不意味着钱包未受影响。
从样本看,攻击者通过恶意书签采集并外传凭证,为后续接管钱包提供条件。用户反馈损失约 4.8 万美元;本次事件最终通过私钥导出还是钱包签名等方式完成转移,仍需进一步核实。
给用户的建议
正常的人机验证不需要把网页图标拖进书签栏,再到钱包或交易页面点击执行。遇到这类要求,应立即停止操作。定期检查书签栏,删除来历不明的 书签,并为重要账号开启平台支持的多因素验证。
如果已经执行过可疑书签,应从可信设备访问官方渠道,优先撤销异常会话和相关授权,检查并移除陌生的验证器,同时联系平台协助处置。通过 Google 等第三方身份登录的用户,也应检查对应身份账号的安全状态。不要只依赖修改密码,现有会话和刷新令牌是否同步失效,需要以平台的实际机制为准。
如有证据表明助记词或私钥已经泄露,应使用全新生成的钱包转移剩余资产。保留可疑页面地址、书签内容、操作时间和交易记录,有助于后续调查。
给项目方的建议
对 MFA 新增、替换、恢复以及资金转移等敏感操作设置独立的验证和风控要求,避免只凭已有会话就完成安全设置变更。向用户提供会话查看与撤销入口,对异常设备、来源和 MFA 变更及时告警,并确保服务端能够按需撤销相关凭证。
同时检查客户端存储的敏感数据范围,减少长期有效凭证的暴露。对外链、用户生成内容及站内引导加强审查,明确提醒用户不要在已登录页面执行陌生书签或代码。若发现类似页面,可保留线索并向慢雾安全团队反馈。
关于 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-skillsAI 编码助手安全技能包,在依赖安装与 URL 访问前自动触发 MistEye 安全检测
🛠️ MistEye-DNS-Guard:https://github.com/slowmist/MistEye-DNS-GuardDNS 安全防护工具,检测恶意域名与风险访问,识别钓鱼、C2 等网络威胁
本文由 SlowMist 威胁情报团队结合 MistEye 威胁情报系统、SlowMist Agent AI 驱动分析编写,有任何问题欢迎咨询反馈。