Module: Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus::TargetInfo

Includes:
StatusCodes, URIs, Msf::Exploit::Remote::HttpClient
Included in:
Msf::Exploit::Remote::HTTP::ManageEngineAdauditPlus, Login
Defined in:
lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/target_info.rb

Constant Summary

Constants included from StatusCodes

StatusCodes::CONNECTION_FAILED, StatusCodes::NO_ACCESS, StatusCodes::NO_BUILD_NUMBER, StatusCodes::NO_DOMAINS, StatusCodes::SUCCESS, StatusCodes::UNEXPECTED_REPLY

Instance Attribute Summary

Attributes included from Msf::Exploit::Remote::HttpClient

#client, #cookie_jar

Instance Method Summary collapse

Methods included from URIs

#adaudit_api_alertprofiles_save_uri, #adaudit_api_js_message_uri, #adaudit_plus_configured_domains_uri, #adaudit_plus_gpo_watcher_data_uri, #adaudit_plus_jump_to_js_uri, #adaudit_plus_license_details_uri, #adaudit_plus_login_uri

Methods included from Msf::Exploit::Remote::HttpClient

#basic_auth, #cleanup, #configure_http_login_scanner, #connect, #connect_ws, #deregister_http_client_options, #disconnect, #download, #full_uri, #handler, #http_fingerprint, #initialize, #lookup_http_fingerprints, #normalize_uri, #path_from_uri, #peer, #proxies, #reconfig_redirect_opts!, #request_opts_from_url, #request_url, #rhost, #rport, #send_request_cgi, #send_request_cgi!, #send_request_raw, #service_details, #setup, #ssl, #ssl_version, #strip_tags, #target_uri, #validate_fingerprint, #vhost

Methods included from Auxiliary::Report

#active_db?, #create_cracked_credential, #create_credential, #create_credential_and_login, #create_credential_login, #db, #db_warning_given?, #get_client, #get_host, #inside_workspace_boundary?, #invalidate_login, #mytask, #myworkspace, #myworkspace_id, #report_auth_info, #report_client, #report_exploit, #report_host, #report_loot, #report_note, #report_service, #report_vuln, #report_web_form, #report_web_page, #report_web_site, #report_web_vuln, #store_cred, #store_local, #store_loot

Methods included from Metasploit::Framework::Require

optionally, optionally_active_record_railtie, optionally_include_metasploit_credential_creation, #optionally_include_metasploit_credential_creation, optionally_require_metasploit_db_gem_engines

Methods included from StatusCodes

#adaudit_plus_status

Instance Method Details

#adaudit_plus_grab_build(adapcsrf_cookie) ⇒ Hash

Check the build number for the ADAudit Plus installation

Parameters:

  • adapcsrf_cookie (String)

    A valid ADAP CSRF cookie for API calls.

Returns:

  • (Hash)

    Hash containing a `status` key, which is used to hold a status value as an Integer value, a `message` key, which is used to hold a message associated with the status value as a String, and an optional 'build_version' key, which is used to hold an object of type Rex::Version if the build number was successfully obtained.

See Also:



202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
# File 'lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/target_info.rb', line 202

def adaudit_plus_grab_build(adapcsrf_cookie)
  vprint_status('Attempting to obtain the ADAudit Plus build number')

  res = send_request_cgi({
    'uri' => adaudit_plus_license_details_uri,
    'method' => 'POST',
    'keep_cookies' => true,
    'vars_post' => { 'adapcsrf' => adapcsrf_cookie.to_s }
  })

  unless res
    return {
      'status' => adaudit_plus_status::CONNECTION_FAILED,
      'message' => 'Connection failed while attempting to obtain the build number.'
    }
  end

  unless res.code == 200
    return {
      'status' => adaudit_plus_status::UNEXPECTED_REPLY,
      'message' => "Received unexpected HTTP response #{res.code} when attempting to obtain the build number."
    }
  end

  build = res.body&.scan(/"buildNumber":"(\s*\d{4}\s*)",/)&.flatten&.first
  if build.blank?
    return {
      'status' => adaudit_plus_status::NO_BUILD_NUMBER,
      'message' => 'No build number was obtained.'
    }
  end

  unless build.strip =~ /^\d{4}$/
    return {
      'status' => adaudit_plus_status::UNEXPECTED_REPLY,
      'message' => "Received an invalid build number: #{build}"
    }
  end

  {
    'status' => adaudit_plus_status::SUCCESS,
    'message' => "The target is ADAudit Plus #{build}",
    'build_version' => Rex::Version.new(build)
  }
