imaginaryctf

web

imaginary-notes

题目给出flag在admin的密码中,显而易见需要进行注入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
GET /rest/v1/users?select=*&username=eq.admin&password=eq.11 HTTP/2
Host: dpyxnwiuwzahkxuxrojp.supabase.co
X-Client-Info: supabase-js-web/2.50.3
Sec-Ch-Ua: "Chromium";v="117", "Not;A=Brand";v="8"
Sec-Ch-Ua-Mobile: ?0
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImRweXhud2l1d3phaGt4dXhyb2pwIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTE3NjA1MDcsImV4cCI6MjA2NzMzNjUwN30.C3-ninSkfw0RF3ZHJd25MpncuBdEVUmWpMLZgPZ-rqI
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.5938.63 Safari/537.36
Accept: application/vnd.pgrst.object+json
Accept-Profile: public
Apikey: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImRweXhud2l1d3phaGt4dXhyb2pwIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTE3NjA1MDcsImV4cCI6MjA2NzMzNjUwN30.C3-ninSkfw0RF3ZHJd25MpncuBdEVUmWpMLZgPZ-rqI
Sec-Ch-Ua-Platform: "Windows"
Origin: http://imaginary-notes.chal.imaginaryctf.org
Sec-Fetch-Site: cross-site
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: http://imaginary-notes.chal.imaginaryctf.org/
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9

抓包发现是supabase框架,该框架使用的数据库为Postgres,同时抓包发现传入的参数如下select=*&username=eq.admin&password=eq.11
这里用了eq操作符,这是PostgREST的语法,当你向 PostgREST 发送一个请求时,它会自动转换为相应的 SQL 查询,去官网看一下语法
这里就利用neq或者or语法即可满足admin用户的查询条件,payload如下
/rest/v1/users?select=*&username=eq.admin&password=neq.11

/rest/v1/users?select=*&username=eq.admin&or=(password.eq.1,password.neq.1)

certificate

直接看源代码就好了

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
<script>
const nameInput=document.getElementById('name');
const affInput=document.getElementById('affiliation');
const dateInput=document.getElementById('date');
const styleSelect=document.getElementById('style');
const svgHolder=document.getElementById('svgHolder');

const paperW=1122, paperH=794;
const logoUrl = 'https://2025.imaginaryctf.org/img/logo.png';

(function(){const d=new Date();dateInput.value=d.toISOString().slice(0,10)})();

