Using the ConfigMgr AdminService to Retrieve BitLocker Recovery Keys and Triggering Key Rotation
November 17, 2021
The Key to Success is Knowledge
Recently Garth Jones accused me of knowing something that I knew nothing about and I was very offended by that. So much so, that when Bryan Dam came to me demanding to know the keys to BitLocker keys in ConfigMgr, I decided I should figure it out. So I did. Here’s what I know now:
Keying in on the Issue
When trying to automate processes around ConfigMgr, there are Ways to do things then there are Supported Ways to do things. Bryan asked if I knew Supported Ways to extract a Bitlocker recovery key from the ConfigMgr database in a way that marks the key as disclosed and forces the client device to rotate keys. Sure sounded like Garth put him up to this.
The Key to Success
Enter AdminService. You remember that guy right? It’s been a while since I’ve written anything about it and I can honestly say, it has come a LONG way. I may need to update my guide soon. The AdminService is now used as part of the backend that enabled Cloud Attach (formerly Tenant Attach) to integrate into the Microsoft Endpoint Manager Admin Console (we all still just call it Intune :-)).
When you enable Cloud Attach, you get access to ConfigMgr attributes and methods that have always been in-console-only items.
One of these items is the Recovery Keys blade. It allows you to, yep, you guessed it, see BitLocker recovery keys for your ConfigMgr managed devices. When you click the Recovery keys (preview) blade, you will see a list of keys and a link on each one to view the Recovery Key.
When you click on Show recovery key you will be notified that the key will be marked disclosed and that this will trigger a key rotation on the client. This is exactly what we want!
What’s great about the AdminService is that it has a verbose log file. You can find the log in the install directory of your SMS Provider server (generally your primary) AdminService.log. If you watch the log as you navigate in the MEM portal and click through to display the recovery key, you will see each call back to the AdminService to retrieve the data which is then displayed in the web console. The logs look like this:
Processing incoming request for resource [https://cm01.asd.net/AdminService/v1.0/Device?$filter=SMSID eq 'GUID:3d03b4dd-2007-47da-af02-83800df961c0'&$select=MachineId,ADSiteName,CNLastOnlineTime,CNLastOfflineTime,CNAccessMP,CurrentLogonUser,CoManaged,CA_IsCompliant,ClientVersion,Domain,IsApproved,IsVirtualMachine,LastPolicyRequest,LastMPServerName,LastActiveTime,DeviceOSBuild,CNIsOnInternet,CNIsOnline,MACAddress,SiteCode], method: [GET], User - [NT AUTHORITY\SYSTEM]
Header: [ServiceNotification]=[**************]
Header: [Authorization]=[**************]
Header: [Host]=[cm01.asd.net]
Context: [RemoteIpAddress]=[fe80::9d59:e444:736c:f5e2%4]
Context: [RemotePort]=[55641]
Context: [ContentType]=[]
Context: [Accept]=[]
Received request from the notification channel.
Successfully validated request from Service Connection Point.
Successfully validated user [10d75920-6cf5-48de-955c-e856de2a39de,S-1-5-21-1909024835-672986419-4090565466-1624] from tenant [**************].
Successfully logged on user using user principal name Adam@ASD.NET.
Provider authentication level and exception list not present or expired. Retrieving from database.
User ASD\Adam is allowed because it is validated with current authentication level Default.
Get all instances of Device.
Completing request with response code [200] reason [OK]
Processing incoming request for resource [https://cm01.asd.net/AdminService/v1.0/Device(16777377)/RecoveryKeys], method: [GET], User - [NT AUTHORITY\SYSTEM]
Header: [ServiceNotification]=[**************]
Header: [Authorization]=[**************]
Header: [Host]=[cm01.asd.net]
Context: [RemoteIpAddress]=[fe80::9d59:e444:736c:f5e2%4]
Context: [RemotePort]=[55646]
Context: [ContentType]=[]
Context: [Accept]=[]
Received request from the notification channel.
Successfully validated request from Service Connection Point.
Successfully validated user [10d75920-6cf5-48de-955c-e856de2a39de,S-1-5-21-1909024835-672986419-4090565466-1624] from tenant [**************].
Successfully logged on user using user principal name Adam@ASD.NET.
Provider authentication level and exception list up to date.
User ASD\Adam is allowed because it is validated with current authentication level Default.
Completing request with response code [200] reason [OK]
Processing incoming request for resource [https://cm01.asd.net/AdminService/v1.0/Device(16777377)/AdminService.GetRecoveryKeyValue], method: [POST], User - [NT AUTHORITY\SYSTEM]
Header: [ServiceNotification]=[**************]
Header: [Content-Length]=[56]
Header: [Content-Type]=[application/json]
Header: [Authorization]=[**************]
Header: [Expect]=[100-continue]
Header: [Host]=[cm01.asd.net]
Context: [RemoteIpAddress]=[fe80::9d59:e444:736c:f5e2%4]
Context: [RemotePort]=[62213]
Context: [ContentType]=[application/json]
Context: [Accept]=[]
Received request from the notification channel.
Successfully validated request from Service Connection Point.
Successfully validated user [10d75920-6cf5-48de-955c-e856de2a39de,S-1-5-21-1909024835-672986419-4090565466-1624] from tenant [**************].
Successfully logged on user using user principal name Adam@ASD.NET.
Provider authentication level and exception list up to date.
User ASD\Adam is allowed because it is validated with current authentication level Default.
Get instance of Device with key '16777377'User ASD\Adam requests to read value of recovery key 5eb3baec-4e9b-49f4-9aee-90d3554aef05 on device 16777377 (CMCB-WS01).
Completing request with response code [200] reason [OK]
The above log entries show 3 calls to the AdminService and although some of the data is obfuscated, we can piece it all together.
#Using a GET Method:#Get the Device:https://cm01.asd.net/AdminService/v1.0/Device?$filter=SMSID eq 'GUID:3d03b4dd-2007-47da-af02-83800df961c0'&$select=MachineId,ADSiteName,CNLastOnlineTime,CNLastOfflineTime,CNAccessMP,CurrentLogonUser,CoManaged,CA_IsCompliant,ClientVersion,Domain,IsApproved,IsVirtualMachine,LastPolicyRequest,LastMPServerName,LastActiveTime,DeviceOSBuild,CNIsOnInternet,CNIsOnline,MACAddress,SiteCode
#Base Device Entityhttps://cm01.asd.net/AdminService/v1.0/Device
#ODATA syntax to filter to find the specific device using SMSID and passing in the device GUID?$filter=SMSID eq 'GUID:3d03b4dd-2007-47da-af02-83800df961c0'# Select statement to explicitly return device properties. We can exclude this if we just want everything back.&$select=MachineId,ADSiteName,CNLastOnlineTime,CNLastOfflineTime,CNAccessMP,CurrentLogonUser,CoManaged,CA_IsCompliant,ClientVersion,Domain,IsApproved,IsVirtualMachine,LastPolicyRequest,LastMPServerName,LastActiveTime,DeviceOSBuild,CNIsOnInternet,CNIsOnline,MACAddress,SiteCode
#Using a GET Method:#Get the list of recovery key ids for the specific devicehttps://cm01.asd.net/AdminService/v1.0/Device(16777377)/RecoveryKeys
#Base Device Entityhttps://cm01.asd.net/AdminService/v1.0/Device
#Device's ResourceId/MachineId from the ConfigMgr database.(16777377)
#RecoveryKeys entity to retrieve the key ids/RecoveryKeys
#Using a POST Method:#Post data to the GetRecoveryKeyValue action/function to return the recovery key valueshttps://cm01.asd.net/AdminService/v1.0/Device(16777377)/AdminService.GetRecoveryKeyValue
#Base Device Entityhttps://cm01.asd.net/AdminService/v1.0/Device
#Device's ResourceId/MachineId from the ConfigMgr database.(16777377)
#GetRecoveryKeyValue Action/function to submit a body with the recovery id key from the previous call./AdminService.GetRecoveryKeyValue
#Not shown in the logs - since this is a POST method, we need to send in JSON a body. The format for the body is found in the metadata XML I'll show later in the post.{
"RecoveryKeyId":"5eb3baec-4e9b-49f4-9aee-90d3554aef05"}
#Response Data{
"value":"161964-088066-081752-251955-663949-379379-473033-388113"}
From a client machine, this is equivalent to running manage-bde -protectors -get c: and retrieving the Numerical Password ID and Password as shown below.
Take note of the NavigationProperty above. This indicates that we can retrieve the recovery key info FROM another entity, in this case, from a Device entity. If we scroll a little more we will see the Action or Function that we want to call GetRecoveryKeyValue
From this snippet we can see that we call this action on a Device entity bindingParameter and have to pass in a body with parameters RecoveryKeyId. This is the bit that translates to these lines from above:
Using Fiddler, you can trigger this command to test it out.
There can only be one Master Key or is it Key Master?
For this next part I was going to show you how to watch the client rotate the key, but it turns out that my lab’s MBAM instance may no longer be in existence, so instead I’ll direct you over to the always thorough and informative, Niall Brady!
Have you Recovered from all of the Key puns yet?
So after digging through all of these thing we have all of the pieces we need to write script, or build this into an enterprise-class 3rd party product so people can leverage it other places…
#The script assumes you have the resource ID.param(
[string]$ServerName [int]$ResourceID)
#Get the Device$Device = Invoke-RestMethod -Uri "https://$($ServerName)/AdminService/v1.0/Device($($ResourceID))" -Method Get -UseDefaultCredentials
#Get the recovery key IDs$KeyIDs = Invoke-RestMethod -Uri "https://$($ServerName)/AdminService/v1.0/Device($($ResourceID))/RecoveryKeys" -Method Get -UseDefaultCredentials
#Loop through the Ids and return the Recovery Keys$KeyIDs.value | ForEach-Object {
$Body = @{RecoveryKeyId = $_.RecoveryKeyId} | ConvertTo-Json$Keys = Invoke-RestMethod -Uri "https://$($ServerName)/AdminService/v1.0/Device($($ResourceID))/AdminService.GetRecoveryKeyValue" -Method Post -Body $Body -UseDefaultCredentials -ContentType "application/json"$Keys}
#Result#value#-----#161964-088066-081752-251955-663949-379379-473033-388113
KEYping up is Hard to Do
Well that’s pretty much it. Hope you learned something new, I sure did. Thanks Bryan and Garth for the inspiration to give it go. Hope to see you at MMSMOA in May 2022!