“My Azure AD has been breached! What now?”

In the cloud, we are all under attack, every day, every minute! In the spirit of zero trust we should always assume breach. The attack will come and it can strike from any direction – the Internet, on-prem, BYOD, etc. The first thing an organisation experiences after the fact is often confusion, fear, and panic. Not the best mix of feelings to have while trying to sort things out! Most organisations don’t have a clear plan of what to do next…

In this blog post I will focus on the first steps you should take in your Azure AD when you suspect/knows you’ve been breached. I humbly leave other services, infrastructure, and on-prem environments to better suited experts out there. I’ve created a bunch of PowerShell scripts that you can run to understand Azure AD role assignments, detect possible attacker accounts, and a couple of panic button mitigation scripts. This and more should be part of your organisations “kick-out” process to get rid of the attackers ones they’re in. I’ve also included these script examples in the latest version of DCToolbox.

Preparations

To use any of the scripts in this blog post, first, you need to connect to Azure AD with PowerShell. Note that you need to use the AzureADPreview module for some of the CMDlets used in this blog post.

# *** Connect to Azure AD ***
Import-Module AzureADPreview
Connect-AzureAD

We need to decide on what Azure AD roles we want to inspect. Of course, you can inspect all of them (60+) but my recommendation is to start with the really important ones, the ones with the highest privileges. You can then work yourself downwards. This list of roles is a good start:

# *** Interesting Azure AD roles to inspect ***
$InterestingDirectoryRoles = 'Global Administrator',
'Global Reader',
'Privileged Role Administrator',
'Security Administrator',
'Application Administrator',
'Compliance Administrator'

Inspect Azure AD Role Assignments

To get an overview of the current role assignments of our selected roles, and to hopefully catch any interesting insights that we can use to kick out the attackers, we can run any of the following two scripts. The first one is for tenants where Azure AD Privileged Identity Management is in use, and the second one is for tenants NOT using PIM.

Run this if you use PIM:

# *** Inspect current Azure AD admins (if you use Azure AD PIM) ***

# Fetch tenant ID.
$TenantID = (Get-AzureADTenantDetail).ObjectId

# Fetch all Azure AD role definitions.
$AzureADRoleDefinitions = Get-AzureADMSPrivilegedRoleDefinition -ProviderId "aadRoles" -ResourceId $TenantID | Where-Object { $_.DisplayName -in $InterestingDirectoryRoles }

# Fetch all Azure AD PIM role assignments.
$AzureADDirectoryRoleAssignments = Get-AzureADMSPrivilegedRoleAssignment -ProviderId "aadRoles" -ResourceId $TenantID | Where-Object { $_.RoleDefinitionId -in $AzureADRoleDefinitions.Id }

# Fetch Azure AD role members for each role and format as custom object.
$AzureADDirectoryRoleMembers = foreach ($AzureADDirectoryRoleAssignment in $AzureADDirectoryRoleAssignments) {
    $UserAccountDetails = Get-AzureAdUser -ObjectId $AzureADDirectoryRoleAssignment.SubjectId

    $LastLogon = (Get-AzureAdAuditSigninLogs -top 1 -filter "UserId eq '$($AzureADDirectoryRoleAssignment.SubjectId)'" | Select-Object CreatedDateTime).CreatedDateTime

    if ($LastLogon) {
        $LastLogon = [System.TimeZoneInfo]::ConvertTimeBySystemTimeZoneId((Get-Date -Date $LastLogon), (Get-TimeZone).Id)
    }

    $CustomObject = New-Object -TypeName psobject
    $CustomObject | Add-Member -MemberType NoteProperty -Name "AzureADDirectoryRole" -Value ($AzureADRoleDefinitions | Where-Object { $_.Id -eq $AzureADDirectoryRoleAssignment.RoleDefinitionId }).DisplayName
    $CustomObject | Add-Member -MemberType NoteProperty -Name "UserID" -Value $UserAccountDetails.ObjectID
    $CustomObject | Add-Member -MemberType NoteProperty -Name "UserAccount" -Value $UserAccountDetails.DisplayName
    $CustomObject | Add-Member -MemberType NoteProperty -Name "UserPrincipalName" -Value $UserAccountDetails.UserPrincipalName
    $CustomObject | Add-Member -MemberType NoteProperty -Name "AssignmentState" -Value $AzureADDirectoryRoleAssignment.AssignmentState
    $CustomObject | Add-Member -MemberType NoteProperty -Name "AccountCreated" -Value $UserAccountDetails.ExtensionProperty.createdDateTime
    $CustomObject | Add-Member -MemberType NoteProperty -Name "LastLogon" -Value $LastLogon
    $CustomObject
}

# List all Azure AD role members (newest first).
$AzureADDirectoryRoleMembers | Sort-Object AccountCreated -Descending | Format-Table

Run this if you DON’T use PIM:

# *** Inspect current Azure AD admins (only if you do NOT use Azure AD PIM) ***

