Gadget Santa
Gadget Santa is a challenge in which we are given the source code as well as a hosted copy of a web app that we need to hack in order to find the flag.
Prompt
It seems that the evil elves have broken the controller gadget for the good old candy cane factory! Can you team up with the real red teamer Santa to hack back?
First Impressions
In this challenge, we’re presented with something that looks like a monitoring console of sorts. As we click through the options, we can see the output of the command
query parameter in the console.
Let’s take a look at the source.
Source code review
Starting with the layout and the build files, we can see that this is a PHP app that runs behind nginx. There isn’t anything notable about the version of PHP that’s in use (it’s whatever is latest of the debian:buster-slim
docker image) or any the config files.
What is curious however is the following two lines found in the Dockerfile:
COPY config/ups_manager.py /root/ups_manager.py
COPY config/santa_mon.sh /
RUN chmod +x /santa_mon.sh
Here we can see the Python script ups_manager.py
being installed to root’s home directory and santa_mon.sh
being installed to /
.
Let’s take a look at these next two files.
ups_manager.py and santa_mon.sh
ups_manager.py
is a Python web server that runs on localhost port 3000. This web server implements a few endpoints, including two that indicate where the “elves” sabotaged the app. Last but not least is GET /get_flag
which, you guessed it, should respond with the flag if we can reach it :)
elif self.path == '/get_flag':
resp_ok()
self.wfile.write(get_json({'status': 'HTB{f4k3_fl4g_f0r_t3st1ng}'}))
return
In santa_mon.sh
, we see a bunch of function definitions that implement the monitoring buttons on the home page. In two of those functions, we can see them reach our ups_manager.py
endpoints using curl.
ups_status() {
curl localhost:3000;
}
restart_ups() {
curl localhost:3000/restart;
}
What stands out more, however, is that the script appears to blindly execute whatever command was passed in the command line arguments. This looks like an opportunity for shell injection!
if [ "$#" -gt 0 ]; then
$1
fi
MonitorModel
and MonitorController
This app has just one controller and one model. In MonitorController
, we can see that when it handles a request, it takes a query variable and uses it as an argument to MonitorModel
without escaping it first.
$command = isset($_GET['command']) ? $_GET['command'] : 'welcome';
$monitor = new MonitorModel($command);
return $router->view('index', ['output' => $monitor->getOutput()]);
In MonitorModel
, we see that it does attempt to sanitize the command argument passed in. Unfortunately, its sanitize
method is insufficient:
public function sanitize($command)
{
$command = preg_replace('/\s+/', '', $command);
return $command;
}
What this translates to is “remove all of the whitespace from $command
”.
Further down we see shell_exec
being called from within the getOutput
method which, remember, is called from our MonitorController
!
The command is passed as an unquoted argument to /santa_mon.sh
and its output (stdout) is returned.
public function getOutput()
{
return shell_exec('/santa_mon.sh '.$this->command);
}
Exploitation
Due to this app passing user input to shell_exec
with insufficient escaping, this app is vulnerable to shell injection!
Exploitation should be straight-forward as long as we can overcome the function intended to sanitize our input. In this case, all we have to do is write our command injection without any spaces.
Beating the sanitizer
As it turns out, we can write bash without using any spaces using the $IFS
variable. What’s $IFS
? In this case, IFS stands for “Internal Field Separator”. What $IFS
does is define the separators that bash uses to separate a string into words. For example, if you have the string "foo bar baz"
, bash will treat these as foo
, bar
, and baz
.
In my initial run through, I had to take one extra step here. The default value of $IFS
is a space, a tab, and a newline.
% echo -n $IFS | xxd
00000000: 2009 0a00 ...
# space ^ ^, ^ newline
# '- tab
What seemd to be happening during my first run through is that the newline was terminating the command in my payload. I changed it to ${IFS%??}
and I was back in business! My first payload might have been weird though because when I went back through to write this, I no longer needed it.
In any case, what this all means is that we should be able to use $IFS
anywhere that we need whitespace.
The payload
At this point, there are actually two places you can have your shell command execute.
-
You can have your command executed by
santa_mon.sh
. As long as you can pass your command in as a single argument, it will be executed by this script. You can achieve this by wrapping your command in quotes.if [ "$#" -gt 0 ]; then $1 fi
In this method, the following payload is how I captured the flag:
# "curl${IFS}localhost:3000/get_flag" http://challenge.htb/?command=%22curl${IFS%??}localhost:3000/get_flag%22
-
You can have your command executed after
/santa_mon.sh
runs by having it executed as a second command. In other words, after the/santa_mon.sh
in theshell_exec
argument.public function getOutput() { return shell_exec('/santa_mon.sh '.$this->command); }
It doesn’t matter if
/santa_mon.sh
succeeds or not, provided you use an operator that allows another command to follow. A payload for that looks like:;curl${IFS%??}localhost:3000/get_flag # http://challenge.htb/?command=;curl${IFS%??}localhost:3000/get_flag
In either case, we get our flag!