I found this golf game online but the third level is so hard 😩😩
See if you can beat it!
We're given a golf minigame that looks like this:
We can see that the client makes a WebSocket connection to the remote server, sending launch
events whenever the user hits the ball.
Unfortunately, as the challenge description suggests, we'll realize when we hit level 3 that the game was rigged from the start:
Sadly, this challenge is truly impossible without server source. Luckily, just a few hours later we're given exactly that!
const express = require("express");
const http = require("http");
const app = express();
const path = require("path");
const { WebSocketServer } = require("ws");
const {readFileSync} = require("fs");
const server = http.createServer(app);
const wss = new WebSocketServer({server: server});
const FLAG = readFileSync(path.join(__dirname, "flag.txt")).toString();
wss.on("connection", ws => {
console.log("New connection!");
let level = 0;
let gameState = JSON.parse(JSON.stringify(levels[level]));
let interval = setInterval(physicsLoop, 20);
let goalTimer = 0;
function physicsLoop() {
applyVelocity(gameState.circle, gameState.rects);
for (let obj of gameState.rects) {
circleRectCollision(gameState.circle, obj);
}
if (circlesIntersecting(gameState.goal, gameState.circle)) {
goalTimer++;
} else {
goalTimer = 0;
}
if (goalTimer > 50) {
level++;
if (level >= levels.length) {
ws.send(JSON.stringify({
type: "congrats",
value: process.env.GZCTF_FLAG
}));
ws.close();
return;
}
goalTimer = 0;
gameState = JSON.parse(JSON.stringify(levels[level]));
init();
}
ws.send(JSON.stringify({
type: "ball",
value: {
x: gameState.circle.x,
y: gameState.circle.y,
moving: gameState.circle.dx + gameState.circle.dy > 0
}
}));
}
ws.send(JSON.stringify({
type: "start"
}));
function init() {
ws.send(JSON.stringify({
type: "colliders",
value: gameState.rects
}));
ws.send(JSON.stringify({
type: "flag",
value: gameState.goal
}));
}
init();
ws.on("message", data => {
let parsed;
try {
parsed = JSON.parse(data.toString?.());
} catch (e) {}
if (!parsed) return;
switch (parsed?.type) {
case "launch":
if (!parsed?.value) return;
if (typeof parsed?.value?.dx !== "number") return;
if (typeof parsed?.value?.dy !== "number") return;
launchBall(gameState.circle, parsed.value);
break;
case "cheat":
gameState.circle.x = gameState.goal.x;
gameState.circle.y = gameState.goal.y;
break;
}
});
ws.on("close", () => {
clearInterval(interval);
});
})
app.get("/", (req, res)=>{
res.sendFile(path.join(__dirname, "public", "index.html"))
});
const WALL_THICKNESS = 30;
const MAX_SPEED = 52;
const DECELERATION = 0.985;
let levels = [{
goal: { x: 230, y: 420, r: 20 },
circle: { x: 200, y: 200, dx: 0, dy: 0, r: 12 },
rects: [
[150, 500, 1000, WALL_THICKNESS],
[150, 300, WALL_THICKNESS, 230],
[150, 300, 800, WALL_THICKNESS],
[1150, 50, WALL_THICKNESS, 480],
[150, 50, 1000, WALL_THICKNESS],
[150, 50, WALL_THICKNESS, 280],
[400, 50, WALL_THICKNESS, 180],
[600, 150, WALL_THICKNESS, 150],
[800, 50, WALL_THICKNESS, 180],
[500, 400, 450, WALL_THICKNESS]
]
},{
goal: { x: 340, y: 425, r: 20 },
circle: { x: 100, y: 100, dx: 0, dy: 0, r: 12 },
rects: [
[50, 50, 1200, WALL_THICKNESS],
[1200 + WALL_THICKNESS, 50, WALL_THICKNESS, 700 + WALL_THICKNESS],
[50, 50, WALL_THICKNESS, 700],
[50, 750, 1200, WALL_THICKNESS],
[50, 120, 1130, WALL_THICKNESS],
[1150, 130, WALL_THICKNESS, 580],
[120, 680, 1050, WALL_THICKNESS],
[120, 200, WALL_THICKNESS, 500],
[120, 200, 980, WALL_THICKNESS],
[1070, 220, WALL_THICKNESS, 400],
[200, 590, 900, WALL_THICKNESS],
[200, 270, WALL_THICKNESS, 330],
[200, 270, 800, WALL_THICKNESS],
[1000, 270, WALL_THICKNESS, 270],
[300, 510, 700, WALL_THICKNESS],
[280, 340, WALL_THICKNESS, 200],
[280, 330, 670, WALL_THICKNESS],
[920, 330, WALL_THICKNESS, 140],
// :3
[370, 390, 25, 25],
[370, 440, 25, 25],
[420, 370, 60, 25],
[460, 370, 25, 110],
[420, 415, 60, 25],
[420, 460, 65, 25],
]
},{
goal: { x: 1100, y: 630, r: 20 },
circle: { x: 250, y: 250, dx: 0, dy: 0, r: 12 },
rects: [
[50, 50, 400, WALL_THICKNESS],
[50, 400, 420, WALL_THICKNESS],
[50, 50, WALL_THICKNESS, 350],
[450, 50, WALL_THICKNESS, 380],
[700, 100, 200, WALL_THICKNESS],
[700, 700, 500, WALL_THICKNESS],
[700, 100, WALL_THICKNESS, 600],
[900, 100, WALL_THICKNESS, 450],
[900, 530, 300, WALL_THICKNESS],
[1200, 530, WALL_THICKNESS, 200],
]
}];
function circlesIntersecting(circle1, circle2) {
return Math.sqrt((circle2.x - circle1.x) ** 2 + (circle2.y - circle1.y) ** 2) <= circle1.r + circle2.r;
}
function launchBall(circle, vel) {
if (circle.dx === 0 && circle.dy === 0) {
circle.dx = vel.dx;
circle.dy = vel.dy;
if (circle.dx > MAX_SPEED) {
circle.dx = MAX_SPEED;
} else if (circle.dx < -MAX_SPEED) {
circle.dx = -MAX_SPEED;
}
if (circle.dy > MAX_SPEED) {
circle.dy = MAX_SPEED;
} else if (circle.dy < -MAX_SPEED) {
circle.dy = -MAX_SPEED;
}
}
}
function applyVelocity(circle, rects) {
let initialX = circle.x;
let initialY = circle.y;
while (initialX === circle.x && initialY === circle.y &&
(circle.dy !== 0 || circle.dx !== 0)) {
circle.x += circle.dx;
circle.y += circle.dy;
for (let rect of rects) {
if (circleRectCollision(circle, rect)) {
circle.x = initialX;
circle.y = initialY;
while (circleRectCollision(circle, rect)) {
circle.x += (circle.dx / Math.abs(circle.dx)) * circle.r;
circle.y += (circle.dy / Math.abs(circle.dy)) * circle.r;
}
}
}
circle.dx = circle.dx * DECELERATION;
circle.dy = circle.dy * DECELERATION;
if (Math.abs(circle.dx * DECELERATION) < 0.15 && Math.abs(circle.dy * DECELERATION) < 0.15) {
circle.dx = 0;
circle.dy = 0;
}
}
}
function circleRectCollision(circle, rect) {
let closestX = clamp(circle.x, rect[0], rect[0] + rect[2]);
let closestY = clamp(circle.y, rect[1], rect[1] + rect[3]);
let distanceX = circle.x - closestX;
let distanceY = circle.y - closestY;
let distanceSquared = (distanceX * distanceX) + (distanceY * distanceY);
if (distanceSquared < (circle.r * circle.r)) {
let distance = Math.sqrt(distanceSquared);
let overlap = circle.r - distance;
if (distance > 0) {
circle.x += overlap * (distanceX / distance);
circle.y += overlap * (distanceY / distance);
}
let velocityMagnitude = Math.sqrt(circle.dx * circle.dx + circle.dy * circle.dy);
if (velocityMagnitude > 0) {
let normalX = distanceX / distance;
let normalY = distanceY / distance;
let dotProduct = circle.dx * normalX + circle.dy * normalY;
if (dotProduct < 0) {
circle.dx -= 2 * dotProduct * normalX;
circle.dy -= 2 * dotProduct * normalY;
}
}
return true;
}
return false;
}
function clamp(value, min, max) {
return Math.min(Math.max(value, min), max);
}
app.use(express.static(path.join(__dirname, "public")))
server.listen(8080);
Looking at the server's event handling logic,
switch (parsed?.type) {
case "launch":
if (!parsed?.value) return;
if (typeof parsed?.value?.dx !== "number") return;
if (typeof parsed?.value?.dy !== "number") return;
launchBall(gameState.circle, parsed.value);
break;
case "cheat":
gameState.circle.x = gameState.goal.x;
gameState.circle.y = gameState.goal.y;
break;
}
we just cheat 3 times to get the flag.
Thank you so much a for to playing my game! sdctf{i'm in your walls dfe6e287-73a9-4d0d-9386-ffa8258a8b69}