Original description:

Clam’s tired of people hacking his sites so he spammed obfuscation on his new game. I have a feeling that behind that wall of obfuscated javascript there’s still a vulnerable site though. Can you get enough points to get the flag? I also found the backend source. Second instance: https://wooooosh.2020.chall.actf.co/ Author: aplet123

Problem code

const express = require("express");
const exphbs = require("express-handlebars");
const socket = require("socket.io");
const path = require("path");
const http = require("http");
const morgan = require("morgan");

const app = express();
const serv = http.createServer(app);
const io = socket.listen(serv);
const port = process.env.PORT || 60600;

function rand(bound) {
    return Math.floor(Math.random() * bound);
}

function genId() {
    const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
    return new Array(64).fill(0).map(v => chars[rand(chars.length)]).join``;
}

function genShapes() {
    return new Array(20).fill(0).map(v => ({ x: rand(500), y: rand(300) }));
}

function dist(a, b, c, d) {
    return Math.sqrt(Math.pow(c - a, 2), Math.pow(d - b, 2));
}

app.use(morgan("combined"));

app.use(express.static(path.join(__dirname, "public")));

const hbs = exphbs.create({
    extname: ".hbs",
    helpers: {}
});

app.engine("hbs", hbs.engine);
app.set("view engine", "hbs");
app.set("views", path.join(__dirname, "views"));

io.on("connection", client => {
    let game;
    setTimeout(function() {
        try {
            client.disconnect();
        } catch (err) {
            console.log("err", err);
        }
    }, 1 * 60 * 1000);
    function endGame() {
        try {
            if (game) {
                if (game.score > 20) {
                    client.emit(
                        "disp",
                        `Good job! You're so good at this! The flag is ${process.env.FLAG}!`
                    );
                } else {
                    client.emit(
                        "disp",
                        "Wow you're terrible at this! No flag for you!"
                    );
                }
                game = null;
            }
        } catch (err) {
            console.log("err", err);
        }
    }
    client.on("start", function() {
        try {
            if (game) {
                client.emit("disp", "Game already started.");
            } else {
                game = {
                    shapes: genShapes(),
                    score: 0
                };
                game.int = setTimeout(endGame, 10000);
                client.emit("shapes", game.shapes);
                client.emit("score", 0);
            }
        } catch (err) {
            console.log("err", err);
        }
    });
    client.on("click", function(x, y) {
        try {
            if (!game) {
                return;
            }
            if (typeof x != "number" || typeof y != "number") {
                return;
            }
            if (dist(game.shapes[0].x, game.shapes[1].y, x, y) < 10) {
                game.score++;
            }
            game.shapes = genShapes();
            client.emit("shapes", game.shapes);
            client.emit("score", game.score);
        } catch (err) {
            console.log("err", err);
        }
    });
    client.on("disconnect", function() {
        try {
            if (game) {
                clearTimeout(game.int);
            }
            game = null;
        } catch (err) {
            console.log("err", err);
        }
    });
});

app.get("/", function(req, res) {
    res.render("home");
});

serv.listen(port, function() {
    console.log(`Server listening on port ${port}!`);
});

Solution

The frontend calls the endpoints and generates a canvas, where 19 squares and 1 circle is shown. You gotta click the circle, then you advance a round. But if you take too long, you loose too.

Fortunately, with the devtools of the browser you can see that 20 x/y coordinates are transmitted, once you set ignore all breakpoints (right upper corner).

The displayed circle is always the first one in the list (you have to plot the 20 points), and we can mostly confirm this, even though game.shapes[0].x, game.shapes[1].y in the server node.js code checks the X of the 0th and the Y of the 1st coordinate.

By further analyzing the transferred protocol in the developer-tools (or via reading the code above), we see that we only have to react to “shapes” events by replying wiht a click-event with coordintes close to the 0th transferred x/y coordinates as a tuple and for debugging, print the other events (commented out below).

Solution Code

import socketio
import numpy as np

sio = socketio.Client()
#@sio.event
#def connection(): print("EVENT conn")
@sio.event
def disp(x): print("EVENT disp", x)
#@sio.event
#def score(x): print("EVENT scores", x)
    
@sio.event
def shapes(shapes):
    #print("EVENT shapes", shapes)
    ret = [int(shapes[0]["x"])+5.1, int(shapes[]["y"])+5.1]
    sio.emit("click", (ret[0], ret[1]))

sio.connect("https://wooooosh.2020.chall.actf.co/socket.io")
sio.emit("start")
#Good job! You're so good at this! The flag is actf{w0000sh_1s_th3_s0und_0f_th3_r3qu3st_fly1ng_p4st_th3_fr0nt3nd}!