end

#adaudit_plus_grab_configured_domains(adapcsrf_cookie, only_get_cookie = false) ⇒ Hash

Performs an API call to obtain the configured domains. The adapcsrf cookie obtained from this request is necessary to perform further authenticated actions.

Parameters:

  • adapcsrf_cookie (String)

    A valid adapcsrf_cookie obtained via a successful login action

  • only_get_cookie (Boolean) (defaults to: false)

    If this is enabled, the method will only try to obtain an 'adapcsrf' cookie that is required to perform API calls.

Returns:

  • (Hash)

    Hash containing a `status` key, which is used to hold a status value as an Integer value, an optional `message` key, which is used to hold a message associated with the status value as a String, an optional `adapcsrf_cookie` key which maps to a String containing the adapcsrf cookie to be used for authentication purposes, and an optional `configured_domains` key which maps to an Array of Strings, each containing a domain name that has been configured to be used by the ManageEngine ADAudit Plus target.



98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
# File 'lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/target_info.rb', line 98

def adaudit_plus_grab_configured_domains(adapcsrf_cookie, only_get_cookie = false)
  vprint_status('Attempting to obtain the list of configured domains...') unless only_get_cookie

  res = send_request_cgi({
    'uri' => adaudit_plus_configured_domains_uri,
    'method' => 'POST',
    'keep_cookies' => true,
    'vars_post' => {
      'JSONString' => '{"checkGDPR":true}',
      'adapcsrf' => adapcsrf_cookie.to_s
    }
  })

  if only_get_cookie
    purpose = 'obtain the adapcsrf cookie required to perform API calls'
  else
    purpose = 'obtain the list of configured domains'
  end

  unless res
    return {
      'status' => adaudit_plus_status::CONNECTION_FAILED,
      'message' => "Connection failed while attempting to #{purpose}."
    }
  end

  # if we didn't get an expected response, we should always return since we won't be able to return the domains and/or a valid cookie
  unless res.code == 200 && res.body&.include?('domainFullList')
    return {
      'status' => adaudit_plus_status::UNEXPECTED_REPLY,
      'message' => "Unexpected reply while attempting to #{purpose}."
    }
  end

  # try to obtain the adapcsrf cookie
  adapcsrf_cookie = cookie_jar.cookies.select { |k| k.name == 'adapcsrf' }&.first
  got_cookie = adapcsrf_cookie && adapcsrf_cookie.value.present? ? true : false

  # if we have no valid cookie there is no point in continuing
  unless got_cookie
    return {
      'status' => adaudit_plus_status::NO_ACCESS,
      'message' => 'Failed to obtain the adapcsrf cookie required to perform API calls'
    }
  end

  # if we only wanted to obtain the cookie, we can return here
  if only_get_cookie
    return {
      'status' => adaudit_plus_status::SUCCESS,
      'message' => 'Obtained the adapcsrf cookie required to perform API calls!',
      'adapcsrf_cookie' => adapcsrf_cookie.value
    }
  end

  # if we are here, we want to obtain the configured domains as well as the cookie
  configured_domains = []
  begin
    domain_info = JSON.parse(res.body)
    if domain_info && domain_info.include?('domainFullList') && !domain_info['domainFullList'].empty?
      domain_full_list = domain_info['domainFullList']
      domain_full_list.each do |domain|
        next unless domain.is_a?(Hash) && domain.key?('name')

        domain_name = domain['name']
        next if domain_name.empty?

        configured_domains << domain_name
      end
    else
      print_error('Failed to identify any configured domains.')
    end
  rescue JSON::ParserError => e
    print_error('Failed to identify any configured domains - The server response did not contain valid JSON.')
    print_error("Error was: #{e.message}")
  end

  if configured_domains.empty?
    return {
      'status' => adaudit_plus_status::NO_DOMAINS,
      'message' => 'Failed to obtain the list of configured domains.',
      'adapcsrf_cookie' => adapcsrf_cookie.value
    }
  end

  print_status("Found #{configured_domains.length} configured domain(s): #{configured_domains.join(', ')}")
  {
    'status' => adaudit_plus_status::SUCCESS,
    'message' => 'Obtained the adapcsrf cookie required to perform API calls along with the configured domains!',
    'adapcsrf_cookie' => adapcsrf_cookie.value,
    'configured_domains' => configured_domains
  }
