🟦Web - Blueprint Heist

Hack the Box Business CTF 2024 - Web - Blueprint Heist Writeup

Intro

While I managed to complete a few challenges in this years HTB Business CTF I thought this one deserved a writeup.

While this challenge was labeled as a medium I think it would be a hard-insane level challenge anywhere else. This is going to be a wild one so strap in and put on your learning caps.

Challenge Description

Amidst the chaos of their digital onslaught, they manage to extract the blueprints by inflitrating the ministry of internal affair's urban planning commission office detailing the rock and soil layout crucial for their underground tunnel schematics.

Challenge Files

Walk-through

To start with we are able to spin up an instance of the challenge and are given a an IP address and port number the website it running on. Along with that we are given the source code to this site, Dockerfile and all.

Right off the bat, before even reviewing the source code, when browsing the site and intercepting traffic with a proxy, I am using Burp Suite Community Edition (I'm poor), you will see when you click on 'Enviromental Impact' or 'Construction Progress' it sends a POST request to /downloads.

This download request has to arguments being passed with it. One is the token in the url, which was received by a call to /getToken (we will get to that later). The other is url, located in the POST data.

First thing I checked was if this URL parameter was vulnerable to SSRF, which it was. This allowed me to point to anywhere, including my own server and localhost on the remote target. This download feature was grabbing the page it was pointed at by sending a GET request and then rendering it and converting it to a PDF using a library. This library was able to be identified by looking at the PDF properties.

The library they were using was wkhtmltopdf. This particular library is vulnerable to Dynamic PDF XSS and SSRF, allowing me, the attacker, to point it to a page I control and telling it to redirect and grab a file instead from its local system and convert that to a pdf.

Now, finally, I took a peek at the source code. After a quick skim I found that I wanted to get an admin JWT to reach the /admin and /graphql endpoints, and to do that and sign it I needed the secret key. Luckily the secret key is located in the /app/.env file. To grab it I followed the steps in the linked exploit of wkhtmltopdf.

Image Correction: Use this to host instead of python3, (for obvious reasons):

php -S 0.0.0.0:8080

Upon pasting the link from Burp's request in browser feature we see the .env file contents!

Now we can just forge a JWT token using the secret and walk right into the admin page right? Right?

Hell no. We have to jump through some more hurdles of course.

So armed with our forged JWT saying we are an admin I tried to access the admin endpoint at /admin. No luck. What about the /graphql endpoint? Again, no luck. It said I wasn't internal.

To try to bypass this I tried the old X-Forwarded-For: 127.0.0.1 bypass but no dice. Looks like I actually have to have it come from internal. Luckily for us though we have an SSRF, though through a PDF generator, blah.

I took the forged token and edited my test.html file on my own server to load an iframe of http://localhost:<random port HTB generated>/admin?token=ey... See my problem yet?

Took a try but I then remembered that the default port for this application is 1337 and its being mapped to an external port of whatever. I fixed this in my file by just changing that port number to 1337.

I started the webserver on my server and sent the download POST request.. and... BAM got a view of the anticlimactic admin page!

Not much interesting to see here besides the search user bar towards the bottom. Looking at this in the source code we can see its interacting with the /graphql endpoint, also only available locally.

Now here's where I got stuck for a bit. It was sending POST requests, as the vast majority of GraphQL endpoints use POST, and so I was trying to find a way to interact with the endpoint through the SSRF using POST requests. I tried hundreds of ways to get the pdf generator to load the javascript and make a request to the endpoint but to no avail. Finally I decided to go read some docs and thank God I did that.

You can send GraphQL queries over GET requests... As just an argument... query=... UGH

Ok so that was way easier than expected. Now I could send queries and to test it out I did the easy one that just grabs all the users:

{
        getAllData {
            name
            department
            isPresent
        }
    }

Now there was one other query we could do and this one was vulnerable to SQL injection:

If we sent a GraphQL query for getDataByName we could inject into the name argument:

query{getDataByName(name: "john"){isPresent, name}}

But again, not so simple.

If we look above the try for the query we see an if statement checking for detectSqli(args.name). Argh, are we foiled? Lets see...

We can see from this function that it blocks basically all special characters, except @, `, and ~. Now I went down a rabbit hole of what I could do with those but, again, to no avail.

After a bunch of messing around I went to trying random things and when testing a new line (/n) I saw some strange behavior... I could put special characters in after the newline! This is awesome! Turns out the pattern check is not multi-line unless you specify it.

Now I created a simple request that I could test locally using SQLmap:

query{getDataByName(name: "john\n*"){isPresent, name}}

SQLmap fired up and immediately got me a nice UNION injection!

Now we had SQLi into the database of the server! But wait, the flag isn't here, its in /root/flag.txt. And even though I was a database admin and could read files from the server I couldn't read in /root.

Looking at the source code some more we can see there is a SUID binary compiled on creation, located at /readflag, that will cat out the contents of /root/flag.txt when executed. That was the goal. Though I could read most files on the machine, I couldn't execute any, much to my annoyance.

Now I figured I should have write privileges as well in most areas but SQLmap was erroring out and not writing for some reason, turns out this is an issue with SQLmap, at least the version I was using. After intercepting the traffic with Wireshark I could see that it was using a malformed query when writing the file, causing the graphql endpoint to return an error instead of executing the query. To fix this I started doing things manually.

Now, with write permissions I needed to find what to write to in order to get code execution. There was a few places I would like to overwrite, but you can;t overwrite a file with MySQL/MariaDB. After a long source code review I found what I think could be the path to take.

In the errorController.js file it looks for a file in /app/views/errors/ with the appropriate HTTP error code number and the extension '.ejs' and then renders that.

I found that there was a popular error code missing, 404. If I could write to /app/views/errors/404.ejs and have it rendered then I could get an SSTI vulnerability and execute code!

I created a simple SSTI payload for this:

<h1><%= process.mainModule.require('child_process').execSync('/readflag') %></h1>

and then I created my custom iframe to write to /app/views/errors/404.ejs:

<iframe src=http://localhost:1337/graphql?query=%7BgetDataByName%28name%3a%20%22john%5Cn%27%20UNION%20ALL%20SELECT%200x3c68313e3c253d2070726f636573732e6d61696e4d6f64756c652e7265717569726528276368696c645f70726f6365737327292e6578656353796e6328272f72656164666c6167272920253e3c2f68313e%2CNULL%2CNULL%2CNULL%20INTO%20DUMPFILE%20%27%2Fapp%2Fviews%2Ferrors%2F404%2Eejs%27%2D%2D%20%2D%22%29%7BisPresent%2C%20name%7D%7D&token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYWRtaW4iLCJpYXQiOjE3MTYxMTI0MjN9.lqWwYLhKec8r4AeyG3uCeL1qaICOymVda8rFAdeEGco width=1000px height=10000px></iframe>

Now, I executed the /download request pointing to the file containing ^this^ on my server, I see the hit, I hold my breath and type in a random endpoint /axnfne, and hit enter...

BAM!

We got it! This was definitely one of the hardest web challenges I have ever done but I loved it.

PWNED!

Last updated