Initial Reconnaissance

A quick nmap scan shows only ports 22 (SSH) and 80 (HTTP) open. Nothing interesting in service versions.

We run directory fuzzing and find some interesting paths:

  _|. _ _  _  _  _ _|_    v0.4.3
 (_||| _) (/_(_|| (_| )

Extensions: php, aspx, jsp, html, js | HTTP method: GET | Threads: 200 | Wordlist size: 11460

Output File: /home/kali/Desktop/HTB/TwoMillion/nmap/reports/http_2million.htb/__25-11-14_17-45-53.txt

Target: http://2million.htb/

[17:45:53] Starting: 
[17:45:53] 301 -  162B  - /js  ->  http://2million.htb/js/
[17:45:58] 200 -    2KB - /404
[17:46:06] 401 -    0B  - /api
[17:46:07] 401 -    0B  - /api/v1
[17:46:07] 403 -  548B  - /assets/
[17:46:07] 301 -  162B  - /assets  ->  http://2million.htb/assets/
[17:46:11] 403 -  548B  - /controllers/
[17:46:13] 301 -  162B  - /css  ->  http://2million.htb/css/
[17:46:15] 301 -  162B  - /fonts  ->  http://2million.htb/fonts/
[17:46:17] 302 -    0B  - /home  ->  /
[17:46:17] 301 -  162B  - /images  ->  http://2million.htb/images/
[17:46:17] 403 -  548B  - /images/
[17:46:19] 403 -  548B  - /js/
[17:46:20] 200 -    4KB - /login
[17:46:21] 302 -    0B  - /logout  ->  /
[17:46:29] 200 -    4KB - /register
[17:46:37] 301 -  162B  - /views  ->  http://2million.htb/views/

Task Completed

Trying to register asks for an invite code we don’t have.

However, there is a page where we can verify if a code is valid. The validation is done with the following JavaScript:

$(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!") {
                    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.");
            }
        });
    });
});

There is also an obfuscated file inviteapi.min.js:

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, {}))

After deobfuscating it we get:

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); }
    });
}

The makeInviteCode function is exactly what we need – it tells us how to generate a real invite code.

Intrusion – Getting the Invite Code

We make a POST request to the endpoint it mentions:

curl -L -X POST "http://10.10.11.221/api/v1/invite/how/to/generate"

First response (ROT13 encrypted):

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

Decode ROT13 → “In order to generate the invite code, make a POST request to /api/v1/invite/generate”

❯ echo "Va beqre gb trarengr gur vaivgr pbqr, znxr n CBFG erdhrfg gb /ncv/i1/vaivgr/trarengr" | tr 'A-Za-z' 'N-ZA-Mn-za-m'
In order to generate the invite code, make a POST request to /api/v1/invite/generate

Second POST request returning Base64 code

We get a Base64 string ending with =:

❯ echo "QTBBQ0ktSkY4N1AtNTRNSE0tS1o1NFU=" | base64 -d
A0ACI-JF87P-54MHM-KZ54U% 

We paste the code, register with dummy credentials and we’re in!

Inside the Application

On hover over the VPN download button we see it calls a special endpoint.

Fuzzing /api/v1/user/ reveals the auth endpoint that returns user info, including is_admin.

User auth endpoint showing admin exists

We try to see if there is an endpoint for admin in the API making a request to/api/v1/admin and we see that it exists (no 404 error).

As we don't see too much information, we do a request to /api/v1/, where we discover all the endpoints available of the API:

Original request in Burp

In Burp we modify our user JSON to is_admin: 1:

Original request in Burp

Now we can generate VPN configs as admin!

VPN generation working as admin

RCE → Initial Shell

The VPN generation likely runs something like generate_vpn.sh $username. So we will try a command injection to see if it works:

Command injection success

We get RCE as www-data. We try to send a typical reverse shell but we can't (it might be blocked as we are executing commands as www-data.

Looking around /var/www/html we find .env with DB credentials (which are also valid for SSH as user admin).

.env location

.env contents

Although we are logged in as an admin user, we see that we don't have sudo privileges.

SSH as admin

But we obtain the user flag.

Privilege Escalation

linpeas.sh doesn’t show anything obvious. After a while searching and reading files and directories of the machine, I see that at /var/mail we find an interesting email:

From: ch4p 
To: admin 
Cc: g0blin 
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

By searching at internet we see that it hints at CVE-2023-0386. We download the exploit from:

https://github.com/sxlmnwb/CVE-2023-0386

Now we just follow the steps of the exploit:

Root shell obtained

Root flag → machine pwned!