2023-11-24 23:13:17 +00: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:
|
|
|
|
```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.
|