# Fetch Azure AD role details.
$AzureADDirectoryRoles = Get-AzureADDirectoryRole | Where-Object { $_.DisplayName -in $InterestingDirectoryRoles }

# Fetch Azure AD role members for each role and format as custom object.
$AzureADDirectoryRoleMembers = foreach ($AzureADDirectoryRole in $AzureADDirectoryRoles) {
    $RoleAssignments = Get-AzureADDirectoryRoleMember -ObjectId $AzureADDirectoryRole.ObjectId

    foreach ($RoleAssignment in $RoleAssignments) {
        $LastLogon = (Get-AzureAdAuditSigninLogs -top 1 -filter "UserId eq '$($RoleAssignment.ObjectId)'" | Select-Object CreatedDateTime).CreatedDateTime

        if ($LastLogon) {
            $LastLogon = [System.TimeZoneInfo]::ConvertTimeBySystemTimeZoneId((Get-Date -Date $LastLogon), (Get-TimeZone).Id)
        }

        $CustomObject = New-Object -TypeName psobject
        $CustomObject | Add-Member -MemberType NoteProperty -Name "AzureADDirectoryRole" -Value $AzureADDirectoryRole.DisplayName
        $CustomObject | Add-Member -MemberType NoteProperty -Name "UserID" -Value $RoleAssignment.ObjectID
        $CustomObject | Add-Member -MemberType NoteProperty -Name "UserAccount" -Value $RoleAssignment.DisplayName
        $CustomObject | Add-Member -MemberType NoteProperty -Name "UserPrincipalName" -Value $RoleAssignment.UserPrincipalName
        $CustomObject | Add-Member -MemberType NoteProperty -Name "AccountCreated" -Value $RoleAssignment.ExtensionProperty.createdDateTime
        $CustomObject | Add-Member -MemberType NoteProperty -Name "LastLogon" -Value $LastLogon
        $CustomObject
    }
}

# List all Azure AD role members (newest first).
$AzureADDirectoryRoleMembers | Sort-Object AccountCreated -Descending | Format-Table

The output looks something like this and it show you all assigned accounts sorted by when they last signed in. Try to find admin accounts that you don’t recognise and check when they last signed in. Maybe there is something fishy going on here?

Output:

On-Prem Accounts With Azure AD Role Assignments

We’ve recently seen how vulnerable the cloud is to on-prem attacks. Solarigate learned us that the attack will come from inside, and to protect our Microsoft tenant we need to make sure that no synced accounts are used as admin accounts in the cloud. All admin accounts in Azure AD should be cloud-only accounts!

This script lists all of the admin accounts from our last script and finds out if there are synced accounts in the list. If you have synced admin accounts, they might be the ones that you want to disable first!

# *** Check if admin accounts are synced from on-prem (bad security) ***

# Loop through the admins from previous output and fetch sync status.
$SyncedAdmins = foreach ($AzureADDirectoryRoleMember in $AzureADDirectoryRoleMembers) {
    $IsSynced = (Get-AzureADUser -ObjectId $AzureADDirectoryRoleMember.UserID | Where-Object {$_.DirSyncEnabled -eq $true}).DirSyncEnabled

    $CustomObject = New-Object -TypeName psobject
    $CustomObject | Add-Member -MemberType NoteProperty -Name "UserID" -Value $AzureADDirectoryRoleMember.UserID
    $CustomObject | Add-Member -MemberType NoteProperty -Name "UserAccount" -Value $AzureADDirectoryRoleMember.UserAccount
    $CustomObject | Add-Member -MemberType NoteProperty -Name "UserPrincipalName" -Value $AzureADDirectoryRoleMember.UserPrincipalName

    if ($IsSynced) {
        $CustomObject | Add-Member -MemberType NoteProperty -Name "SyncedOnPremAccount" -Value 'True'
    } else {
        $CustomObject | Add-Member -MemberType NoteProperty -Name "SyncedOnPremAccount" -Value 'False'
    }
    
    $CustomObject
}

# List admins (synced on-prem accounts first).
$SyncedAdmins | Sort-Object UserPrincipalName -Descending -Unique | Sort-Object SyncedOnPremAccount -Descending | Format-Table

Output:

Panic Button: On-Prem Sync Admin Accounts

If you suspect that you are under attack and that the attacker might have a foothold on-prem, you need to disable those admin accounts in the cloud. This is a risky operation and you need to make sure that you have other global admins in the cloud. The admin user that you run this PowerShell script with should be a cloud-only Global Admin account. Note that synced accounts probably will be re-enabled at the next sync and you need to pause your Azure AD Connect sync during an ongoing attack.

# *** ON-PREM SYNC PANIC BUTTON: Block all Azure AD admin accounts that are synced from on-prem ***
# WARNING: Make sure you understand what you're doing before running this script!

