Contact CTF writeups Notes

[PicoCTF 2018] - web - Fancy Alive Monitoring

This is one of my writeups for PicoCTF 2018

Problem

One of my school mate developed an alive monitoring tool. Can you get a flag from http://2018shell3.picoctf.com:56517

hints:

  1. This application uses the validation check both on the client side and on the server side, but the server check seems to be inappropriate.
  2. You should be able to listen through the shell on the server.

Solution

Browsing to the site, we are greeted by a page asking for an IP to check and a link to the backend source code :

<html>
<head>
    <title>Monitoring Tool</title>
    <script>
    function check(){
        ip = document.getElementById("ip").value;
        chk = ip.match(/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/);
        if (!chk) {
            alert("Wrong IP format.");
            return false;
        } else {
            document.getElementById("monitor").submit();
        }
    }
    </script>
</head>
<body>
    <h1>Monitoring Tool ver 0.1</h1>
    <form id="monitor" action="index.php" method="post" onsubmit="return false;">
    <p> Input IP address of the target host
    <input id="ip" name="ip" type="text">
    </p>
    <input type="button" value="Go!" onclick="check()">
    </form>
    <hr>

<?php
$ip = $_POST["ip"];
if ($ip) {
    // super fancy regex check!
    if (preg_match('/^(([1-9]?[0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]).){3}([1-9]?[0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])/',$ip)) {
        exec('ping -c 1 '.$ip, $cmd_result);
        foreach($cmd_result as $str){
            if (strpos($str, '100% packet loss') !== false){
                printf("<h3>Target is NOT alive.</h3>");
                break;
            } else if (strpos($str, ', 0% packet loss') !== false){
                printf("<h3>Target is alive.</h3>");
                break;
            }
        }
    } else {
        echo "Wrong IP Format.";
    }
}
?>
<hr>
<a href="index.txt">index.php source code</a>
</body>
</html>

Since the input is passed to php's exec, it seems like we have a command injection to exploit. Two things stand in our way :

  1. The input is validated
  2. The command output is not returned

Let's figure this out

Broken input validation = command injection

As the hint indicates, the IP addresses are not validated the same way on the front-end and on the backend :

Front-end validation regex :

/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/

Back-end validation regex

/^(([1-9]?[0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]).){3}([1-9]?[0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])/

The important difference here is that both regex start with ^ (which matches the beginning of the string), but only the front-end regex ends with $ (which matches the end of the string). What that means is that the front-end will reject the input if it contains anything after the IP, but the back-end won't.

Front-end validation is completely useless, since we can send requests directly to the server.

The command executed is shown in that line of the source :

exec('ping -c 1 '.$ip, $cmd_result);

Where $ip is our input ($cmd_result is used to store the output).

now our input needs to start with an ip address and provide a command to run. so if we provide 127.0.0.1 ; command, command will be executed after ping.

Now what should our payload be ?

Choosing a payload

The backend code doesn't return the input of what it execs, it only returns a message indicating whether the pinged ip replied :

foreach($cmd_result as $str){
    if (strpos($str, '100% packet loss') !== false){
        printf("<h3>Target is NOT alive.</h3>");
        break;
    } else if (strpos($str, ', 0% packet loss') !== false){
        printf("<h3>Target is alive.</h3>");
        break;
    }
}

That means we can't simply run ls to look for the flag file and then cat it.

There are many ways of getting around that, the most obvious approach here being a reverse shell.

I decided to use a different payload for fun, based on two assumptions :

  1. Python 2 is installed on the server
  2. There is a flag file in the task's directory

My payload was python -m SimpleHTTPServer 9999 (127.0.0.1; python -m SimpleHTTPServer 9999).

The SimpleHTTPServer module is part of the Python 2 standard library (it has been renamed to http.server in Python 3), which means it's available if python is. When executed (using python's -m option), it creates an HTTP server listening on the port passed as an argument (9999 here) and serving the current working directory.

When sending that payload, the client hangs : that means the code hasn't returned, since it's running the http server.

Now if port 9999 is not firewalled, we should be able to browse to http://2018shell3.picoctf.com:56517 and get a file listing of the task's directory.

And indeed, the flag is at http://2018shell3.picoctf.com:9999/flag.txt : picoCTF{n3v3r_trust_a_b0x_36d4a875}.