PowerShell Solutions
So, have you ever tried to join a computer to a domain using PowerShell? It's a simple line or two of code:
$domainName = "yourdomain.local"
$username = "administrator"
$password = "password"
$computerName = "yourcomputername"
$credential = New-Object System.Management.Automation.PSCredential ($username, (ConvertTo-SecureString $password -AsPlainText -Force))
Add-Computer -DomainName $domainName -Credential $credential -ComputerName $computerName -Restart
Fill in the variables with the correct information and voila, the computer has been registered and joined to your local Active Directory. Hop in the AD tenant and get the computer moved into the right group and you are good to go!
Currently, I'm employed with a Market Service Provider, which means I've got a lot of different Local AD tenants and several Azure AD tenants to contend with.
For local AD device joining, I use a similar script during the configuration of a computer. It's an automated process, a "lite-touch" deployment, if you will. Using Windows Deployment Server and Windows Deployment Toolkit, I network boot the machine, select the image for the customer, name it, and let it fly! 10 minutes later and you have a near-fully imaged computer ready to head out the door. Some last minute cleanup includes running any leftover Windows updates and ensuring our RMM tool is installed.
Unfortunately, for the companies using Azure AD, the process has a hiccup: PowerShell doesn't support joining a device to Azure AD.
As you can probably tell by the fact that I've written this post, this bugged me. Every so often I'll have to configure a few hundred devices in a weeks time. Between un-packaging all the computers, getting plenty of paper-cuts, and drying my hands out on all of the cardboard, I'd prefer not to manually join each of the devices to Azure AD. I'd like to simplify device configuration as much as possible so I can get back to the rest of my job.
A few Google searches later and I discovered that the internet had declared my worst fear: making it work with PowerShell is impossible.
I was crushed.
But there had to be a workaround.
Windows Configuration Designer
It's clunky, but it works.
Windows Configuration Designer (WCD) is a Microsoft Store application designed to supposedly simplify workstation deployment. It can be used to name a device, join it WiFi, installWwindows Store applications, and most importantly, join a device to Azure AD or Local AD.
Of course the only component of this application I was interested in was the Azure AD joining function.
This article by Anders Ahl was a great starting point for my Azure AD journey. WCD is a pretty simple program. Following Anders' article I was able to plug in the necessary information and get a .ppkg file. The .ppkg is just a package file with an Azure token that can be installed upon applying the package to the device. There's even a handy little line of code within the article for installing the package:
DISM.exe /Image=C:\ /Add-ProvisioningPackage /PackagePath:C:\BulkJoin.ppkg
After applying the package, the device should be joined to the Azure domain! Simple! Just slap that package and script in the customer image and we're off to the races!
Except...
Six months later, the script broke for each and every customer. I knew it was coming, but its easy to forget about over such a long time period. The token that WCD pulls expires after a maximum of 6 months. Afterwards, the package needs to be rebuilt.
Not a big deal, just rebuild the packages! But now I have dozens of these scripts, packages and images. To add to the complexity, trying to create more than one package in a day can sometimes result in WCD locking itself to one Azure account. This will force me to wait a day or two for the login to expire, and then I'm free to start working on the next package.
This was not ideal. So, back to the drawing board.
Autopilot
So why is this a problem in the first place? Why not just allow a PowerShell to do the heavy lifting and script its way into an Azure account?
I've got a theory, and it's pretty universal in the technology sector: monetization.
Autopilot, in Microsoft's infallible opinion, is the future: grab the hardware hash and hand it over to Microsoft, set up the user's account with an Intune license, and send it off to the user. All the user needs to do is login and wait for the apps and configuration you specified to download. It's that easy!
..Sort of.
There are immediately some issues for my use case. First, many of my customers do not want to pay for the proper licensing to use Intune. Many of our customers are healthcare facilities operating with near destitute equipment budgets. As much as I would love for them to utilize the latest and greatest equipment, the truth is that many of them still have 10 year old Cisco switches in their network racks. In fact, I only have one customer that is currently using Autopilot, a logistics company that insists on being on the cutting edge of technology.
Second, a user will not always have the necessary bandwidth for the initial deployment of a computer using Autopilot. Many of our customers work in rural areas that don't have the blessing of high speed fiber. An Autopilot deployment in one of these areas could last as long as two days. That is simply unacceptable.
Third, sometimes Autopilot just doesn't work. The logistics company we support does have the blessing of high-speed fiber internet. Autopilot relies on a callback to Intune to actually configure the device. Sometimes Intune just... decides to take it's time. We've had computers sit in stasis for up to three hours, and some would finish their deployment missing a full half of the applications we'd specified to be installed.
Fourth, allowing a user to have permissions to join Azure AD is a recipe for disaster. Microsoft wants to push user enrollment to be the solution for joining a device to the company tenant. This way, there's an Intune license, which means more money for Microsoft. Let's look at why user joining is a problem.
The Vulnerability
On his website, Dr. Nestori Syynimaa explores an attack called "AAD Kill Chain" wherein a threat actor can exploit the same tokens being pulled by WCD to gain access to a company's Azure AD tenant. This is, of course, a disastrous scenario. Details about his discovery of this vulnerability can be read about here. His conclusion: Don't allow users to have permissions to join a device to Azure AD. Ironically, this vulnerability also presents me with a convenient opportunity.
Exploiting BPRT Tokens
After some fiddling, lots of documentation skimming, and some correspondence with Dr. Nestori, I found a solution.
First, we need to install some packages. We start with bypassing the execution policy. Next, we install the latest NuGet package, the AzureAD package, and AADInternals, a trusted package developed by Dr. Nestori.
Next we begin grabbing the information we need. After making some variables for our credentials, we'll reach out to Microsoft's registration service. After touching base and getting an access token to AzureAD, we direct Microsoft to create a user named "BPRT AzureJoinScriptUser." This user exist only to hold the fresh BPRT token we need to join the device to Azure AD. Because the account is brand new, there is no need to worry about token expiration in the future.
Now we'll use the token to emulate the join process by "joining" a device to Azure AD. We're really just contacting Azure AD to create a corresponding device, transport key (=Certificate) and a Device Object. We'll need all of this pre-made to actually configure the device itself.
The next step is calling the certificate and using it join the device.
Finally, we clean up by deleting the temporary user we needed to host the fresh BPRT token.
Here's the script when it's all put together:
echo "Setting execution policy."
set-executionpolicy bypass -Force
echo "Installing NuGet latest"
Install-PackageProvider -Name NuGet -Force
echo "Installing AzureAD module."
Install-Module AzureAD -Force
echo "Installing and importing 3rd party module: AADInternals"
Install-Module AADInternals -Force
Import-Module AADInternals -Force
$AdminUserName = "Azure join account username"
$AdminPassword = "Azure join account password"
$SecurePassword = ConvertTo-SecureString $AdminPassword -AsPlainText -Force
$Credential = New-Object System.Management.Automation.PSCredential -argumentlist $AdminUserName, $SecurePassword
Get-AADIntAccessTokenForAADGraph -Resource urn:ms-drs:enterpriseregistration.windows.net -Credential $Credential -SaveToCache
$bprt = New-AADIntBulkPRTToken -Name "BPRT AzureJoinScriptUser"
Get-AADIntAccessTokenForAADJoin -BPRT $BPRT -SaveToCache
$compname = hostname
Join-AADIntDeviceToAzureAD -DeviceName $compname
$pfxfile = ".\" + (dir .\ -n *.pfx)
Join-AADIntLocalDeviceToAzureAD -UserPrincipalName $AdminUserName -PfxFileName $pfxfile
Connect-AzureAD -Credential $Credential
echo "Cleaning Up..."
Start-Sleep -Seconds 25
Remove-AzureADUser -ObjectID (Get-AzureADUser | where {$_.DisplayName -eq "BPRT AzureJoinScriptUser"}).UserPrincipalName
echo "Operations complete."
And there it is! A working script to join devices to an Azure AD tenant. No more rebuilding packages, no more manually entering credentials. Using
AADinternals, I've successfully defied what the internet declared: "It's impossible."
I'd like to clarify that tools on Dr. Nestori's website aren't really for this purpose. Certainly, they can be used in any way you would like, but AADinternals is closer to a penetration testing tool. Dr. Nestori was kind enough to help me use it for simplifying my workflow. I highly recommend reading through his website.