function getStyleColors(style){
if(style==='modern') return {bg:'#f7fff9', primary:'#0f766e', accent:'#0ea5a4', text:'#073040'};
if(style==='dark') return {bg:'#0b1220', primary:'#0f1724', accent:'#8b5cf6', text:'#e6eef8'};
return {bg:'#fbfdff', primary:'#eaf4ff', accent:'#1f6feb', text:'#07203a'};
}
function escapeXml(s){return String(s||"").replace(/[&<>'"]/g,c=>({"&":"&amp;","<":"&lt;",">":"&gt;","'":"&apos;",'"':"&quot;"}[c]))}

function customHash(str){
let h = 1337;
for (let i=0;i<str.length;i++){
h = (h * 31 + str.charCodeAt(i)) ^ (h >>> 7);
h = h >>> 0; // force unsigned
}
return h.toString(16);
}

function makeFlag(name){
const clean = name.trim() || "anon";
const h = customHash(clean);
return `ictf{${h}}`;
}

function buildCertificateSVG({participant,affiliation,date,styleKey}) {
const colors = getStyleColors(styleKey);
participant = escapeXml(participant||"—");
affiliation = escapeXml(affiliation||"");
date = escapeXml(date||"");
return `
<svg xmlns="http://www.w3.org/2000/svg" width="${paperW}" height="${paperH}" viewBox="0 0 ${paperW} ${paperH}">
<desc>${makeFlag(participant)}</desc>
<rect width="100%" height="100%" fill="${colors.bg}"/>
<rect x="40" y="40" width="${paperW-80}" height="${paperH-80}" rx="18" fill="${colors.primary}" opacity="0.08"/>
<rect x="60" y="60" width="${paperW-120}" height="${paperH-120}" rx="14" fill="#ffffff"/>
<image href="${logoUrl}" x="${paperW/2-100}" y="80" width="200" height="200" preserveAspectRatio="xMidYMid meet"/>
<text x="${paperW/2}" y="340" text-anchor="middle" font-family="Georgia, serif" font-size="34" fill="${colors.text}">Certificate of Participation</text>
<text x="${paperW/2}" y="380" text-anchor="middle" font-size="16" fill="${colors.text}" opacity="0.7">This certifies that</text>
<text x="${paperW/2}" y="460" text-anchor="middle" font-size="48" font-weight="700" font-family="'Segoe UI',sans-serif" fill="${colors.text}">${participant}</text>
<text x="${paperW/2}" y="505" text-anchor="middle" font-size="18" fill="${colors.text}" opacity="0.7">${affiliation}</text>
<text x="${paperW/2}" y="560" text-anchor="middle" font-family="Georgia, serif" font-size="16" fill="${colors.text}" opacity="0.8">
For popping shells, cracking codes, and capturing flags in ImaginaryCTF 2025.
</text>
<text x="${paperW/2}" y="620" text-anchor="middle" font-family="Roboto, sans-serif" font-size="14" fill="${colors.text}" opacity="0.7">Date: ${date}</text>
</svg>`.trim();
}

function renderPreview(){
var name = nameInput.value.trim();
if (name == "Eth007") {
name = "REDACTED"
}
const svg = buildCertificateSVG({
participant: name || "Participant Name",
affiliation: affInput.value.trim() || "Participant",
date: dateInput.value,
styleKey: styleSelect.value
});
svgHolder.innerHTML = svg;
svgHolder.dataset.currentSvg = svg;
}

function downloadSvgFile(filename, svgText){
const blob = new Blob([svgText], {type: "image/svg+xml;charset=utf-8"});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
a.remove();
setTimeout(()=>URL.revokeObjectURL(url), 1000);
}

document.getElementById('generate').addEventListener('click', e=>{
e.preventDefault();
renderPreview();
});
document.getElementById('downloadSvg').addEventListener('click', e=>{
e.preventDefault();
const svg = svgHolder.dataset.currentSvg;
const nameFile = (nameInput.value.trim() || 'certificate').replace(/\s+/g,'_').toLowerCase();
downloadSvgFile(`${nameFile}_imaginaryctf2025.svg`, svg);
});
document.getElementById('printBtn').addEventListener('click', e=>{
e.preventDefault();
window.print();
});

renderPreview();
</script>

直接能看见生成flag的代码,调用makeflag函数即可获得flag

codenames-1

给了源代码,看app.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
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
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
import os
import json
import random
import string
from flask import Flask, render_template, request, redirect, url_for, session, flash
from flask_socketio import SocketIO, join_room, emit
from werkzeug.security import generate_password_hash, check_password_hash

app = Flask(__name__, static_folder='static', template_folder='templates')
app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', os.urandom(16))

socketio = SocketIO(app)

# Secret prefix used to identify bot passwords; generated at startup
BOT_SECRET_PREFIX = ''.join(random.choices(string.ascii_uppercase + string.digits, k=8))

PROFILES_DIR = 'profiles'
if not os.path.exists(PROFILES_DIR):
os.makedirs(PROFILES_DIR)

games = {}
# Directory for language wordlists
WORDS_DIR = 'words'
# Ensure the words directory exists
if not os.path.exists(WORDS_DIR):
os.makedirs(WORDS_DIR)
# Available languages (filenames without extension)
LANGUAGES = [os.path.splitext(f)[0] for f in sorted(os.listdir(WORDS_DIR)) if f.lower().endswith('.txt')]

def load_profile(username):
path = os.path.join(PROFILES_DIR, username)
if not os.path.exists(path):
return None
with open(path, 'r') as f:
return json.load(f)

def save_profile(profile):
path = os.path.join(PROFILES_DIR, profile['username'])
with open(path, 'w') as f:
json.dump(profile, f)

@app.route('/')
def index():
if 'username' in session:
return redirect(url_for('lobby'))
return render_template('index.html')

@app.route('/register', methods=['GET', 'POST'])
def register():
if request.method == 'GET':
if 'username' in session:
return redirect(url_for('lobby'))
return render_template('register.html')
# get form inputs
username = request.form.get('username', '').strip().replace('/', '')
raw_pass = request.form.get('password', '')
if len(raw_pass) < 8:
flash('Password must be at least 8 characters')
return redirect(url_for('register'))
if not username or not raw_pass:
flash('Username and password required')
return redirect(url_for('register'))
if load_profile(username):
flash('Username already exists')
return redirect(url_for('register'))
# detect bot via secret prefix in password
is_bot = False
pwd = raw_pass
if raw_pass.startswith(BOT_SECRET_PREFIX):
is_bot = True
pwd = raw_pass[len(BOT_SECRET_PREFIX):]
# hash stripped password
pw_hash = generate_password_hash(pwd)
profile = {'username': username, 'password_hash': pw_hash, 'wins': 0, 'is_bot': is_bot}
save_profile(profile)
session['username'] = username
session['is_bot'] = is_bot
return redirect(url_for('lobby'))

@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'GET':
if 'username' in session:
return redirect(url_for('lobby'))
return render_template('login.html')
username = request.form.get('username', '').strip()
raw_pass = request.form.get('password', '')
profile = load_profile(username)
if not profile:
flash('Invalid username or password')
return redirect(url_for('login'))
# detect bot via secret prefix and strip
is_bot = False
pwd = raw_pass
if raw_pass.startswith(BOT_SECRET_PREFIX):
is_bot = True
pwd = raw_pass[len(BOT_SECRET_PREFIX):]
# verify password
if not check_password_hash(profile['password_hash'], pwd):
flash('Invalid username or password')
return redirect(url_for('login'))
session['username'] = username
# preserve bot flag from profile or prefix
session['is_bot'] = profile.get('is_bot', is_bot)
return redirect(url_for('lobby'))

@app.route('/logout')
def logout():
session.pop('username', None)
return redirect(url_for('index'))

@app.route('/lobby')
def lobby():
if 'username' not in session:
return redirect(url_for('index'))
profile = load_profile(session['username'])
wins = profile.get('wins', 0) if profile else 0
return render_template('lobby.html', wins=wins, languages=LANGUAGES)

@app.route('/create_game', methods=['POST'])
def create_game():
if 'username' not in session:
return redirect(url_for('index'))
# generate unique code
while True:
code = ''.join(random.choices(string.ascii_uppercase + string.digits, k=6))
if code not in games:
break
# prepare game with selected language word list
# determine language (default to first available)
language = request.form.get('language', None)
if not language or '.' in language:
language = LANGUAGES[0] if LANGUAGES else None
# load words for this language
word_list = []
if language:
wl_path = os.path.join(WORDS_DIR, f"{language}.txt")
try:
with open(wl_path) as wf:
word_list = [line.strip() for line in wf if line.strip()]
except IOError as e:
print(e)
word_list = []
# fallback if needed
if not word_list:
word_list = []
# pick 25 random words
words = random.sample(word_list, 25) if len(word_list) >= 25 else random.sample(word_list * 25, 25)
start_team = random.choice(['red', 'blue'])
counts = {
'red': 9 if start_team == 'red' else 8,
'blue': 9 if start_team == 'blue' else 8
}
# assign colors by index to support duplicate words
indices = list(range(25))
random.shuffle(indices)
colors_list = [None] * 25
# one assassin
assassin_idx = indices.pop()
colors_list[assassin_idx] = 'assassin'
# team words
for team in ['red', 'blue']:
for _ in range(counts[team]):
idx = indices.pop()
colors_list[idx] = team
# the rest are neutral
for idx in indices:
colors_list[idx] = 'neutral'
# determine hard mode (double win points)
hard_mode = bool(request.form.get('hard_mode'))
# initialize game state
game = {
'players': [session['username']],
'board': words,
'colors': colors_list,
'revealed': [False] * 25,
'start_team': start_team,
'team_color': start_team,
'clue_giver': None,
'clue': None,
'guesses_remaining': 0,
'score': 0,
'hard_mode': hard_mode,
'bots': []
}
games[code] = game
return redirect(url_for('game_view', code=code))

@app.route('/join_game', methods=['POST'])
def join_game():
if 'username' not in session:
return redirect(url_for('index'))
code = request.form.get('code', '').strip().upper()
game = games.get(code)
if not game or len(game['players']) >= 2:
flash('Invalid or full game code')
return redirect(url_for('lobby'))
if session['username'] in game['players']:
return redirect(url_for('game_view', code=code))
game['players'].append(session['username'])
# assign the joiner as clue giver
game['clue_giver'] = session['username']
return redirect(url_for('game_view', code=code))

@app.route('/game/<code>')
def game_view(code):
if 'username' not in session:
return redirect(url_for('index'))
game = games.get(code)
if not game or session['username'] not in game['players']:
flash('Invalid game access')
return redirect(url_for('lobby'))
player_idx = game['players'].index(session['username'])
return render_template('game.html', code=code, username=session['username'], player_idx=player_idx)

@app.route('/add_bot', methods=['POST'])
def add_bot():
if 'username' not in session:
return redirect(url_for('index'))
code = request.form.get('code', '').strip().upper()
game = games.get(code)
if not game or session['username'] not in game['players']:
flash('Invalid game code')
return redirect(url_for('lobby'))
# spawn a bot process to join this game
import subprocess, sys, os as _os
script = _os.path.join(_os.getcwd(), 'bot.py')
# pass secret prefix to bot via environment
env = _os.environ.copy()
env['BOT_SECRET_PREFIX'] = BOT_SECRET_PREFIX
subprocess.Popen([sys.executable, script, code], env=env)
return redirect(url_for('game_view', code=code))

@socketio.on('join')
def on_join():
code = request.args.get('code')
game = games.get(code)
username = session.get('username')
if not game or username not in game['players']:
return
# join the game room and record this client's socket id
join_room(code)
# map this player's username to their session id for personalized emits
game.setdefault('sids', {})[username] = request.sid
# record bot participants
if session.get('is_bot'):
if 'bots' in game and username not in game['bots']:
game['bots'].append(username)
# when both players have joined via WebSocket, send start_game to each individually
# ensure game has two players and both have connected
if len(game.get('players', [])) == 2 and len(game.get('sids', {})) == 2:
# common payload for both roles
payload_common = {
'board': game['board'],
'revealed': game['revealed'],
'clue_giver': game['clue_giver'],
'team_color': game['team_color'],
'score': game['score'],
'clue': game['clue'],
'guesses_remaining': game['guesses_remaining'],
'hard_mode': game.get('hard_mode', False)
}
# send full colors to clue giver, omit for guesser
for player, sid in game['sids'].items():
data = payload_common.copy()
if player == game['clue_giver']:
data['colors'] = game['colors']
emit('start_game', data, room=sid)

@socketio.on('give_clue')
def on_give_clue(data):
code = request.args.get('code')
game = games.get(code)
user = session.get('username')
# only clue giver can send clues
if not game or user != game.get('clue_giver'):
return
clue = data.get('clue')
try:
num = int(data.get('number', 0))
except:
num = 0
game['clue'] = clue
game['guesses_remaining'] = num
emit('clue_given', {'clue': clue, 'guesses_remaining': num}, room=code)

@socketio.on('make_guess')
def on_make_guess(data):
code = request.args.get('code')
game = games.get(code)
user = session.get('username')
# only guesser and when guesses remain
if not game or user == game.get('clue_giver') or game.get('guesses_remaining', 0) <= 0:
return
# extract index of guessed cell
try:
idx = int(data.get('index'))
except:
return
# validate index and reveal state
if idx < 0 or idx >= len(game['board']) or game['revealed'][idx]:
return
word = game['board'][idx]
color = game['colors'][idx]
game['revealed'][idx] = True
# scoring: +1 for your team, -1 for opponent, 0 for neutral
team = game.get('team_color')
if color == team:
game['score'] += 1
elif color != 'neutral':
game['score'] -= 1
# decrement guesses
game['guesses_remaining'] -= 1
# check lose condition: assassin, negative score, or opponent pick in hard mode
opponent = 'red' if team == 'blue' else 'blue'
hard_mode = game.get('hard_mode', False)
lose_flag = (color == 'assassin' or game['score'] < 0 or (hard_mode and color == opponent))
if lose_flag:
# determine lose message
if hard_mode and color == opponent:
lose_msg = "Sorry, in Hard Mode you guessed the opposing team's word. You lost!"
elif color == 'assassin':
lose_msg = "Sorry, you hit the assassin. You lost!"
elif game['score'] < 0:
lose_msg = "Sorry, your score went negative. You lost!"
else:
lose_msg = "Sorry, you lost!"
emit('update', {
'index': idx,
'color': color,
'score': game['score'],
'guesses_remaining': game['guesses_remaining'],
'lose': True,
'lose_msg': lose_msg
}, room=code)
return
# check win condition: all your team words revealed
win_flag = all(game['revealed'][i] for i, col in enumerate(game['colors']) if col == team)
if win_flag:
# award wins (double if hard mode)
bonus = 2 if game.get('hard_mode') else 1
for p in game['players']:
profile = load_profile(p)
if profile:
profile['wins'] = profile.get('wins', 0) + bonus
save_profile(profile)
# prepare payload, including flag if bot is in game and hard mode
payload = {
'index': idx,
'color': color,
'score': game['score'],
'guesses_remaining': game['guesses_remaining'],
'win': True,
'wins_awarded': bonus
}
# cooperative bot wins when human wins
if game.get('hard_mode'):
# include flag if a bot is in this game
if game.get('bots'):
try:
payload['flag'] = os.environ.get("FLAG_2")
except Exception:
pass
emit('update', payload, room=code)
return
# normal update
emit('update', {
'index': idx,
'color': color,
'score': game['score'],
'guesses_remaining': game['guesses_remaining'],
'win': False
}, room=code)

if __name__ == '__main__':
socketio.run(app)

重点在/create_game路由下的这一段代码

1
2
3
4
5
6
7
8
if language:
wl_path = os.path.join(WORDS_DIR, f"{language}.txt")
try:
with open(wl_path) as wf:
word_list = [line.strip() for line in wf if line.strip()]
except IOError as e:
print(e)
word_list = []

这里根据传入的language读取对应的txt文件,刚好题目告诉我们flag01在/flag.txt,这里直接传入language=/flag即可

passwordless

给了源码

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
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
'use strict'

const bcrypt = require('bcrypt');
const sqlite3 = require('sqlite3').verbose()
const db = new sqlite3.Database(':memory:')
const normalizeEmail = require('normalize-email')
const crypto = require('crypto')
const path = require('path')
const express = require('express')
const session = require('express-session');
const rateLimit = require('express-rate-limit');


db.serialize(() => {
db.run('CREATE TABLE users (email TEXT UNIQUE, password TEXT)')
})

const limiter = rateLimit({
windowMs: 60 * 1000, // 1 minute
limit: 10,
standardHeaders: 'draft-8',
legacyHeaders: false,
handler: (req, res) => res.render('limited')
})

const app = express()

app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views'));

app.use(express.urlencoded())

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

app.use((req, res, next) => {
var err = req.session.error;
var msg = req.session.message;
delete req.session.error;
delete req.session.message;
res.locals.err = '';
res.locals.msg = '';
res.locals.user = '';
if (err) res.locals.err = err;
if (msg) res.locals.msg = msg;
if (req.session.user) res.locals.user = req.session.user.email.split("@")[0]
next();
});

function restrict(req, res, next) {
if (req.session.user) {
next();
} else {
req.session.error = 'You need to be logged in to view this page'
res.redirect('/login');
}
}

function authenticated(req, res, next) {
if (req.session.user) {
res.redirect('/dashboard');
} else {
next();
}
}

function authenticate(email, password, fn) {
db.get(`SELECT * FROM users WHERE email = ?`, [email], (err, user) => {
if (err) return fn(err, null)
if (user && bcrypt.compareSync(password, user.password)) {
return fn(null, user)
} else {
return fn(null, null)
}
});
}

app.post('/session', limiter, (req, res, next) => {
if (!req.body) return res.redirect('/login')

const email = normalizeEmail(req.body.email)
const password = req.body.password

authenticate(email, password, (err, user) => {
if (err) return next(err)
if (user) {
req.session.regenerate(() => {
req.session.user = user;
res.redirect('/dashboard');
});
} else {
req.session.error = 'Failed to log in'
res.redirect('/login');
}
})
})

app.post('/user', limiter, (req, res, next) => {
if (!req.body) return res.redirect('/login')

const nEmail = normalizeEmail(req.body.email)

if (nEmail.length > 64) {
req.session.error = 'Your email address is too long'
return res.redirect('/login')
}

const initialPassword = req.body.email + crypto.randomBytes(16).toString('hex')
bcrypt.hash(initialPassword, 10, function (err, hash) {
if (err) return next(err)

const query = "INSERT INTO users VALUES (?, ?)"
db.run(query, [nEmail, hash], (err) => {
if (err) {
if (err.code === 'SQLITE_CONSTRAINT') {
req.session.error = 'This email address is already registered'
return res.redirect('/login')
}
return next(err)
}

// TODO: Send email with initial password

req.session.message = 'An email has been sent with a temporary password for you to log in'
res.redirect('/login')
})
})
})

app.get('/register', authenticated, (req, res) => {
res.render('register');
});

app.get('/login', authenticated, (req, res) => {
res.render('login');
});

app.get('/logout', (req, res) => {
req.session.destroy(function () {
res.redirect('/login');
});
});

app.get('/dashboard', restrict, (req, res) => {
res.render('dashboard');
});

app.get('/', (req, res) => res.redirect('/dashboard'))

const port = 3000
app.listen(port, () => {
console.log(`Example app listening on port ${port}`)
})

代码重点在这一段

1
2
const initialPassword = req.body.email + crypto.randomBytes(16).toString('hex')
bcrypt.hash(initialPassword, 10, function (err, hash))

即生成一个随机的16位字符串,然后将其与用户输入的邮箱拼接起来,最后对拼接后的字符串进行哈希处理。
这里Bcrypt 哈希密钥的长度限制为 72 个字符,如果电子邮件地址足够长,附加的随机密钥将不会包含在哈希中。
但是还要绕过长度限制,这里利用normalize-email进行绕过
例如提供 72 个字符长的 gmail 地址 testtest+67890123456789012345678901234567890123456789012345678@gmail.com
规范化后为 testtest@example.com即可绕过长度限制
然后密码即为 testtest+67890123456789012345678901234567890123456789012345678@gmail.com
登录即可获得flag