Azure AD Kerberos Tickets: Pivoting to the Cloud
If you've ever been doing an Internal Penetration test where you've reached Domain Admin status and you have a cloud presence, your entire Azure cloud can still be compromised. In this blog, I'll take you through this scenario and show you the dangers of machine account SSO compromise. We will do so without extracting any user account hashes and will have the ability to impersonate any account without MFA to achieve full cloud dominance.
Scenario:
While an Internal Penetration test was being conducted, a service account with backup privileges to a Domain Controller (DC) was compromised. The team conducting the test was able to achieve full internal domain compromise. Using SecretsDump, the team extracted the machine account, AZUREADSSOACC$. This indicates that Azure SSO may be enabled in the target tenant.
To confirm Azure SSO was in use in this hybrid environment, AADInternals was used to perform reconnaissance on the Azure environment as an outsider. AADInternals can be obtained from the PowerShell Gallery or from GitHub. We can perform reconnaissance with the following command:
Invoke-AADIntReconAsOutsider -Domain domain.local | Format-Table
Now that we have confirmed that SSO is in use, we will need to do some initial reconnaissance of the Azure environment. First, we will need to pick out a user. Service accounts are preferred since they are normally not backed with any form of MFA. One common issue that occurs when an enterprise syncs Azure AD for the first time, is that they will sync all on-premises AD environment IDs to the cloud. Service accounts can easily be pulled from an internal foothold in an AD environment.
An easy way to manually confirm if an ID is synced to an Azure environment is to simply type the full UPN in the Azure portal, and see if it asks for a password. In the case below, we are prompted to enter a password. This is a good sign that we have an ID that can be used for initial reconnaissance.
Going back to our foothold in the internal AD environment, usernames beginning with 'SVC' SIDs were exfiltrated using rpcclient. Using rpcclient to query accounts directly from a DC does not produce any alerts from Defender for Identity.
We can now confirm we have the following pieces of information needed to impersonate accounts in the Azure cloud:
- AZUREADSSOACC$ NTLM hash
- SIDs to accounts of interest that may not be backed by MFA
- None of the service accounts have conditional access policies for sign-in restrictions from untrusted locations
Using AADInternals, we can create a new Kerberos ticket. I should also mention that getting a Kerberos ticket and requesting a token for services such as Microsoft Graph does not require line of sight to an on-premises DC. We are now issuing an access request via Kerberos to the cloud. If you receive a token, then you're in good shape to start doing initial cloud reconnaissance with an Azure tool called ROADtools(One of my personal favorites). Keep in mind, we have zero knowledge of what the password is to the svc_mssql account.
First, command grabs a Kerberos ticket for the svc_mssql account, then we use the NTLM hash from the AZUREADSSOACC$ machine account.
$kerberos=NewAADIntKerberosTicket -SidString <Internal AD SID> -Hash <SSOACC$ NTLM Hash>
Second, command initiates an access token for Azure AD graph from the Kerberos ticket. This is necessary so we can pass the token to use with ROADTools.
Get-AADIntAccessTokenForAADGraph -KerberosTicket $kerberos -Domain domain.local
Next, we replay the token with ROADRecon for initial authentication by issuing the following command from a Linux box with ROADRecon installed.
roadrecon auth –access-token <Token>
Once the tokens are written, you can issue a gather command in ROADRecon. This will gather information for all users, groups, and anyone with special privileges in Azure.
roadrecon gather
After the initial recon with road tools is finished, we can load the GUI, which will spin up a web server running on port 5000. Take care in doing this by making sure your attack box is not exposed on a public IP, as this would be accessible for anyone making a connection on this port.
roadrecon gui
Inside the ROADRecon GUI, we can see a few different options at our disposal for global administrator takeover. Two AD account types have global administrator privileges, ADSync and svc_backup. ADSync is more than likely a manually created service account running directory synchronizations with Azure. This is very common to see. We also can distinguish between cloud accounts and AD accounts within the GUI as well.
Next, lets flip back into PowerShell. We can connect to Azure AD with the AadAccessToken parameter and replay the same token from the previous one we generated.
Connect-AzureAD -AadAccessToken <Token>
At this point, we no longer need access to the DC, even if the Blue Team evicts us and changes the passwords in their environment. If the AZUREADSSOACC$ machine account doesn’t have the NTLM hash rotated, we will continue to have persistent access to the cloud environment with the previously identified service accounts. If we need to gather SIDs via impersonation, we can do so with the following command.
Get-AzureADUser | Select UserPrincipalName, OnPremisesSecurityIdentifier
So where do we go from here? We've already identified the SID for the adsync account, so the next step is to impersonate the global administrator by using the adsync account. Since we have access to the global administrator service account, we can repeat the process above but in a slightly different way with AADInternals.
A few things to keep in mind on this next playbook with impersonating a global administrator:
- A service account may not have access to Azure subscriptions to the account.
- We'll use the
-savetocache
command so that any AADInternals commands or Azure PowerShell commands we use will just pull the tokens we have from cache. - We'll elevate our authenticated global administrator to Azure User Access Administrator with the
Grant-AADIntAzureUserAccessAdminRole
that is scoped at the root “/” of our account.- This will allow the global administrator to see subscriptions and assign permissions to running Azure assets if necessary.
- We'll confirm that we can see the subscriptions with the
Get-AADIntAzureSubscriptions
command.
First, grab your Kerberos ticket.
$kerberos=NewAADIntKerberosTicket -SidString <Internal AD SID> -Hash <SSOACC$ NTLM Hash>
Grab the access tokens to three different services. I find this is easiest when using AADInternals.
Get-AADIntAccessTokenForAADGraph -KerberosTicket $kerberos -Domain domain.local -SaveToCache
Get-AADIntAccessTokenForMSGraph -KerberosTicket $kerberos -Domain domain.local -SaveToCache
Get-AADIntAccessTokenForAzureCoreManagement -KerberosTicket $kerberos -Domain domain.local -SaveToCache
Elevate the access to User Access Administrator to the root of Azure.
Grant-AADIntAzureUserAccessAdminRole
Make sure you can now see subscriptions after you've elevated permissions to the root of Azure.
Get-AADIntAzureSubscriptions
Now that we've ensured that our compromised global administrator service account has full subscription access, we can create a new cloud user for interactive sign-ins with AADInternals. Make sure you save the ObjectID that gets flushed out with the NewAADIntUser
command since we'll need it for later.
New-AADIntUser -UserPrincipalName [email protected] -DisplayName "pwned user"
Creating the user above will also give us the password to the account in the output as well.
Now, with our freshly minted subscription rights via our adsync account, we will use connect-azaccount
and some PowerShell commands in a fresh terminal window so we can re-walk the process of impersonating our adsync account, grab a token for Azure core management, and log in with connect-azaccount with the token.
Here is our command flow using the adsync account:
$kerberos=NewAADIntKerberosTicket -SidString “<Internal AD sid>” -Hash <SSOACC$ NTLM Hash>
Get-AADIntAccessTokenForAzureCoreManagement -KerberosTicket $kerberos -Domain domain.local -SaveToCache
Connect-AzAccount -AccessToken <Token>
Next, we can use Get-AZSubscription
and issue a $subScope variable. This will make it easier to add subscription ownership to the pwned.user account we have already created.
$subScope = "subscriptions/<id of subscription>"
Now we can issue a New-AzRoleAssignment
command with the ObjectID of the pwned.user account and give it ownership rights to the subscription.
New-AzRoleAssignment -ObjectID <object id of account> -RoleDefinitionName "Owner" -$subScope
Using our new user account, we can sign into Azure and see that we have ownership access to the subscription. The sky is the limit at this point.
Conclusion:
Great care should be taken into consideration when using Azure SSO in your enterprise. The seamless authentication experience has great benefits for end-users getting access granted to different workloads without the need to put in a password multiple times, but danger is also present if an enterprise isn’t including the AZUREADSSOACC$ in their security hygiene process. Just as you should be rotating the krbtgt account to your internal domain on a regular basis, you should be including the AZUREADSSOACC$ machine account key rotation as well. Microsoft has this process documented in the following link:
Service account sign on should also be controlled in conditional access by trusted locations. Most of the time, service accounts are probably not signing into Azure at all. Having a conditional access policy in place for service accounts will minimize pivoting options for an attacker.
References:
AADInternals - https://aadinternals.com/aadinternals/
ROADtools - https://github.com/dirkjanm/ROADtools
Special thanks to @DrAzureAD for AADInternals and @_dirkjan for ROADtools.