gctf2023/web/where-is-the-scope/README.md

110 lines
4.4 KiB
Markdown

# 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: `<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:
```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></iframe>` will break us out of the iframe and allow us to render
any HTML content that we would like.