Siunam's Website

My personal website

Home About Blog Writeups Projects E-Portfolio

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