Challenge
A Kahoo -style quiz over websockets. You need 21 correct answers in a row to get the flag. The server picks one of four choices each round:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| io.on("connect", socket => {
socket.emit("gameState", { ...gameState(), timeLeft: sleepEndTime - new Date().getTime() });
socket.on("answer", (answer: number) => {
if (answered.has(socket.id) || answerRevealed) {
return;
}
answered.add(socket.id);
if (!(socket.id in scoreboard)) {
scoreboard[socket.id] = 0;
}
if (answer == correctAnswer) {
scoreboard[socket.id] += 1;
} else {
scoreboard[socket.id] = 0;
}
if (scoreboard[socket.id] >= 21) {
socket.emit("flag", process.env.FLAG ?? "bctf{fake_flag}");
}
})
})
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| while (true) {
if (questionIndex === 0) {
shuffleArray(QUESTIONS);
}
answerRevealed = false;
answered.clear();
correctAnswer = randomInt(4);
io.emit("gameState", gameState());
await sleep(8);
answerRevealed = true;
for (const player of Object.keys(scoreboard)) {
if (!answered.has(player)) {
delete scoreboard[player];
}
}
io.emit("gameState", gameState());
await sleep(3);
questionIndex = (questionIndex + 1) % QUESTIONS.length;
}
|
First Approach
The first idea was brute force. There are eight seconds per round and the answer is one of four, so a fresh socket has a 1 in 4 chance each round. The problem is answered.has(socket.id) blocks a socket from guessing twice in the same round, so a single client only gets one shot per question.
Solution
The better angle is that the scoreboard is emitted on connect and updated immediately after each guess, not just at the end of the round. That turns it into an oracle. Spin up four dummy clients and have each guess a different answer, then connect a fresh client and read the scoreboard it receives. Whichever dummy gained a point reveals correctAnswer for the current round. Submit that answer on the real client for a point, and repeat until the real client reaches 21.