Closed dmulder closed 8 months ago
Additionally, this may allow a 'blessed' user authentication (such as how MS's current Intune broker does now).
Here is a blog explaining some of it: https://jairocadena.com/2016/02/01/azure-ad-join-what-happens-behind-the-scenes/
The Intune portal prints the following debug which may be useful:
2023-09-11 13:55:28 INFO get_client{capability="LinuxEnrollmentService" resource=ResourceId("XXXXX") endpoint="https://fef.msua08.manage.microsoft.com/LinuxMDM/LinuxEnrollmentService/" endpoint="https://fef.msua08.manage.microsoft.com/LinuxMDM/LinuxEnrollmentService/"}: Requesting a token silently resource=ResourceId("XXXXX")
I thought device registration was happening via this graph request, but the Azure AD audit logs indicate there is a difference between a device add and a device enrollment (which externally MS refers to as a join). Both processes create a device object, but perhaps device add isn't fully functional?
Here is the definition of the alternativeSecurityIds used in the device add: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dvrj/f900e812-8f1c-4345-9ab0-b91111068651
It is also base64 encoded and using widechars (the spec says it is unicode, so apparently UTF-16 encoded), for example:
base64<widechar<"X509:
I was able to successfully join using a alternativeSecurityId ripped from an intune-portal for Linux join.
So the join process is as follows: Submit a post to v1.0/devices (with sufficient creds via the bearer) with json content, with the following payload (essentially):
{ "accountEnabled": true, "alternativeSecurityIds": [ { "key": "X509:[thumbprint]+[publickeyhash]", "type": 2 } ], "deviceId": "RANDOM_UUID_GEN", "displayName": "localhost.localdomain", "operatingSystem": "Linux", "operatingSystemVersion": "openSUSE Tumbleweed 20230816", "isCompliant": true, "isManaged": true, "mdmAppId": "0000000a-0000-0000-c000-000000000000" }
Where the alternativeSecurityIds is formatted as previously described, and the deviceId is a randomly generated UUID. The mdmAppId says we are joining as the Microsoft Intune portal (faking it). Perhaps at some point we can create our own mdmAppId. It's important that the operatingSystem is set to Linux (at least for a Linux join, anyway).
Next register an owner of the device: Submit a post to v1.0/devices/{id}/registeredOwners/$ref where {id} is the id of the device (do NOT use the deviceId) with json content, with the following payload:
{ "@odata.id": "https://graph.microsoft.com/v1.0/directoryObjects/{UID}" }
Where {UID} is the id of the user object which will own the device (perhaps this can be a group?).
This creates a seemingly viable joined Device object, with an attached alternativeSecurityId that we can auth with (this will enable nss lookups, etc).
This protocol appears to define the whole process: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dvrj/fbac29b8-92dd-4757-8fd9-c004b9efb6e3
More importantly, here is the specific request and response protocol details which we need.
Closely inspecting several valid Alt-Security-Identities revealed that the publickeyhash is not SHA1, but clearly SHA256. This lead me to this documentation: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dvre/1f2ebba7-7783-42c4-982a-ce9ca76af949
I think this is how the device is enrolled in Intune after device creation and CSR. https://learn.microsoft.com/en-us/graph/api/intune-devices-manageddevice-create?view=graph-rest-1.0&tabs=http
MS's intune-portal sends a json request (somewhere) such as this to get a certificate after device creation:
{
"CertificateSigningRequest": "SOME_CSR",
"AppVersion": "Application version",
"DeviceName": "A named device",
}
The request includes the parameters api-version
and client-version
in the url (I'm not sure of their value).
I think maybe this is requesting a replacement certificate every time a user auths to the device? I suspect the purpose is to continually renew the device auth and keep kicking the can down the road.
Device creation via the intune-portal sends the following json POST:
{
"DeviceId": "XXXXXXXX",
"DeviceName": "XXXXXXXX",
"Manufacturer": "XXXXXXXX",
"OSDistribution": "XXXXXXXX",
"OSVersion": "XXXXXXXX",
}
Also with the api-version
and client-version
url params.
You can fetch the enrollment endpoints with a get request to: https://graph.windows.net/myorganization/applicationRefs/0000000a-0000-0000-c000-000000000000/appData?$select=appData&api-version=1.6-internal This appears to be some kind of internal graph. The access_token is something different than expected. I tried sending this request using a token from authenticating via msal, but it fails. If I sign into the intune portal (via the web), then navigate to https://portal.azure.com/#view/Microsoft_AAD_IAM/MdmConfiguration.ReactView/appName/Microsoft%20Intune/appId/0000000a-0000-0000-c000-000000000000 I can scrape a token from the web dev tools. This token successfully authenticates to the internal graph. Apparently I need to authenticate differently here.
The following get query response contains the enrollment url found in the debug previously (https://fef.msua08.manage.microsoft.com/LinuxMDM/LinuxEnrollmentService/
):
GET https://graph.windows.net/myorganization/applicationRefs/0000000a-0000-0000-c000-000000000000?api-version=1.6-internal&$select=identifierUris
{
"odata.metadata": "https://graph.windows.net/myorganization/$metadata#applicationRefs/@Element",
"identifierUris": [
"https://fef.amsuin01.manage.microsoft.com",
"https://fef.msuc06.manage.microsoft.com",
"https://fef.msua09.manage.microsoft.com",
"https://manage-mig.microsoft.com",
"https://fef.migsu01.manage-mig.microsoft.com",
"https://fef.migsu02.manage-mig.microsoft.com",
"https://fef.msub07.manage.microsoft.com",
"https://fef.msub06.manage.microsoft.com",
"https://fef.msua08.manage.microsoft.com",
"https://fef.amsub0502.manage.microsoft.com",
"https://fef.amsua0702.manage.microsoft.com",
"https://fef.amsua0102.manage.microsoft.com",
"https://fef.msud01.manage.microsoft.com",
"https://fef.shmsua02.manage-selfhost.microsoft.com",
"https://warehouse.manage.microsoft.com",
"https://warehouse.manage-beta.microsoft.com",
"https://warehouse.manage-selfhost.microsoft.com",
"https://fef.amsub0302.manage.microsoft.com",
"https://fef.amsub0202.manage.microsoft.com",
"https://fef.amsua0202.manage.microsoft.com",
"https://fef.amsua0402.manage.microsoft.com",
"https://manage-beta.microsoft.com",
"https://manage.microsoft.com",
"https://manage-beta.microsoft.com/",
"https://manage.microsoft.com/",
"https://manage-selfhost.microsoft.com",
"https://fef.shmsua01.manage-selfhost.microsoft.com",
"https://fef.bmsua01.manage-beta.microsoft.com",
"https://fef.bmsub01.manage-beta.microsoft.com",
"https://fef.msua07.manage.microsoft.com",
"https://fef.msub05.manage.microsoft.com",
"https://fef.msub03.manage.microsoft.com",
"https://fef.msub02.manage.microsoft.com",
"https://FEF.amsub0102.manage.microsoft.com",
"https://fef.msub01.manage.microsoft.com",
"https://fef.msuc05.manage.microsoft.com",
"https://fef.msuc03.manage.microsoft.com",
"https://fef.msuc02.manage.microsoft.com",
"https://fef.msuc01.manage.microsoft.com",
"https://fef.amsua0602.manage.microsoft.com",
"https://fef.msua06.manage.microsoft.com",
"https://fef.amsua0502.manage.microsoft.com",
"https://fef.msua05.manage.microsoft.com",
"https://fef.msua04.manage.microsoft.com",
"https://fef.msua02.manage.microsoft.com",
"https://fef.msua01.manage.microsoft.com"
]
}
The string LinuxMDM
at least is hard coded in the intune-portal binary, so perhaps the rest of the url is generated.
FYI, the queries against graph.windows.net
fail against the public graph server graph.microsoft.com
. Interestingly, with an error saying 'Invalid version', suggesting the api version 1.6-internal
is important.
Interesting, this internal graph allows me to do a full user listing with much more information than the public graph returns: https://graph.windows.net/myorganization/users?api-version=1.6&$select=userPrincipalName
Ah, graph.windows.net
is the deprecated Azure AD graph... which MS is still using to perform the enrollment, hrm.
Ok, this is the correct request using the new MS Graph:
https://graph.microsoft.com/v1.0/servicePrincipals?$filter=appId eq '0000000a-0000-0000-c000-000000000000'
Which returns an interesting tidbit of information:
"oauth2PermissionScopes": [
{
"adminConsentDescription": "Allows Intune Admins to enroll a Microsoft Tunnel Gateway Agent",
"adminConsentDisplayName": "MicrosoftTunnelGatewayEnrollment",
"id": "e323f13a-1fcc-49cc-883f-c6da13ae0542",
"isEnabled": true,
"type": "User",
"userConsentDescription": "Allows Intune Admins to enroll a Microsoft Tunnel Gateway Agent",
"userConsentDisplayName": "MicrosoftTunnelGatewayEnrollment",
"value": "MicrosoftTunnelGatewayEnrollment"
}
],
The following GET request returns a list of service URLs for enrollment:
GET https://enterpriseregistration.windows.net/$tenant_id/Discover?api-version=1.9&managed=True
Where $tenant_id
is the tenant id of your domain. The request must be made with an Authorization header with the access_token obtained from msal. It also requires the headers ocp-adrs-client-name
and ocp-adrs-client-version
, which are respectively the machine name and the version of your client software (these are arbitrary).
The following spec appears to describe the service URLs for enrollment: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dvrd/296ebf70-bd4b-489e-a531-460d8ef7519b
This is gold: https://learn.microsoft.com/en-us/windows/client-management/azure-active-directory-integration-with-mdm Explains how to create our own MDM, so we can get access to the join secrets.
And here is a useful walk through.
I noticed the services listed from the discovery service response refer to the services as 'ms-drs', which is the device registration service: https://learn.microsoft.com/en-us/windows-server/identity/ad-fs/technical-reference/device-registration-technical-reference This can apparently be installed on a Windows Server.
The Intune portal prints the following debug which may be useful:
2023-09-11 13:55:28 INFO get_client{capability="LinuxEnrollmentService" resource=ResourceId("XXXXX") endpoint="https://fef.msua08.manage.microsoft.com/LinuxMDM/LinuxEnrollmentService/" endpoint="https://fef.msua08.manage.microsoft.com/LinuxMDM/LinuxEnrollmentService/"}: Requesting a token silently resource=ResourceId("XXXXX")
I finally found where this comes from. An authenticated get request to https://graph.microsoft.com/beta/servicePrincipals/appId=0000000a-0000-0000-c000-000000000000/endpoints returns a long list of enrollment endpoints, including this one for Linux. You can see it in the network traffic if you navigate to the Intune admin center, then open the Data Warehouse pane by selecting Reports > Data warehouse.
These are the interesting ones:
{
"id": "352ac532-313b-4e22-b513-1f81e153ab68",
"deletedDateTime": null,
"capability": "LinuxDeviceCheckinService",
"providerId": "0000000a-0000-0000-c000-000000000000",
"providerName": "LinuxDeviceCheckinService",
"uri": "https://fef.amsua0602.manage.microsoft.com/TrafficGateway/TrafficRoutingService/LinuxMdm/LinuxDeviceCheckinService",
"providerResourceId": "88cf0172-3b18-4cd4-abe5-552181a59741"
},
{
"id": "7bf281a6-4d6a-42b1-b01e-12bd6fc02f06",
"deletedDateTime": null,
"capability": "LinuxEnrollmentService",
"providerId": "0000000a-0000-0000-c000-000000000000",
"providerName": "LinuxEnrollmentService",
"uri": "https://fef.amsua0602.manage.microsoft.com/LinuxMDM/LinuxEnrollmentService",
"providerResourceId": "6a559596-421e-4d9f-b81d-3a27680c5e66"
}
The following scopes are required to read that list: "CloudPC.Read.All CloudPC.ReadWrite.All DeviceManagementApps.ReadWrite.All DeviceManagementConfiguration.ReadWrite.All DeviceManagementManagedDevices.PrivilegedOperations.All DeviceManagementManagedDevices.ReadWrite.All DeviceManagementRBAC.ReadWrite.All Directory.AccessAsUser.All email openid profile Sites.Read.All"
Seems like we get preauthorization from a broker app?
https://graph.windows.net/myorganization/applicationRefs/29d9ed98-a469-4536-ade2-f981bc1d605e?api-version=1.6-internal
{
"odata.metadata": "https://graph.windows.net/myorganization/$metadata#applicationRefs/@Element",
"appCategory": null,
"appContextId": "f8cdef31-a31e-4b4a-93e4-5f571e91255a",
"appData": null,
"appId": "29d9ed98-a469-4536-ade2-f981bc1d605e",
"appRoles": [],
"availableToOtherTenants": true,
"certification": null,
"displayName": "Microsoft Authentication Broker",
"errorUrl": null,
"homepage": null,
"identifierUris": [],
"knownClientApplications": [],
"logoutUrl": null,
"logoUrl": null,
"mainLogo@odata.mediaEditLink": "applicationRefs/29d9ed98-a469-4536-ade2-f981bc1d605e/Microsoft.DirectoryServices.ApplicationRef/mainLogo",
"oauth2Permissions": [],
"publisherDomain": "sharepoint.com",
"publisherName": "Microsoft Services",
"publicClient": true,
"replyUrls": [
"https://login.microsoftonline.com/WebApp/CloudDomainJoin/8",
"https://login.microsoftonline.com/WebApp/CloudDomainJoin/3",
"https://login.microsoftonline.com/WebApp/CloudDomainJoin/6",
"https://login.microsoftonline.com/WebApp/CloudDomainJoin/4",
"urn:ietf:wg:oauth:2.0:oob",
"ms-appx-web://Microsoft.AAD.BrokerPlugin/DRSFF",
"ms-aadj-redir://auth/mdm",
"ms-aadj-redir://auth/drs",
"https://clouddomainjoin.azurewebsites.net/webapp/clouddomainjoin/2",
"msauth://Microsoft.AAD.BrokerPlugin",
"ms-appx-web://Microsoft.AAD.BrokerPlugin/DRS",
"ms-appx-web://Microsoft.AAD.BrokerPlugin/MDM",
"ms-appx-web://Microsoft.AAD.BrokerPlugin",
"https://login.microsoftonline.com/applebroker/msauth",
"https://login.microsoftonline.com/applebroker/msauthv2",
"https://login.microsoftonline.com/androidbroker/msauth"
],
"requiredResourceAccess": [],
"samlMetadataUrl": null,
"supportsConvergence": true,
"verifiedPublisher": {
"displayName": null,
"verifiedPublisherId": null,
"addedDateTime": null
}
}
Someone else has already figured this out: https://aadinternals.com/post/prt/#creating-your-own-prt
https://learn.microsoft.com/en-us/python/api/msal/msal.application.confidentialclientapplication?view=msal-py-latest#parameters
The constructor for ConfidentialClientApplication allows us to pass a
client_credential
, which I believe is the private key associated with the public key sent when enrolling the device. This allows us to obtain an auth_token associated with the enrolled device. This could enable nss lookups for users and groups prior to authentication, for example. It could also resolve some the of policy application issues faced, as well as allowing Device policy application.