Home Hack The Box Writeup - Networked
Post
Cancel

Hack The Box Writeup - Networked

Networked is an easy Linux box. This box was an amazing opportunity to practice code auditing skills. First, a flaw in the PHP upload mechanism had to be found to gain initial access to the system. Then, another flaw in a cronjob had to be exploited to become user guly. Finally, another code flaw in an network interface script had to be used to obtain root access to the system.

Enumeration

As always, we start by scanning the target machine’s open ports:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
rustscan --ulimit 5000 networked.htb -- sV -sC -oN nmap_scan

PORT      STATE SERVICE       REASON  VERSION
22/tcp open  ssh     syn-ack OpenSSH 7.4 (protocol 2.0)
| ssh-hostkey: 
|   2048 22:75:d7:a7:4f:81:a7:af:52:66:e5:27:44:b1:01:5b (RSA)
| ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDFgr+LYQ5zL9JWnZmjxP7FT1134sJla89HBT+qnqNvJQRHwO7IqPSa5tEWGZYtzQ2BehsEqb/PisrRHlTeatK0X8qrS3tuz+l1nOj3X/wdcgnFXBrhwpRB2spULt2YqRM49aEbm7bRf2pctxuvgeym/pwCghb6nSbdsaCIsoE+X7QwbG0j6ZfoNIJzQkTQY7O+n1tPP8mlwPOShZJP7+NWVf/kiHsgZqVx6xroCp/NYbQTvLWt6VF/V+iZ3tiT7E1JJxJqQ05wiqsnjnFaZPYP+ptTqorUKP4AenZnf9Wan7VrrzVNZGnFlczj/BsxXOYaRe4Q8VK4PwiDbcwliOBd
|   256 2d:63:28:fc:a2:99:c7:d4:35:b9:45:9a:4b:38:f9:c8 (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBAsf1XXvL55L6U7NrCo3XSBTr+zCnnQ+GorAMgUugr3ihPkA+4Tw2LmpBr1syz7Z6PkNyQw6NzC3KwSUy1BOGw8=
|   256 73:cd:a0:5b:84:10:7d:a7:1c:7c:61:1d:f5:54:cf:c4 (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILMrhnJBfdb0fWQsWVfynAxcQ8+SNlL38vl8VJaaqPTL
80/tcp open  http    syn-ack Apache httpd 2.4.6 ((CentOS) PHP/5.4.16)
|_http-title: Site doesn't have a title (text/html; charset=UTF-8).
| http-methods: 
|_  Supported Methods: GET HEAD POST OPTIONS
|_http-server-header: Apache/2.4.6 (CentOS) PHP/5.4.16

As seen in the nmap output, only two ports are open:

  • Port 22: OpenSSH 7.4
  • Port 80: Apache 2.4.6 with PHP 5.4.16

Let’s start with the enumeration of the hosted web application

Port 80 - Web application

Looking at the website and its source code, we can clearly see that the web application might contain an upload and a gallery functionality.

Scanning for existing directories and files, we indeed find an /uploads directory. However, what’s far more interesting, is the found /backup directory which contains a file called backup.tar. We downloaded the file and extracted its contents.

We get the following files:

1
2
3
4
5
backup
├── index.php
├── lib.php
├── photos.php
└── upload.php

Let’s have a look at those files:

photos.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
<html>
<head>
<style type="text/css">
.tg  {border-collapse:collapse;border-spacing:0;margin:0px auto;}
.tg td{font-family:Arial, sans-serif;font-size:14px;padding:10px 5px;border-style:solid;border-width:1px;overflow:hidden;word-break:normal;border-color:black;}
.tg th{font-family:Arial, sans-serif;font-size:14px;font-weight:normal;padding:10px 5px;border-style:solid;border-width:1px;overflow:hidden;word-break:normal;border-color:black;}
.tg .tg-0lax{text-align:left;vertical-align:top}
@media screen and (max-width: 767px) {.tg {width: auto !important;}.tg col {width: auto !important;}.tg-wrap {overflow-x: auto;-webkit-overflow-scrolling: touch;margin: auto 0px;}}</style>
</head>
<body>
Welcome to our awesome gallery!</br>
See recent uploaded pictures from our community, and feel free to rate or comment</br>
<?php
require '/var/www/html/lib.php';
$path = '/var/www/html/uploads/';
$ignored = array('.', '..', 'index.html');
$files = array();

$i = 1;
echo '<div class="tg-wrap"><table class="tg">'."\n";

foreach (scandir($path) as $file) {
  if (in_array($file, $ignored)) continue;
  $files[$file] = filemtime($path. '/' . $file);
}
arsort($files);
$files = array_keys($files);

foreach ($files as $key => $value) {
  $exploded  = explode('.',$value);
  $prefix = str_replace('_','.',$exploded[0]);
  $check = check_ip($prefix,$value);
  if (!($check[0])) {
    continue;
  }
  // for HTB, to avoid too many spoilers
  if ((strpos($exploded[0], '10_10_') === 0) && (!($prefix === $_SERVER["REMOTE_ADDR"])) ) {
    continue;
  }
  if ($i == 1) {
    echo "<tr>\n";
  }

echo '<td class="tg-0lax">';
echo "uploaded by $check[1]<br>";
echo "<img src='uploads/".$value."' width=100px>";
echo "</td>\n";


  if ($i == 4) {
    echo "</tr>\n";
    $i = 1;
  } else {
    $i++;
  }
}
if ($i < 4 && $i > 1) {
    echo "</tr>\n";
}
?>
</table></div>
</body>
</html>

The photos.php file is the backend functionality of the mentioned gallery. It checks the uploads directory for newly added files and then displays them as img on the gallery page. Security-wise, everything seems to be fine.

lib.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
<?php

function getnameCheck($filename) {
  $pieces = explode('.',$filename);
  $name= array_shift($pieces);
  $name = str_replace('_','.',$name);
  $ext = implode('.',$pieces);
  #echo "name $name - ext $ext\n";
  return array($name,$ext);
}

function getnameUpload($filename) {
  $pieces = explode('.',$filename);
  $name= array_shift($pieces);
  $name = str_replace('_','.',$name);
  $ext = implode('.',$pieces);
  return array($name,$ext);
}

function check_ip($prefix,$filename) {
  //echo "prefix: $prefix - fname: $filename<br>\n";
  $ret = true;
  if (!(filter_var($prefix, FILTER_VALIDATE_IP))) {
    $ret = false;
    $msg = "4tt4ck on file ".$filename.": prefix is not a valid ip ";
  } else {
    $msg = $filename;
  }
  return array($ret,$msg);
}

function file_mime_type($file) {
  $regexp = '/^([a-z\-]+\/[a-z0-9\-\.\+]+)(;\s.+)?$/';
  if (function_exists('finfo_file')) {
    $finfo = finfo_open(FILEINFO_MIME);
    if (is_resource($finfo)) // It is possible that a FALSE value is returned, if there is no magic MIME database file found on the system
    {
      $mime = @finfo_file($finfo, $file['tmp_name']);
      finfo_close($finfo);
      if (is_string($mime) && preg_match($regexp, $mime, $matches)) {
        $file_type = $matches[1];
        return $file_type;
      }
    }
  }
  if (function_exists('mime_content_type'))
  {
    $file_type = @mime_content_type($file['tmp_name']);
    if (strlen($file_type) > 0) // It's possible that mime_content_type() returns FALSE or an empty string
    {
      return $file_type;
    }
  }
  return $file['type'];
}

function check_file_type($file) {
  $mime_type = file_mime_type($file);
  if (strpos($mime_type, 'image/') === 0) {
      return true;
  } else {
      return false;
  }  
}

function displayform() {
?>
<form action="<?php echo $_SERVER['PHP_SELF']; ?>" method="post" enctype="multipart/form-data">
 <input type="file" name="myFile">
 <br>
<input type="submit" name="submit" value="go!">
</form>
<?php
  exit();
}


?>

The lib.php implements several utility functions that are used in the photos.php and the uploads.php file. Nothing interesting stands out here.

upload.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
<?php
require '/var/www/html/lib.php';

define("UPLOAD_DIR", "/var/www/html/uploads/");

if( isset($_POST['submit']) ) {
  if (!empty($_FILES["myFile"])) {
    $myFile = $_FILES["myFile"];

    if (!(check_file_type($_FILES["myFile"]) && filesize($_FILES['myFile']['tmp_name']) < 60000)) {
      echo '<pre>Invalid image file.</pre>';
      displayform();
    }

    if ($myFile["error"] !== UPLOAD_ERR_OK) {
        echo "<p>An error occurred.</p>";
        displayform();
        exit;
    }

    //$name = $_SERVER['REMOTE_ADDR'].'-'. $myFile["name"];
    list ($foo,$ext) = getnameUpload($myFile["name"]);
    $validext = array('.jpg', '.png', '.gif', '.jpeg');
    $valid = false;
    foreach ($validext as $vext) {
      if (substr_compare($myFile["name"], $vext, -strlen($vext)) === 0) {
        $valid = true;
      }
    }

    if (!($valid)) {
      echo "<p>Invalid image file</p>";
      displayform();
      exit;
    }
    $name = str_replace('.','_',$_SERVER['REMOTE_ADDR']).'.'.$ext;

    $success = move_uploaded_file($myFile["tmp_name"], UPLOAD_DIR . $name);
    if (!$success) {
        echo "<p>Unable to save file.</p>";
        exit;
    }
    echo "<p>file uploaded, refresh gallery</p>";

    // set proper permissions on the new file
    chmod(UPLOAD_DIR . $name, 0644);
  }
} else {
  displayform();
}
?>

The uploads.php, as the name already says, implements the upload functionality of the web application. As this is usually the root of many vulnerabilities, we take a detailed look at the implementation:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
require '/var/www/html/lib.php';

define("UPLOAD_DIR", "/var/www/html/uploads/");

if( isset($_POST['submit']) ) {
  if (!empty($_FILES["myFile"])) {
    $myFile = $_FILES["myFile"];
    ...
} else {
  displayform();
}
?>

First, UPLOAD_DIR is set to the path /var/www/html/uploads/. Next, it is checked whether the submit post parameter is set. If not, the function displayform() is called. This function is defined in the lib.php and simply creates an HTML post form and displays it. However, if the submit parameter is set, the next step is to check the $_FILES["myFile"] parameter.

The global predefined variable $_FILES is an associative array containing items uploaded via HTTP POST method. Uploading a file requires HTTP POST method form with enctype attribute set to multipart/form-data. This _FILES array also contains the following properties:

  • $_FILES[‘file’][‘name’] - The original name of the file to be uploaded.
  • $_FILES[‘file’][‘type’] - The mime type of the file.
  • $_FILES[‘file’][‘size’] - The size, in bytes, of the uploaded file.
  • $_FILES[‘file’][‘tmp_name’] - The temporary filename of the file in which the uploaded file was stored on the server.
  • $_FILES[‘file’][‘error’] - The error code associated with this file upload.

This means the variable $myFile then contains all the information about the uploaded file. So let’s take a look at the code snippet that’s called if the post request has been successfully sent i.e. sent with all required parameters:

First, the check_file_type function is called, which is defined in the lib.php file. If check_file_type() is false (which is the case if the filetype is not image/) AND the filesize of tmp_name is lower than 60000 then the error message Invalid image file is shown and the post form is displayed again. Also, if error occurred during the upload, the error is shown and the post form is displayed.

1
2
3
4
5
6
7
8
9
10
    if (!(check_file_type($_FILES["myFile"]) && filesize($_FILES['myFile']['tmp_name']) < 60000)) {
      echo '<pre>Invalid image file.</pre>';
      displayform();
    }

    if ($myFile["error"] !== UPLOAD_ERR_OK) {
        echo "<p>An error occurred.</p>";
        displayform();
        exit;
    }

If it passes these tests, the function getNameUpload() is called, which is also defined in the lib.php file. This function replaces all underscores of the filename with dots and returns the filename and the extension of the file in an array. Then, a variable validext is defined as an array containing the file extensions of '.jpg', '.png', '.gif', '.jpeg'. If one of those validext elements is part of the filename, then the variable valid is set to true. If the filename is not valid i.e. none of the validext elements matches, then the image is determined to be invalid and an error message + the form is displayed on the website.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
    //$name = $_SERVER['REMOTE_ADDR'].'-'. $myFile["name"];
    list ($foo,$ext) = getnameUpload($myFile["name"]);
    $validext = array('.jpg', '.png', '.gif', '.jpeg');
    $valid = false;
    foreach ($validext as $vext) {
      if (substr_compare($myFile["name"], $vext, -strlen($vext)) === 0) {
        $valid = true;
      }
    }
    if (!($valid)) {
      echo "<p>Invalid image file</p>";
      displayform();
      exit;
    }

If the filename is valid, then the file is renamed to a name consisting of the $_SERVER['REMOTE_ADDR'] with all dots replaced by underscores followed by the determined file extension. Next the function move_uploaded_file is called to move the uploaded file to the specified upload directory. And finally, if the upload succeeded, the permissions of the file are set to 644 which basically means that the owner has RWX permissions and everyone else has Read and Write permissions on that file.

1
2
3
4
5
6
7
8
9
10
11
12
13
    $name = str_replace('.','_',$_SERVER['REMOTE_ADDR']).'.'.$ext;

    $success = move_uploaded_file($myFile["tmp_name"], UPLOAD_DIR . $name);
    if (!$success) {
        echo "<p>Unable to save file.</p>";
        exit;
    }
    echo "<p>file uploaded, refresh gallery</p>";

    // set proper permissions on the new file
    chmod(UPLOAD_DIR . $name, 0644);
  }

Initial Foothold

At the first glance, this seems like a solid upload mechanism. Several checks have been implemented to validate the filenames and the extensions. However, the flaw lies within the extension check: It is only checked if those extensions are a substring of the filename. E.g. uploading a file called .php.png would still pass this check and would thus be successfully uploaded.

1
2
3
4
5
6
7
  list ($foo,$ext) = getnameUpload($myFile["name"]);
  $validext = array('.jpg', '.png', '.gif', '.jpeg');
  $valid = false;
  foreach ($validext as $vext) {
    if (substr_compare($myFile["name"], $vext, -strlen($vext)) === 0) {
      $valid = true;
    }

This means, we can create a valid .png (or any other valid image format that is specified in the code) include php code and name it name.php.png. Now we just have to pray that the apache server is misconfigured i.e. it has a general handler for .php extensions instead of a decent FilesMatch which only checks for the .php extension at the very end.

Let’s try it. First, we create a black image with the following command (alternatively we could have also just created a file with the corresponding magic bytes for one of the abovementioned valid types):

1
convert -size 100x100 xc:#000000 test.png

Then, we use any viable editor to modify the raw test.png file to append php reverse shell code. Next, we rename the file to test.php.png and upload it. We get the message: file uploaded, refresh gallery. Perfect!

All we have to do now is to start a nc listener and access the uploaded file via the /photos.php endpoint to get a reverse shell!

Privilege Escalation - guly

Now that we have access to the system, we can check which other users exist (we are currently user apache). Looking at the /home directory, we see another user called guly. Inspecting his/her directory (as we have read permissions for whatever reason), we discover some interesting files:

1
2
-r--r--r--. 1 root root 782 Oct 30  2018 check_attack.php
-rw-r--r--  1 root root  44 Oct 30  2018 crontab.guly

The crontab.guly file is suspected to be a configuration file for a cronjob that is being executed by the user guly. And indeed! Looking at the file, we see a cronjob which executes the other interesting file, check_attack.php, every 3 minutes.

1
*/3 * * * * php /home/guly/check_attack.php

So let’s see if we can find some flaws in there which we can exploit:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<?php
require '/var/www/html/lib.php';
$path = '/var/www/html/uploads/';
$logpath = '/tmp/attack.log';
$to = 'guly';
$msg= '';
$headers = "X-Mailer: check_attack.php\r\n";

$files = array();
$files = preg_grep('/^([^.])/', scandir($path));

foreach ($files as $key => $value) {
        $msg='';
  if ($value == 'index.html') {
        continue;
  }
  #echo "-------------\n";

  #print "check: $value\n";
  list ($name,$ext) = getnameCheck($value);
  $check = check_ip($name,$value);

  if (!($check[0])) {
    echo "attack!\n";
    # todo: attach file
    file_put_contents($logpath, $msg, FILE_APPEND | LOCK_EX);

    exec("rm -f $logpath");
    exec("nohup /bin/rm -f $path$value > /dev/null 2>&1 &");
    echo "rm -f $path$value\n";
    mail($to, $msg, $msg, $headers, "-F$value");
  }
}

?>

A good habit is to always start by looking at keywords such as exec, or system that are typical sources of vulnerabilities. Here, we have two key points:

1
2
exec("rm -f $logpath");
exec("nohup /bin/rm -f $path$value > /dev/null 2>&1 &");

While $logpath and $path are hardcoded, we have full control of the $value variable:

1
2
3
$files = preg_grep('/^([^.])/', scandir($path));

foreach ($files as $key => $value) {

First, the directory /var/www/html/uploads/ is scanned for files. Afterwards, the $value variable is set to each of the filesnames. Thus, we have Command Injection in the file names of the uploads directory. This means, if we create a file containing a semicolon in its name followed by a command, this command will be executed, as no input sanitization is done on those filenames.

As seen in the screenshot, we create a file which contains a nc command that executes bash and connects back to our machine on port 5555.

And now we just have to wait until the cronjob is executed. Once done, we got access to the system as user guly

Privilege Escalation - root

Going through the basic enumeration steps like checking SUID, running services & processes, we discovered en entry in the sudoers list. Apparently, our current user guly is allowed to execute the file /usr/local/sbin/changename.sh with root privileges and without password:

1
2
[guly@networked ~]$ sudo -l
  (root) NOPASSWD: /usr/local/sbin/changename.sh

Let’s have a look at this file:

1
-rwxr-xr-x 1 root root 422 Jul  8  2019 /usr/local/sbin/changename.sh

Unfortunately, we cant modify the content of it, but we can read it. Let’s read the code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/bin/bash -p
cat > /etc/sysconfig/network-scripts/ifcfg-guly << EoF
DEVICE=guly0
ONBOOT=no
NM_CONTROLLED=no
EoF

regexp="^[a-zA-Z0-9_\ /-]+$"

for var in NAME PROXY_METHOD BROWSER_ONLY BOOTPROTO; do
        echo "interface $var:"
        read x
        while [[ ! $x =~ $regexp ]]; do
                echo "wrong input, try again"
                echo "interface $var:"
                read x
        done
        echo $var=$x >> /etc/sysconfig/network-scripts/ifcfg-guly
done
  
/sbin/ifup guly0

So what does this do? First, it creates a file called /etc/sysconfig/network-scripts/ifcfg-guly. Next, it iterates over the values NAME, PROXY_METHOD, BROWSER_ONLY and BOOTPROTO. For each of them, we are asked for an input via stdin. These values will the be stored in the created file with the format “IDENTIFIER=INPUT_VALUE” e.g. NAME=our input is here.

Once, we have provided input for all 4 configuration values, /sbin/ifup is called. This is a command to bring up an interface. Let’s do some research on how that works.

According to this article, the created file /etc/sysconfig/network-scripts/ifcfg-guly is a configuration file for the interface guly.

Every network interface has its own configuration file in the /etc/sysconfig/network-scripts directory. Each interface has a configuration file named ifcfg-X, where X is the number of the interface, starting with zero or 1 depending upon the naming convention in use; for example /etc/sysconfig/network-scripts/ifcfg-eth0 for the first Ethernet interface.

Another article describes an issue with this kind of network interface configuration. Apparently, including whitespaces in any of the options may lead to code execution as the code after the whitespace will be executed. So let’s try that:

There we go! The proof-of-concept works! Now we can use that to spawn a shell as root!

Perfect! It works. Final step is to obtain the root flag.

This post is licensed under CC BY 4.0 by the author.