# Loop through admins synced from on-prem and block sign-ins.
foreach ($SyncedAdmin in ($SyncedAdmins | Where-Object { $_.SyncedOnPremAccount -eq 'True' })) {
    Set-AzureADUser -ObjectID $SyncedAdmin.UserID -AccountEnabled $false
}

# Check account status.
foreach ($SyncedAdmin in ($SyncedAdmins | Where-Object { $_.SyncedOnPremAccount -eq 'True' })) {
    Get-AzureADUser -ObjectID $SyncedAdmin.UserID | Select-Object userPrincipalName, AccountEnabled
}

Output:

Admins Last Password Set Time

Another thing that is good to investigate is which admin accounts recently changed their password. By doing this, you might be able to understand which admin accounts the attackers are using, and disable them.

# *** Check admins last password set time ***

# Connect to Microsoft online services.
Connect-MsolService

# Loop through the admins from previous output and fetch LastPasswordChangeTimeStamp.
$AdminPasswordChanges = foreach ($AzureADDirectoryRoleMember in ($AzureADDirectoryRoleMembers| Sort-Object UserID -Unique)) {
    $LastPasswordChangeTimeStamp = [System.TimeZoneInfo]::ConvertTimeBySystemTimeZoneId((Get-Date -Date (Get-MsolUser -ObjectId $AzureADDirectoryRoleMember.UserID | Select-Object LastPasswordChangeTimeStamp).LastPasswordChangeTimeStamp), (Get-TimeZone).Id)

    $CustomObject = New-Object -TypeName psobject
    $CustomObject | Add-Member -MemberType NoteProperty -Name "UserID" -Value $AzureADDirectoryRoleMember.UserID
    $CustomObject | Add-Member -MemberType NoteProperty -Name "UserAccount" -Value $AzureADDirectoryRoleMember.UserAccount
    $CustomObject | Add-Member -MemberType NoteProperty -Name "UserPrincipalName" -Value $AzureADDirectoryRoleMember.UserPrincipalName
    $CustomObject | Add-Member -MemberType NoteProperty -Name "LastPasswordChangeTimeStamp" -Value $LastPasswordChangeTimeStamp
    $CustomObject
}

# List admins (newest passwords first).
$AdminPasswordChanges | Sort-Object LastPasswordChangeTimeStamp -Descending | Format-Table

Output:

Panic Button: Change All Admin Passwords

This script is dangerous to run but might be necessary to make sure you kick out the attackers. Understand what you are doing before you run this! The script will reset the password of ALL admin accounts from the previous scripts EXCEPT for your specified break glass accounts (set the $BreakGlassAccounts variable) and the current PowerShell user.

The script generates extreme passwords (using New-Guid) and they are never stored in memory or shown on the screen. You can modify the script to do this of course but the purpose of the script is to kick-out the attackers by removing Azure AD backdoors. Ones they all have been reset, set new passwords for the ones you need during incident management. Monitor all admin activity closely.

# *** ADMIN PASSWORD PANIC BUTTON: Reset passwords for all Azure AD admins (except for current user and break glass accounts) ***
# WARNING: Make sure you understand what you're doing before running this script!

# IMPORTANT: Define your break glass accounts.
$BreakGlassAccounts = 'breakglass1@example.onmicrosoft.com', 'breakglass2@example.onmicrosoft.com'

# The current user running PowerShell against Azure AD.
$CurrentUser = (Get-AzureADCurrentSessionInfo).Account.Id

# Loop through admins and set new complex passwords (using generated GUIDs).
foreach ($AzureADDirectoryRoleMember in ($AzureADDirectoryRoleMembers | Sort-Object UserPrincipalName -Unique)) {
    if ($AzureADDirectoryRoleMember.UserPrincipalName -notin $BreakGlassAccounts -and $AzureADDirectoryRoleMember.UserPrincipalName -ne $CurrentUser) {
        Write-Verbose -Verbose -Message "Setting new password for $($AzureADDirectoryRoleMember.UserPrincipalName)..."
        Set-AzureADUserPassword -ObjectId $AzureADDirectoryRoleMember.UserID -Password (ConvertTo-SecureString (New-Guid).Guid -AsPlainText -Force)
    } else {
        Write-Verbose -Verbose -Message "Skipping $($AzureADDirectoryRoleMember.UserPrincipalName)!"
    }
}

Output:

Summary

I hope this inspires you to create your own kick-out plan and to practice it ones in a while. If you are well prepared you will be able to resolve the coming attack much quicker and minimise the damage. If you don’t have a plan at all, it might take months and cost you a fortune.

Of course, this is only part of everything you need to do during an attack. You need to do the same on-prem and in other services as well. When you feel confident that you have control of your Azure AD admin accounts, you need to continue looking for backdoors. If there is interest out there, I will add more content like this for things like service principals and devices. Let me know what you think!

Please follow me here, on LinkedIn and on Twitter!

@DanielChronlund

4 thoughts on ““My Azure AD has been breached! What now?”

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s