end

#adaudit_plus_grab_domain_aliases(res_body) ⇒ Hash

Extract the configured aliases for the configured Active Directory domains from a HTTP response body.

Parameters:

  • res_body (String)

    HTTP response body obtained via a GET request to the ADAudit Plus base path

Returns:

  • (Hash)

    Hash containing a `status` key, which is used to hold a status value as an Integer value, a `message` key, which is used to hold a message associated with the status value as a String, and a 'domain_aliases' key, which holds an Array of Strings for the configured domain aliases, or an empty Array if no domain aliases were found.



52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
# File 'lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/target_info.rb', line 52

def adaudit_plus_grab_domain_aliases(res_body)
  doc = ::Nokogiri::HTML(res_body)
  css_dom_name = doc.css('select#domainName')&.first
  domain_aliases = []

  no_domains_response = {
    'status' => adaudit_plus_status::NO_DOMAINS,
    'message' => 'No configured Active Directory domains were found.',
    'domain_aliases' => domain_aliases
  }

  return no_domains_response if css_dom_name.blank?

  css_configured_domains = css_dom_name.css('option')
  return no_domains_response if css_configured_domains.blank?

  css_configured_domains.each do |domain|
    next unless domain&.keys&.include?('value')
    value = domain['value']
    domain_aliases << value
  end

  return no_domains_response if domain_aliases.empty?

  {
    'status' => adaudit_plus_status::SUCCESS,
    'message' => "Identified #{domain_aliases.length} configured authentication domain(s): #{domain_aliases.join(', ')}",
    'domain_aliases' => domain_aliases
  }
end

#adaudit_plus_target_checkHash

Check that a target is likely running ManageEngine ADAudit Plus

Returns:

  • (Hash)

    Hash containing a `status` key, which is used to hold a status value as an Integer value, a `message` key, which is used to hold a message associated with the status value as a String, and an optional 'server_response' key, which is used to hold the response body (String) received from the server.



15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# File 'lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/target_info.rb', line 15

def adaudit_plus_target_check
  res = send_request_cgi({
    'uri' => normalize_uri(target_uri.path),
    'method' => 'GET'
  })

  unless res
    return {
      'status' => adaudit_plus_status::CONNECTION_FAILED,
      'message' => 'Connection failed.'
    }
  end

  if res.code == 200 && res.body =~ /<title>ADAudit Plus/
    return {
      'status' => adaudit_plus_status::SUCCESS,
      'message' => 'The target appears to be MangeEngine ADAudit Plus',
      'server_response' => res.body
    }
  end

  {
    'status' => adaudit_plus_status::UNEXPECTED_REPLY,
    'message' => 'The target does not appear to be MangeEngine ADAudit Plus',
  }
end

#gpo_watcher_data_checkInteger

Check if the GPOWatcherData endpoint is available

Returns:

  • (Integer)

    Status code



251
252
253
254
255
256
257
258
259
260
261
# File 'lib/msf/core/exploit/remote/http/manage_engine_adaudit_plus/target_info.rb', line 251

def gpo_watcher_data_check
  res = send_request_cgi({
    'uri' => adaudit_plus_gpo_watcher_data_uri,
    'method' => 'POST'
  })

  return adaudit_plus_status::CONNECTION_FAILED unless res
  return adaudit_plus_status::NO_ACCESS unless res.code == 200

  adaudit_plus_status::SUCCESS
end