diff --git a/web/where-is-the-scope/README.md b/web/where-is-the-scope/README.md index eb41822..4fe6e67 100644 --- a/web/where-is-the-scope/README.md +++ b/web/where-is-the-scope/README.md @@ -1 +1,110 @@ -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. \ No newline at end of file +# 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: +```javascript +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: +```javascript +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: +```javascript +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: `` + } + 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: +```javascript +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>` will break us out of the iframe and allow us to render +any HTML content that we would like. \ No newline at end of file