gctf2023/web/where-is-the-scope
2023-11-24 18:13:17 -05:00
..
patches Add web challenges 2023-11-24 13:05:41 -05:00
public Add web challenges 2023-11-24 13:05:41 -05:00
src Add web challenges 2023-11-24 13:05:41 -05:00
.babelrc Add web challenges 2023-11-24 13:05:41 -05:00
package.json Add web challenges 2023-11-24 13:05:41 -05:00
README.md Add notes to Where is the Scope 2023-11-24 18:13:17 -05:00
src.zip Add web challenges 2023-11-24 13:05:41 -05:00

Where is the Scope?

This year we are launching our new GlacierTV website allowing you to play any video from youtube. You can also take some notes while watching them and also restrict the access to those with a 2FA token. Hope you enjoy it.

Challenge Notes

XSS via Report Function

Theory: Administrator can be XSS'ed via the "Report" function. To see if our hypothesis is correct, let's examine the relevant parts of the code.

A user clicking on the "Report" button is handled by the following code:

function onReportClick() {
    document.getElementById("reportBtn").addEventListener("click", _ => {
        alert('Thank you for reporting this uri. A moderator will review it soon.')
        fetch("/report", {
            headers: {
                "Content-Type": "application/json"
            },
            method: "POST",
            body: JSON.stringify({
                path: window.location.search
            })
        });
    })
}

The server handles post requests to the /report endpoint in server.js on lines 96 through 120:

app.post("/report", async (req, res) => {
    try {
        const path = req.body.path;
        if(typeof path !== "string") return res.status(400).send("No path provided");
        const uri = `http://localhost:8080/${path}`

        const browser = await puppeteer.launch({
            headless: "new",
            args: ["--no-sandbox", "--disable-dev-shm-usage", "--disable-setuid-sandbox"],
        });
        const context = await browser.createIncognitoBrowserContext();
        const page = await context.newPage();
        await page.goto('http://localhost:8080/');
        await page.waitForNavigation({
            waitUntil: 'networkidle0',
        });
        await page.evaluate(async message => {
            await fetch("/setup_2fa", {method: "POST"});
            await fetch("/secret_note", {
                method: "POST",
                body: JSON.stringify({message}),
                headers: {
                    "Content-Type": "application/json"
                }
            });
        }, FLAG)
        await page.goto(uri);
        await sleep(5000);
        await browser.close();
        res.status(200).send("Thank you for your report. We will check it soon")
    } catch(err) {
	    console.log(err)
        res.status(400).send("Something went wrong! If you think this is an error on our site, contact an admin.")
    }
})

Walking through the /report callback:

  • The server checks the JSON body for a field called "path"
  • URI is created from the value of "path": http://localhost:8080/${path}
  • A browser is launched with Puppeteer and it loads the main app
  • The browser session POSTs to /setup_2fa to get a token
  • Next, the browser places the flag into a Note on the UI
  • The browser then will browse to the URI created with our payload

If we can XSS the admin and steal their cookie, we can get the flag from the saved note.

The uri parameter is vulnerable to XSS. When the site is loaded, the loadFromQuery method is called, which pulls out the uri parameter and passes it to parseUI. The URI returned by parseUI is used as the src attribute of the page's iframe:

function loadFromQuery() {
    const query = new URLSearchParams(window.location.search);
    const source = query.get("source") || "youtube";
    const uri = query.get("uri");
    document.getElementById("searchInput").value = uri || "https://www.youtube.com/embed/dQw4w9WgXcQ?&autoplay=1";
    if(!uri) return false;
    updateSource(uri, source);
    var ifconfig = {
        pathname: `<iframe frameborder="0" width=950 height=570 src="${parseURI(uri)}"></iframe>`
    }
    document.getElementById("viewer").srcdoc = ifconfig.pathname;
    return true;
}

The parseURI method tries to create a URL object from the passed in uri variable. Then the origin of the URL object is checked to see if it matches "https://www.youtube.com", and if it does, it is returned to loadFromQuery and used as the src attribute for the iframe:

function parseURI(uri) {
    const uriParts = new URL(uri);
    if(uriParts.origin === "https://www.youtube.com")
        return uri;
    // If user does not provide a youtube uri, we take the default one.
    return "https://www.youtube.com/embed/dQw4w9WgXcQ?&autoplay=1";
}

A uri such as https://www.youtube.com/%22></iframe> will break us out of the iframe and allow us to render any HTML content that we would like.