siunam's Website

My personal website

Home Writeups Research Blog Projects About

pogn

Table of Contents

  1. Overview
  2. Background
  3. Enumeration
  4. Exploitation
  5. Conclusion

Overview

Background

Pogn in mong.

pogn.chall.lac.tf

Enumeration

Index page:

In here, we can play a game called "Pong".

lactf{7_supp0s3_y0u_g0t_b3773r_NaNaNaN}

When we lost the game, it pops up an alert box:

Burp Suite HTTP history:

As you can see, this web application communicates with the server using WebSocket (ws).

Burp Suite WebSockets history:

Hmm… Not sure what those data for.

Not much we can do in here, let's dig through this web application's source code.

In this challenge, we can download a file:

┌[siunam♥Mercury]-(~/ctf/LA-CTF-2024/web/pogn)-[2024.02.19|14:05:53(HKT)]
└> file pogn.zip              
pogn.zip: Zip archive data, at least v2.0 to extract, compression method=deflate
┌[siunam♥Mercury]-(~/ctf/LA-CTF-2024/web/pogn)-[2024.02.19|14:05:54(HKT)]
└> unzip pogn.zip              
Archive:  pogn.zip
  inflating: Dockerfile              
  inflating: package.json            
  inflating: package-lock.json       
   creating: src/
  inflating: src/index.html          
  inflating: src/style.css           
  inflating: src/pogn.js             
  inflating: src/server.js

By reading the source code, we have the following findings:

src/server.js:

[...]
app.ws('/ws', (ws, req) => {
  [...]
  const Msg = {
    GAME_UPDATE: 0,
    CLIENT_UPDATE: 1,
    GAME_END: 2
  };
     [...]
      // check if there has been a winner
      // server wins
      if (ball[0] < 0) {
        ws.send(JSON.stringify([
          Msg.GAME_END,
          'oh no you have lost, have you considered getting better'
        ]));
        clearInterval(interval);

      // game still happening
      } else if (ball[0] < 100) {
        ws.send(JSON.stringify([
          Msg.GAME_UPDATE,
          [ball, me]
        ]));

      // user wins
      } else {
        ws.send(JSON.stringify([
          Msg.GAME_END,
          'omg u won, i guess you considered getting better ' +
          'here is a flag: ' + flag,
          [ball, me]
        ]));
[...]

When we win the game, the server will return a message with the flag.

How to win the game? Well, it's when the game's ball X position (ball[0]) is greater than 100.

Hmm… Can we modify the server's ball/paddle?

In the message event on the server-side, it looks like this:

[...]
  ws.on('message', (data) => {
    try {
      const msg = JSON.parse(data);
      if (msg[0] === Msg.CLIENT_UPDATE) {
        const [ paddle, paddleV ] = msg[1];
        if (!isNumArray(paddle) || !isNumArray(paddleV)) return;
        op = [clamp(paddle[0], 0, 50), paddle[1]];
        opV = mul(normalize(paddleV), 2);
      }
    } catch (e) {}
  });
[...]

As you can see, when a new WebSocket message with CLIENT_UPDATE (1) message is received on the server-side, it'll update our user's paddle X Y position and paddle vector.

So… Nope, we can't modify the server's ball/paddle.

How about the client-side?

src/pogn.js:

[...]
const clamp = (x, low, high) => min(max(x, low), high);
[...]
const viewportToServer = ([x, y]) => [
  x * 100 / innerWidth,
  y * 30 / (0.5 * innerHeight) - 30
];
[...]
ws.addEventListener('open', () => {
  ws.addEventListener('message', (e) => {
    const msg = JSON.parse(e.data);
    switch (msg[0]) {
      case Msg.GAME_UPDATE:
        ballPos = serverToViewport(msg[1][0]);
        serverPos = serverToViewport(msg[1][1]);
        updateFromRemote();
        break;
      case Msg.GAME_END:
        alert(msg[1]);
        break;
    }
  })

  const interval = setInterval(() => {
    if (!moved) return;
    ws.send(JSON.stringify([
      Msg.CLIENT_UPDATE,
      [ userPos, v ]
    ]));
  }, 50);

  ws.addEventListener('close', () => clearInterval(interval));
});

const $ = x => document.querySelector(x);

const userPaddle = $('.user.paddle');
const serverPaddle I got tired of people leaking my password from the db so I moved it out of the db. [penguin.chall.lac.tf](https://penguin.chall.lac.tf)= $('.server.paddle');
const ball = $('.ball');

let moved = false;
let p_x = 0;
let p_y = 0;
let v = [0, 0];
window.addEventListener('mousemove', (e) => {
  moved = true;
  const x = clamp(e.clientX, 0, innerWidth / 2 - 48);
  const y = e.clientY;
  userPaddle.style = `--x: ${x}px; --y: ${y}px`;
  userPos = viewportToServer([ x, y ]);
  v = viewportToServer([0.01 * (x - p_x), 0.01 * (y - p_y)]);
  p_x = x;
  p_y = y;
});
[...]

In here, when our mouse has moved, it'll send our user's X Y position and vector to the server.

Exploitation

Hmm… I wonder what will happen when our user's X Y position and vector are 0? If our paddle stays in the Y middle axis, and vector X Y are 0, the game ball should just flies to the other end when it collides with us.

To do so, we can use our browser console to patch the client-side JavaScript code:

userPos = [0, 0];
v = [0, 0];
moved = true;

Nice! Here's the flag!

Conclusion

What we've learned:

  1. Exploiting WebSocket