MDM_utils.ps1
# This file contains utility functions for Intune MDM # Get MDM discovery information # Aug 20th function Get-MDMEnrollmentService { [cmdletbinding()] Param( [Parameter(Mandatory=$False)] [String]$UserName="user@alt.none" ) Process { $messageId = $(New-Guid).ToString() $deviceType = "CIMClient_Windows" $applicationVersion = "10.0.18363.0" $OSEdition = "4" $body=@" <s:Envelope xmlns:a="http://www.w3.org/2005/08/addressing" xmlns:s="http://www.w3.org/2003/05/soap-envelope"> <s:Header> <a:Action s:mustUnderstand="1">http://schemas.microsoft.com/windows/management/2012/01/enrollment/IDiscoveryService/Discover</a:Action> <a:MessageID>urn:uuid:$messageId</a:MessageID> <a:ReplyTo> <a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address> </a:ReplyTo> <a:To s:mustUnderstand="1">https://enrollment.manage.microsoft.com:443/enrollmentserver/discovery.svc</a:To> </s:Header> <s:Body> <Discover xmlns="http://schemas.microsoft.com/windows/management/2012/01/enrollment"> <request xmlns:i="http://www.w3.org/2001/XMLSchema-instance"> <EmailAddress>$UserName</EmailAddress> <RequestVersion>4.0</RequestVersion> <DeviceType>$deviceType</DeviceType> <ApplicationVersion>$applicationVersion</ApplicationVersion> <OSEdition>$OSEdition</OSEdition> <AuthPolicies> <AuthPolicy>OnPremise</AuthPolicy> <AuthPolicy>Federated</AuthPolicy> </AuthPolicies> </request> </Discover> </s:Body> </s:Envelope> "@ $headers=@{ "Content-Type" = "application/soap+xml; charset=utf-8" "User-Agent" = "ENROLLClient" } $response = Invoke-RestMethod -UseBasicParsing -Method Post -Uri "https://enrollment.manage.microsoft.com/enrollmentserver/discovery.svc" -Body $body -ContentType "application/soap+xml; charset=utf-8" -Headers $headers # Get the data $activityId = $response.Envelope.Header.ActivityId.'#text' $serviceUri = $response.Envelope.Body.DiscoverResponse.DiscoverResult.EnrollmentServiceUrl if(!$serviceUri.EndsWith($activityId)) { $serviceUri += "?client-request-id=$activityId" } # Return return $serviceUri } } # Enroll device to MDM # Aug 28th function Enroll-DeviceToMDM { [cmdletbinding()] Param( [Parameter(Mandatory=$True)] [String]$AccessToken, [Parameter(Mandatory=$True)] [String]$DeviceName, [Parameter(Mandatory=$True)] [bool]$BPRT, [Parameter(Mandatory=$False)] [object]$ZtdCorrelationId ) Process { # Get the claims from the access token $claims = Read-Accesstoken -AccessToken $AccessToken # Construct the values $enrollmentUrl = Get-MDMEnrollmentService -UserName $claims.upn $binarySecurityToken = Convert-ByteArrayToB64 -Bytes ([text.encoding]::UTF8.GetBytes($AccessToken)) $HWDevID = "$($claims.deviceid)$($claims.tid)".Replace("-","") $deviceId = $claims.deviceid.Replace("-","") # Create a private key $rsa = [System.Security.Cryptography.RSA]::Create(2048) # Initialize the Certificate Signing Request object $CN = "CN=$($claims.deviceid)" $req = [System.Security.Cryptography.X509Certificates.CertificateRequest]::new($CN, $rsa, [System.Security.Cryptography.HashAlgorithmName]::SHA256,[System.Security.Cryptography.RSASignaturePadding]::Pkcs1) # Create the signing request $csr = Convert-ByteArrayToB64 -Bytes $req.CreateSigningRequest() $headers=@{ "Content-Type" = "application/soap+xml; charset=utf-8" "User-Agent" = "ENROLLClient" } # Create the CSR request body $csrBody=@" <s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:a="http://www.w3.org/2005/08/addressing" xmlns:u="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" xmlns:wst="http://docs.oasis-open.org/ws-sx/ws-trust/200512" xmlns:ac="http://schemas.xmlsoap.org/ws/2006/12/authorization"> <s:Header> <a:Action s:mustUnderstand="1">http://schemas.microsoft.com/windows/pki/2009/01/enrollment/RST/wstep</a:Action> <a:MessageID>urn:uuid:$((New-Guid).ToString())</a:MessageID> <a:ReplyTo> <a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address> </a:ReplyTo> <a:To s:mustUnderstand="1">$enrollmentUrl</a:To> <wsse:Security s:mustUnderstand="1"> <wsse:BinarySecurityToken ValueType="urn:ietf:params:oauth:token-type:jwt" EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd#base64binary">$binarySecurityToken</wsse:BinarySecurityToken> </wsse:Security> </s:Header> <s:Body> <wst:RequestSecurityToken> <wst:TokenType>http://schemas.microsoft.com/5.0.0.0/ConfigurationManager/Enrollment/DeviceEnrollmentToken</wst:TokenType> <wst:RequestType>http://docs.oasis-open.org/ws-sx/ws-trust/200512/Issue</wst:RequestType> <wsse:BinarySecurityToken ValueType="http://schemas.microsoft.com/windows/pki/2009/01/enrollment#PKCS10" EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd#base64binary">$csr</wsse:BinarySecurityToken> <ac:AdditionalContext xmlns="http://schemas.xmlsoap.org/ws/2006/12/authorization"> <ac:ContextItem Name="UXInitiated"> <ac:Value>$(($BPRT -eq $false).ToString().ToLower())</ac:Value> </ac:ContextItem> <ac:ContextItem Name="HWDevID"> <ac:Value>$HWDevID</ac:Value> </ac:ContextItem> <ac:ContextItem Name="OfflineAutoPilotEnrollmentCorrelator"> <ac:Value>$ZtdCorrelationId</ac:Value> </ac:ContextItem> <ac:ContextItem Name="BulkAADJ"> <ac:Value>$($BPRT.ToString().ToLower())</ac:Value> </ac:ContextItem> <ac:ContextItem Name="Locale"> <ac:Value>en-US</ac:Value> </ac:ContextItem> <ac:ContextItem Name="TargetedUserLoggedIn"> <ac:Value>$((($BPRT -eq $false) -or ($ZtdCorrelationId -eq $null)).ToString().ToLower())</ac:Value> </ac:ContextItem> <ac:ContextItem Name="EnrollmentData"> <ac:Value>$ZtdCorrelationId</ac:Value> </ac:ContextItem> <ac:ContextItem Name="OSEdition"> <ac:Value>4</ac:Value> </ac:ContextItem> <ac:ContextItem Name="DeviceName"> <ac:Value>$DeviceName</ac:Value> </ac:ContextItem> <ac:ContextItem Name="MAC"> <ac:Value>00-00-00-00-00-00</ac:Value> </ac:ContextItem> <ac:ContextItem Name="DeviceID"> <ac:Value>$deviceId</ac:Value> </ac:ContextItem> <ac:ContextItem Name="EnrollmentType"> <ac:Value>Device</ac:Value> </ac:ContextItem> <ac:ContextItem Name="DeviceType"> <ac:Value>CIMClient_Windows</ac:Value> </ac:ContextItem> <ac:ContextItem Name="OSVersion"> <ac:Value>10.0.18363.0</ac:Value> </ac:ContextItem> <ac:ContextItem Name="ApplicationVersion"> <ac:Value>10.0.18363.0</ac:Value> </ac:ContextItem> <ac:ContextItem Name="NotInOobe"> <ac:Value>true</ac:Value> </ac:ContextItem> <ac:ContextItem Name="RequestVersion"> <ac:Value>5.0</ac:Value> </ac:ContextItem> </ac:AdditionalContext> </wst:RequestSecurityToken> </s:Body> </s:Envelope> "@ # Clean the url $url=$enrollmentUrl.Replace(":443","") # The user might not have the lisence try { $response = Invoke-RestMethod -UseBasicParsing -Method Post -Uri $url -Body $csrBody -ContentType "application/soap+xml; charset=utf-8" -Headers $headers } catch { throw $_ } # Get the data $binSecurityToken = $response.Envelope.Body.RequestSecurityTokenResponseCollection.RequestSecurityTokenResponse.RequestedSecurityToken.BinarySecurityToken.'#text' $xmlSecurityToken = [xml][text.encoding]::UTF8.GetString((Convert-B64ToByteArray -B64 $binSecurityToken)) Write-Debug "BinarySecurityToken: $($xmlSecurityToken.OuterXml)" # Get the certificates $CA = $xmlSecurityToken.'wap-provisioningdoc'.characteristic[0].characteristic[0].characteristic.characteristic.Parm.value $IntMedCA = $xmlSecurityToken.'wap-provisioningdoc'.characteristic[0].characteristic[1].characteristic.characteristic.Parm.value $binCert = [byte[]](Convert-B64ToByteArray -B64 ($xmlSecurityToken.'wap-provisioningdoc'.characteristic[0].characteristic[2].characteristic.characteristic.Parm.value)) # Create a new x509certificate $cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($binCert,"",[System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::PersistKeySet -bor [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable) # Store the private key to so that it can be exported $cspParameters = [System.Security.Cryptography.CspParameters]::new() $cspParameters.ProviderName = "Microsoft Enhanced RSA and AES Cryptographic Provider" $cspParameters.ProviderType = 24 $cspParameters.KeyContainerName ="AADInternals" # Set the private key $privateKey = [System.Security.Cryptography.RSACryptoServiceProvider]::new(2048,$cspParameters) $privateKey.ImportParameters($rsa.ExportParameters($true)) $cert.PrivateKey = $privateKey # Generate the return value $joinInfo = @( $CA, $IntMedCA, $cert ) return $joinInfo } } # Sep 3rd 2020 # Automatically responses to the given command array function New-SyncMLAutoresponse { [cmdletbinding()] Param( [Parameter(Mandatory=$True)] [string]$DeviceName, [Parameter(Mandatory=$True)] [Object[]]$Commands, [Parameter(Mandatory=$True)] [int]$MsgID, [Parameter(Mandatory=$True)] [Hashtable]$Settings ) Begin { $response200 = @( "Add" "Replace" "Atomic" "Delete" "Sequence" ) } Process { $resCommands = @() $CmdID = 1 foreach($command in $commands) { if($command.type -ne "Status") { # Just answer 400 to (almost) all requests $errorCode = 400 # For NodeCache requests if($command.Type -eq "Get" -and $command.LocURI.StartsWith("./Vendor/MSFT/NodeCache/")) { $errorCode = 404 } # Status must be 200 for predefined answers if($command.type -eq "Get" -and $Settings[$command.LocURI] -ne $null) { $errorCode = 200 } # Okay, let's be nice for some commands :) if($response200 -contains $command.Type) { $errorCode = 200 } # Create the status message $attr = [ordered]@{ Type="Status" CmdID = $CmdID++ MsgRef = $MsgID-1 # Status is always referring to the previous message CmdRef = $command.CmdID Cmd = $command.Type Data = $errorCode } $resCommands += New-Object psobject -Property $attr # Create the results message if($command.type -eq "Get" -and $Settings[$command.LocURI] -ne $null) { $attr = [ordered]@{ Type="Results" CmdID = $CmdID++ MsgRef = $MsgID-1 # Status is always referring to the previous message CmdRef = $command.CmdID Cmd = $command.Type LocURI = $command.LocURI Data = $Settings[$command.LocURI] } $resCommands += New-Object psobject -Property $attr } if($command.type -eq "Get" -and $errorCode -ne 200) { #if($VerbosePreference) #{ Write-Warning " < No data ($MsgID): $command" #} } } else { $resCommands += $command $CmdID++ } } return $resCommands } } # Sep 2nd 2020 # Create a new SyncML request function New-SyncMLRequest { [cmdletbinding()] Param( [Parameter(Mandatory=$True)] [string]$DeviceName, [Parameter(Mandatory=$True)] [int]$SessionID, [Parameter(Mandatory=$True)] [int]$MsgID, [Parameter(Mandatory=$False)] [string]$VerDTD="1.2", [Parameter(Mandatory=$False)] [string]$VerProto="DM/1.2", [Parameter(Mandatory=$False)] [Object[]]$commands ) Process { $CmdId = 1 $syncBody="" foreach($command in $commands) { Write-Verbose " > $command" switch($command.Type) { "Alert" { if($command.ItemData) { if($command.MetaType) { $meta = @" <Meta> <Type xmlns="syncml:metinf">$($command.MetaType)</Type> </Meta> "@ } $syncBody += @" <Alert> <CmdID>$($command.CmdId)</CmdID> <Data>$($command.Data)</Data> <Item> $meta <Data>$($command.ItemData)</Data> </Item> </Alert> "@ } else { $syncBody += @" <Alert> <CmdID>$($command.CmdId)</CmdID> <Data>$($command.Data)</Data> </Alert> "@ } break } "Replace" { $syncBody += @" <Replace> <CmdID>$($command.CmdId)</CmdID> "@ foreach($key in $command.Items.Keys) { $syncBody += @" <Item> <Source> <LocURI>$key</LocURI> </Source> <Data>$($command.Items[$key])</Data> </Item> "@ } $syncBody += "`n </Replace>" break } "Delete" { break } "Atomic" { $syncBody += "`n" $syncBody += @" <Status> <CmdID>$($command.CmdId)</CmdID> <MsgRef>$($command.MsgRef)</MsgRef> <CmdRef>$($command.CmdRef)</CmdRef> <Cmd>$($command.Cmd)</Cmd> <Data>200</Data> </Status> "@ break } "Sequence" { $syncBody += "`n" $syncBody += @" <Status> <CmdID>$($command.CmdId)</CmdID> <MsgRef>$($command.MsgRef)</MsgRef> <CmdRef>$($command.CmdRef)</CmdRef> <Cmd>$($command.Cmd)</Cmd> <Data>200</Data> </Status> "@ break } "Final" { break } "Status" { $syncBody += "`n" $syncBody += @" <Status> <CmdID>$($command.CmdId)</CmdID> <MsgRef>$($command.MsgRef)</MsgRef> <CmdRef>$($command.CmdRef)</CmdRef> <Cmd>$($command.Cmd)</Cmd> <Data>$($command.Data)</Data> </Status> "@ break } "Results" { $syncBody += "`n" $syncBody += @" <Results> <CmdID>$($command.CmdId)</CmdID> <MsgRef>$($command.MsgRef)</MsgRef> <CmdRef>$($command.CmdRef)</CmdRef> <Item> <Source> <LocURI>$($command.LocURI)</LocURI> </Source> <Data>$($command.Data)</Data> </Item> </Results> "@ break } } } # Construct the body $syncML = @" <?xml version = "1.0" encoding = "UTF-8" ?> <SyncML> <SyncHdr> <VerDTD>$VerDTD</VerDTD> <VerProto>$VerProto</VerProto> <SessionID>$SessionID</SessionID> <MsgID>$MsgID</MsgID> <Target> <LocURI>https://r.manage.microsoft.com/devicegatewayproxy/cimhandler.ashx</LocURI> </Target> <Source> <LocURI>$DeviceName</LocURI> </Source> </SyncHdr> <SyncBody> $syncBody <Final/> </SyncBody> </SyncML> "@ return $syncML } } # Sep 2nd 2020 # Parses the SyncML response and returns an array containing all the returned commands function Parse-SyncMLResponse { [cmdletbinding()] Param( [Parameter(Mandatory=$True)] [System.Xml.XmlDocument]$SyncML ) Process { $commands = @() $CmdId = 1 function parseNode { Param( [Parameter(Mandatory=$True)] $node, [Parameter(Mandatory=$True)] [ref]$commands ) Process { switch($node.Name) { "Status" { $attr = [ordered]@{ Type="Status" CmdID = $node.CmdID MsgRef = $node.MsgRef CmdRef = 0 Cmd = $node.Cmd Data = $node.Data } $commands.value += New-Object psobject -Property $attr break } "Get" { $attr = [ordered]@{ Type="Get" CmdID = $node.CmdID LocURI = $node.Item.Target.LocURI } $commands.value += New-Object psobject -Property $attr break } "Add" { $attr = [ordered]@{ Type="Add" CmdID = $node.CmdID LocURI = $node.Item.Target.LocURI } $commands.value += New-Object psobject -Property $attr break } "Replace" { $attr = [ordered]@{ Type="Replace" CmdID = $node.CmdID LocURI = $node.Item.Target.LocURI MFormat = $node.Item.Meta.Format.'#text' MType = $node.Item.Meta.Type.'#text' Data = $node.Item.Data } $commands.value += New-Object psobject -Property $attr break } "Delete" { $attr = [ordered]@{ Type="Delete" CmdID = $node.CmdID LocURI = $node.Item.Target.LocURI } $commands.value += New-Object psobject -Property $attr break } "Atomic" { # Parse nodes inside this one foreach($inode in $node.ChildNodes) { parseNode -node $inode -commands $commands } $attr = [ordered]@{ Type="Atomic" CmdID = $node.CmdID } $commands.value += New-Object psobject -Property $attr break } "Sequence" { # Parse nodes inside this one foreach($inode in $node.ChildNodes) { parseNode -node $inode -commands $commands } $attr = [ordered]@{ Type="Sequence" CmdID = $node.CmdID LocURI = "" } $commands.value += New-Object psobject -Property $attr break } "Final" { #$commands.value += New-Object psobject -Property @{Type="Final"} break } } } } foreach($node in $SyncML.SyncML.SyncBody.ChildNodes) { parseNode -node $node -commands ([ref]$commands) } if($VerbosePreference) { foreach($command in $commands) { Write-Verbose " < $command" } } return $commands } } # Sep 2nd 2020 # Sends the given SyncML to Intune and returns the response as an xml document function Invoke-SyncMLRequest { [cmdletbinding()] Param( [Parameter(Mandatory=$True)] [string]$SyncML, [Parameter(ParameterSetName='Certificate',Mandatory=$True)] [System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate ) Process { $headers=@{ "Content-Type" = "application/vnd.syncml.dm+xml; charset=utf-8" "Accept" = "application/vnd.syncml.dm+xml, application/octet-stream" "Accept-Charset" = "UTF-8" "User-Agent" = "MSFT OMA DM Client/1.2.0.1" } Write-Debug "Request: $SyncML" try { $response = Invoke-WebRequest -UseBasicParsing -Certificate $Certificate -Method Post -Uri "https://r.manage.microsoft.com/devicegatewayproxy/cimhandler.ashx?mode=Maintenance&Platform=WoA" -Headers $headers -Body $SyncML -ErrorAction SilentlyContinue -ContentType "application/vnd.syncml.dm+xml; charset=utf-8" $xml = [xml]$response.content } catch { throw "SyncML request failed" } Write-Debug "Response: $($xml.OuterXml)" return $xml } } # Gets the object id of the device using device id # Sep 11th 2020 function Get-DeviceObjectId { [cmdletbinding()] Param( [Parameter(Mandatory=$True)] [String]$DeviceId, [Parameter(Mandatory=$True)] [String]$TenantId, [Parameter(Mandatory=$True)] [String]$AccessToken ) Process { $headers=@{ "Authorization" = "Bearer $AccessToken" "Accept" = "application/json;odata=nometadata" } Write-Verbose "Getting objectId for device $DeviceId" $devices = Invoke-RestMethod -UseBasicParsing -Method Get -Uri "https://graph.windows.net/$tenantId/devices?`$filter=deviceId eq guid'$DeviceId'&`$select=objectId,displayName,deviceId&api-version=1.61-internal" -Headers $headers foreach($device in $devices.value) { if($device.deviceId -eq $DeviceId) { $ObjectId = $device.objectId break } } if([string]::IsNullOrEmpty($ObjectId)) { throw "Device $DeviceId not found!" } return $ObjectId } } |