One of the challenges of LaCTF that I personally found not to be insanely hard, but can be quite complex to exploit. The challenge focuses mainly on business logic that results in a race condition via the websocket manipulation on the frontend side. It did take me a few hours to get the code to function properly and the backend to hit a race condition to spit out the flag. So, enjoy the write-up!
Let’s access the webpage real fast first at https://pogn.chall.lac.tf/ and this is the interface that we have…

There is not much, just an endless game of ping-pong which we will absolutely lose at some points

Now after peaking at the source code for a while, I hit the line where I can manipulate to get the flag

These lines are set inside a setInterval inside a Websocket connection, so that means to attack this, I’d have to deal with WebSockets either through websocat or write a custom Python script that utilizes websocket-client.
Back to our target, according to the code, it is obvious that the value of the ball[0] has to either be bigger than 100 or is something else for the flag to appear. So after overthinking and doing some math for a while on how to make the value of ball[0] to be bigger than 100, it turns out to be impossible and even if it is possible, it is going to be too much of an effort to figure out what should, so what I thought was if I manage to manipulate this section of code…
...
// collision with user's paddle
if (norm(sub(op, ball)) < collisionDist) {
ballV = add(opV, mul(normalize(sub(ball, op)), 1 / norm(ballV)));
}
...and force it to assign ballV something like [NaN, NaN], I’d still get the flag since NaN is not bigger than 100 or smaller than 0 either. And why are these lines of code that I am attacking you may ask? Because we have control over the value of op and opV.
Recalling the last section, I did some math and I figured out that if I send something like [1,[[50,0],[0,0]]] to a newly opened WebSocket connection, it will set ballV to [NaN, NaN] and I will get the flag, but that will ONLY WORK under one condition that I have to send that while the value of ball is around [52.5, 0], which is usually when we first open a WebSocket connection.

But there is a catch, even if we send the payload immediately after the WebSocket has opened, there is no guarantee that our payload will land EXACTLY when the value of ball is [52.55, 0]. But luckily when I re-examine the source code as well as the web page, it turns out that the value will slowly increase to around 80 then slowly decrease back to 1 and the cycle is repeated…

So all I have to do is automate this process, and keep sending the payload over and over again each time a new message arrives until it hits the time when the value of ball is [52.5, 0]. To make this viable, I built a Python script of my own…
import websocket
def on_message(ws, message):
print(message)
ws.send("[1,[[50,0],[0,0]]]")
def on_open(ws):
ws.send("[1,[[50,0],[NaN,NaN]]]")
if __name__ == "__main__":
ws = websocket.WebSocketApp(
"wss://pogn.chall.lac.tf/ws",
on_open=on_open,
on_message=on_message,
)
ws.run_forever(reconnect=5)And when I executed it, the flag was found !!!
