CVE-2018-8412: MS Office 2016 for Mac Privilege Escalation via a Legacy Package

Code signature bypass and insecure sideloading gives root.

This issue affects Microsoft Office for Mac 2016, and SkypeForBusiness (16.17.0.65)

This report covers two main flaws:

XPC Validation Bypass

In /Library/PrivilegedHelperTools/com.microsoft.autoupdate.helper there’s a XPC service com.microsoft.autoupdate.helper.

It’s based on NSXPCConnection and only exports two methods:

@protocol MAUHelperToolProtocol
- (void)logString:(NSString *)arg1 atLevel:(int)arg2 fromAppName:(NSString *)arg3;
- (void)installUpdateWithPackage:(NSString *)arg1 withXMLPath:(NSString *)arg2 withReply:(void (^)(NSString *))arg3;
@end

Only Microsoft signed binaries are allowed:

char __cdecl -[MAUHelperTool listener:shouldAcceptNewConnection:](MAUHelperTool *self, SEL a2, id a3, id a4)
{
  // ...
  caller_pid = (unsigned __int64)objc_msgSend(v6, "processIdentifier", self);
  ksecguestattrpid = kSecGuestAttributePid;
  number_with_pid = objc_msgSend(&OBJC_CLASS___NSNumber, "numberWithInt:", caller_pid);
  pid_as_nsnumber = objc_retainAutoreleasedReturnValue(number_with_pid);
  _dict = objc_msgSend(
    &OBJC_CLASS___NSDictionary,
    "dictionaryWithObjects:forKeys:count:",
    &pid_as_nsnumber,
    &ksecguestattrpid,
    1LL);
  attributes = objc_retainAutoreleasedReturnValue(_dict);
  objc_release(pid_as_nsnumber);
  guest_code = 0LL;
  v12 = 0;
  if ( !(unsigned int)SecCodeCopyGuestWithAttributes(0LL, attributes, 0LL, &guest_code) )// kSecCSDefaultFlags
  {
    v43 = 0LL;
    v12 = 0;
    if ( !(unsigned int)SecRequirementCreateWithString(
      CFSTR("(identifier \"com.microsoft.autoupdate2\" or identifier \"com.microsoft.autoupdate.fba\") 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.13] and certificate leaf[subject.OU] = UBF8T346G9"),
      0LL,
      &v43))
      v12 = (unsigned int)SecCodeCheckValidity(guest_code, 0LL, v43) == 0;
  if ( v43 )
    CFRelease(v43);

Here are two (potential) ways to bypass.

First, it uses pid, which can not be trusted since exec* family functions can replace current process to a new one, leaving the previous pid untouched. See MacOS/iOS userspace entitlement checking is racy and Don’t Trust the PID!

Actually this way is unexploitable. The invalidation handler is triggered when the caller tries to replace itself, which cause the -[MAUHelperTool shouldExit] method to return true.

 v30 = _NSConcreteStackBlock;
 v31 = -1040187392;
 v32 = 0;
 v33 = sub_100002748;
 v34 = &unk_100008440;
 v19 = (void *)objc_retain(v27, v7);
 v35 = v19;
 objc_copyWeak(&v36, &v43);
 objc_msgSend(v7, "setInvalidationHandler:", &v30);
 v20 = objc_msgSend(v19, "loggingConnections");
 v21 = (void *)objc_retainAutoreleasedReturnValue(v20);
 objc_msgSend(v21, "performSelectorOnMainThread:withObject:waitUntilDone:", "addObject:", v7, 1LL);
 objc_release(v21);
__int64 __fastcall sub_100002748(__int64 a1)
{
 void *v1; // rax
 void *v2; // r14
 __int64 v3; // rbx v1 = objc_msgSend(*(void **)(a1 + 32), "loggingConnections");
 v2 = (void *)objc_retainAutoreleasedReturnValue(v1);
 v3 = objc_loadWeakRetained(a1 + 40);
 objc_msgSend(v2, "performSelectorOnMainThread:withObject:waitUntilDone:", "removeObject:", v3, 1LL);
 objc_release(v3);
 return objc_release(v2);
}

Then the main event loop will terminate the process.

But another way is to spawn a valid process with DYLD_* env that enables dynamic code injection.

Since following files are not protected, they can be abused to bypass the signature check.

To protect these binaries, use any one of the following:

So now I have the ability to talk to the XPC. Messing up with the log looks useless. Inside -[MAUHelperTool installUpdateWithPackage:withXMLPath:withReply:], it accepts a path from XPC client and install it. But the daemon locks the package file and also perform digital signature validation on it!

if ( v15 || !v16 )
{
  v22 = v38;
  v23 = objc_msgSend(v38, "lockPackage:", v7);
  V41 = (void *)objc_retainAutoreleasedReturnValue(v23);
  if (objc_msgSend(v41, "isEqualToString:", CFSTR("Success"))) {
    v24 = objc_msgSend(v22, "validatePackageOwnership:", v7);
    v42 = (void *)objc_retainAutoreleasedReturnValue(v24);
    if (objc_msgSend(v42, "isEqualToString:", CFSTR("Success")) {
      objc_msgSend(v22, "setInstallingUpdate:", 1LL);
      obj_msgSend(v22, "installPackage:withCustomChoiceXMLPath:", v7, v40);

Insecure Module Loading in A Legacy SilverLight Package

I was unable to bypass the signature validation on pkg file and this issue seemed unexploitable. I made a decision: not to bypass.

After inspecting some valid packages, I found this legacy SilverLight installer: https://www.microsoft.com/getsilverlight/Get-Started/Install/Default

It’s trusted of course:

$ pkgutil --check-sign /Volumes/Silverlight/silverlight.pkg
Package "silverlight.pkg":
 Status: signed by a certificate trusted by Mac OS X
 Certificate Chain:
 1. Developer ID Installer: Microsoft Corporation (UBF8T346G9)
 SHA1 fingerprint: 9B 6B 91 3B B1 3F 68 26 12 20 EC 72 11 F0 F2 0E 92 E4 B1 EB
 -----------------------------------------------------------------------------
 2. Developer ID Certification Authority
 SHA1 fingerprint: 3B 16 6C 3B 7D C4 B7 51 C9 FE 2A FA B9 13 56 41 E3 88 E1 86
 -----------------------------------------------------------------------------
 3. Apple Root CA
 SHA1 fingerprint: 61 1E 5B 66 2C 59 3A 08 FF 58 D1 4A E2 24 52 D1 98 DF 6C 60

The post-install scripts caught my attention.

Set global write permission:

pushd /Library/Internet\ Plug-Ins/
rm -rf WPFe.plugin/
chown -R root:admin Silverlight.plugin/
chmod -R 775 Silverlight.plugin/
popdpushd /Library/Application\ Support/Microsoft/
chown -R root:admin Silverlight/
chmod -R 775 Silverlight/
popdpushd /Library/Application\ Support/
chown root:admin Microsoft/
chmod 775 Microsoft/Interesting commands:

_PRIBX=`ls -r "/Library/Application Support/Microsoft/PlayReady/Cache" | grep .key | awk '{if (NR==1) {print $1}}' `
 if [ "$_PRIBX" ]
 then
 _PRIBXVER=`./PlayReadyGetIBXVersionTool "/Library/Application Support/Microsoft/PlayReady/Cache/"$_PRIBX`
 if [ "$_PRIBXVER" = "mspribx.1.5.8" ]pushd "/tmp/SilverlightInstallTools"
_SPRDResult=`./rundylib "/Library/Internet Plug-Ins/Silverlight.plugin/Contents/MacOS/SLMSPRBootstrap.dylib"`(it's writable after previous chmod)

So what do they do?

rundylib, just as its name

int __cdecl main(int argc, const char **argv, const char **envp)
{
 ...
 v3 = argv[1];
 if ( !v3 )
 {
 puts("ERROR: Invalid path ");
 return 1;
 }
 v5 = dlopen(v3, 5);
}

What about PlayReadyGetIBXVersionTool?

signed int __cdecl GetDyLibVersion(const char *path, unsigned int *a2, unsigned int *a3, unsigned int *a4)
{
  // ...
  handle = dlopen(path, 1);
  if ( handle )
  {
    v6 = _dyld_image_count();
    for ( i = 0; ; ++i ) {
      if ( i == v6 )
        goto LABEL_22;
      v8 = _dyld_get_image_name(i);
      if ( !v8 )
      {
        v9 = dlerror();
        printf("Image name not found or index out of range. Error: %s\n", v9);
        v5 = 5;
        goto LABEL_21;
      }
      if ( !strcmp(v8, path) )
        break;
    }
    v10 = _dyld_get_image_header(i);

It just loads and executes a shared library from the “Cache”, in a privileged process, just to get its version.

Both /Library/Internet Plug-Ins/Silverlight.plugin/Contents/MacOS/SLMSPRBootstrap.dylib and /Library/Application Support/Microsoft/PlayReady/Cache are globally writable. The only difference is that the former needs to win the race. I prefer stabler exploit.

The Exploit

It’s deadly simple.

  1. DYLD_INSERT_LIBRARIES to inject to “Microsoft AutoUpdate”
  2. Place the vulnerable SilverLight installer somewhere, send the XPC request to updaterhelper to ask for installation
  3. Create cache folder and place the shared library for root context
  4. The installer gets executed and our malicious code was loaded by a privileged process

Demo: