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.