#Requires -Version 5.1 <# ########################################################################################### # # # AzureStealth - Discover the most privileged users in Azure and secure\target them # # # ########################################################################################### # # # # # Written by: Asaf Hecht (@Hechtov) # # CyberArk Labs # # Future updates via Twitter # # # ########################################################################################### Versions Notes: Version 0.1 - 03.03.19 Version 0.2 - 21.03.19 Version 0.3 - 08.07.19 Version 0.4 - 11.07.19 Version 1.0 - 12.07.19 - published on GitHub as part of SkyArk tool: https://github.com/cyberark/SkyArk https://github.com/cyberark/SkyArk/tree/master/AzureStealth Version 1.1 - 01.09.19 - added two sensitive directory roles Version 1.2 - 07.09.20 - add support for CSP suscriptions Version 1.3 - 01.07.21 - add support for scanning for all roles and all scopes ########################################################################################### HOW TO INSTALL AZURE POWERSHELL MODULE: Guide for installing Azure "AZ" PowerShell Module: https://docs.microsoft.com/en-us/powershell/azure/install-az-ps Guide for installing Azure "AzureAD" PowerShell Module (you need this in addtion to the az module): https://docs.microsoft.com/en-us/powershell/azure/active-directory/install-adv2 If local admin (PowerShell command): Install-Module -Name Az -AllowClobber Install-Module AzureAD -AllowClobber Else: Install-Module -Name Az -AllowClobber -Scope CurrentUser Install-Module AzureAD -AllowClobber -Scope CurrentUser ########################################################################################### HOW TO RUN AZURESTEALTH: 1) Download/sync locally the script file AzureStealth.ps1 2) Open PowerShell in the AzureStealth folder with the permission to run scripts: "powershell -ExecutionPolicy Bypass -NoProfile" 3) Run the following commands (1) Import-Module .\AzureStealth.ps1 -Force (load the scan) (2) Scan-AzureAdmins (start the AzureStealth scan) Optional commands: (-) Scan-AzureAdmins -UseCurrentCred (if you used Azure PowerShell in the past, it uses the current cached Azure credentials) (-) Scan-AzureAdmins -GetPrivilegedUserPhotos (if you want to focus only on the privileged Azure users, you can also get their photos (if they have profile photos)) ########################################################################################### HOW TO RUN AZURESTEALTH DIRECTLY FROM AZURE'S CLOUDSHELL: You can load and run the scan directly from GitHub, simply use the following PowerShell commands: (1) IEX (New-Object Net.WebClient).DownloadString('https://raw.githubusercontent.com/cyberark/SkyArk/master/AzureStealth/AzureStealth.ps1') (2) Scan-AzureAdmins ########################################################################################### #> $AzureStealthVersion = "v1.3" $AzureStealth = @" ------------------------------------------------------------------------------- _____ _ _ _ _ /\ / ____| | | | | | | / \ _____ _ _ __ ___| (___ | |_ ___ __ _| | |_| |__ / /\ \ |_ / | | | `'__/ _ \\___ \| __/ _ \/ _`` | | __| `'_ \ / ____ \ / /| |_| | | | __/____) | || __/ (_| | | |_| | | | /_/ \_\/___|\__,_|_| \___|_____/ \__\___|\__,_|_|\__|_| |_| "@ $Author = @" ------------------------------------------------------------------------------- Author: Asaf Hecht - @Hechtov CyberArk Labs Future updates via Twitter ------------------------------------------------------------------------------- "@ Write-Output $AzureStealth Write-Output "`n *** Welcome to AzureStealth $AzureStealthVersion ***`n" Write-Output " Discover the most privileged users in Azure and secure\target them :)`n" Write-Output $Author # Check if the PowerShell Azure Module exists on the machine function Check-AzureModule { $oneAzureModuleExist = $true # Try loading the AZ PowerShell Module try { $azModule = Get-InstalledModule -Name Az -ErrorAction Stop } Catch { Write-Host "`nCouldn't find the Azure `"AZ`" PowerShell Module" -ForegroundColor Yellow Write-Host "The tool will prompt you and install it using the `"Install-Module -Name Az`" command" -ForegroundColor Yellow if ([bool](([System.Security.Principal.WindowsIdentity]::GetCurrent()).groups -match "S-1-5-32-544")) { Install-Module -Name Az -AllowClobber } else { Install-Module -Name Az -AllowClobber -Scope CurrentUser } } try { $azModule = Get-InstalledModule -Name Az -ErrorAction Stop if ($azModule){ Write-Host "`n [+] Great, Azure `"AZ`" PowerShell Module exists`n" } } catch { Write-Host "`nEncountered an error - couldn't find the Azure `"AZ`" PowerShell Module" -BackgroundColor Red Write-Host "Please install Azure Az PowerShell Module (requires PowerShell version 5.1+)" -BackgroundColor Red Write-Host "Installation guideline:" -BackgroundColor Red Write-Host "https://docs.microsoft.com/en-us/powershell/azure/install-az-ps" -BackgroundColor Red $oneAzureModuleExist = $false #Return $false } # Try loading the AzureAD PowerShell Module try { $azModule = Get-InstalledModule -Name AzureAD -ErrorAction Stop } Catch { Write-Host "`nCouldn't find the Azure `"AzureAD`" PowerShell Module" -ForegroundColor Yellow Write-Host "The tool will prompt you and install it using the `"Install-Module -Name AzureAD`" command" -ForegroundColor Yellow if ([bool](([System.Security.Principal.WindowsIdentity]::GetCurrent()).groups -match "S-1-5-32-544")) { Install-Module -Name AzureAD -AllowClobber } else { Install-Module -Name AzureAD -AllowClobber -Scope CurrentUser } } try { $azModule = Get-InstalledModule -Name AzureAD -ErrorAction Stop if ($azModule){ Write-Host " [+] Great, Azure `"AzureAD`" PowerShell Module exists`n" $oneAzureModuleExist = $true } } catch { Write-Host "`nEncountered an error - couldn't find the Azure `"AzureAD`" PowerShell Module" -BackgroundColor Red Write-Host "Please install Azure Az PowerShell Module (requires PowerShell version 5.1+)" -BackgroundColor Red Write-Host "Installation guideline:" -BackgroundColor Red Write-Host "https://docs.microsoft.com/en-us/powershell/azure/active-directory/install-adv2" -BackgroundColor Red if ($oneAzureModuleExist -eq $false) { $oneAzureModuleExist = $false } } Return $oneAzureModuleExist } # Connect to the target Azure environment function Connect-AzureEnvironment { try { $answer = "n" $AzContext = Get-AzContext | Where-Object {($_.tenant) -or ($_.TenantId)} if ($AzContext.Account) { Write-Host "The current Azure account context is set for:" Write-Host ($AzContext | select Name, Account, Environment | Format-List | Out-String) -NoNewline $answer = Read-Host "Do you want to use this Azure Account context? Press (y/Y or n/N)" } if ($answer.ToLower() -notmatch "y") { $AzAllCachedContext = Get-AzContext -ListAvailable $AzCachedContext = $AzAllCachedContext | Where-Object {($_.Tenant) -or ($_.TenantId)} if ($AzCachedContext) { Write-Host "The follwoing Azure user/s are available through the cache:`n" $counter = 0 $AzCachedContext | foreach { $counter++ $contextAccount = $_.Account.id $contextName = $_.Name $contextNameEx = "*" + $contextAccount + "*" if ($contextName -like $contextNameEx){ Write-Host "$counter) Name: $contextName" } else { Write-Host "$counter) Name: $contextName - $contextAccount" } } $contextAnswer = Read-Host "`nDo you want to use one of the above cached users?`nPress the user's number from above (or n/N for chosing a new user)" if ($contextAnswer.ToString() -le $counter) { $contextNum = [int]$contextAnswer $contextNum-- $chosenAccount = $AzCachedContext[$contextNum].Account.id Write-Host "`nYou chose to proceed with $chosenAccount" Set-AzContext -Context $AzCachedContext[$contextNum] -ErrorAction Stop > $null return $true } } Write-Host "Please connect to your desired Azure environment" Write-Host "These are the available Azure environments:" $AzEnvironment = Get-AzEnvironment | select Name, ResourceManagerUrl Write-Host ($AzEnvironment | Format-Table | Out-String) -NoNewline $answer = read-host "Do you use the US-based `"AzureCloud`" environment? Press (y/Y or n/N)" $rand = Get-Random -Maximum 10000 if ($answer.ToLower() -match "y") { Connect-AzAccount -ContextName "Azure$rand" -ErrorAction Stop > $null } else { $AzEnvironment = Read-Host "Ok, please write your Azure environment Name from the list above.`nAzure environment Name" Connect-AzAccount -ContextName "Azure$rand" -Environment $AzEnvironment -ErrorAction Stop > $null } } } catch { Write-Host "Encountered an error - check again the inserted Azure Credentials" -BackgroundColor red Write-Host "There was a problem when trying to access the target Azure Tenant\Subscription" -BackgroundColor Red Write-Host "Please try again... and use a valid Azure user" Write-Host "You can also try different Azure user credentials or test the scan on a different environment" return $false } Write-Host "`n [+] Got valid Azure credentials" return $true } # Connect to the target Azure Directory function Connect-AzureActiveDirectory { [CmdletBinding()] param( $AzContext ) try { $tenantId = $AzContext.Tenant.Id $accountId = $AzContext.Account.Id if ($tenantId){ $AzAD = Connect-AzureAD -TenantId $tenantId -AccountId $accountId -ErrorAction Stop } else { $AzAD = Connect-AzureAD -AccountId $accountId -ErrorAction Stop } $directoryName = $AzAD.TenantDomain Write-Host "`n [+] Connected to the Azure Active Directory: "$directoryName } catch { Write-Host "`nCouldn't connect to the Azure Active Directory using the chosen user" -BackgroundColor red Write-Host "Please try again... and use a valid Azure AD user" -BackgroundColor red Write-Host "The tool will continue but it won't scan the Tenant Directory level (only subscriptions will be scanned)" -BackgroundColor red Write-Host "You can also try different Azure user credentials or test the scan on a different environment" return $false } return $true } # Add the detected privileged entity to the Results Dictionary function Add-PrivilegeAzureEntity { [CmdletBinding()] param( [string] $entityId, [string] $DirectoryTenantID, [string] $SubscriptionID, [string] $RoleId, [string] $PrivilegeReason, [string] $ClassicSubscriptionAdminRole, [string] $scope, [switch] $ClassicAdmin, [switch] $GroupPrivilege, [string] $PrivilegeGroup ) $fullDirectoryAdmins = @("Application Administrator", "Authentication Administrator",` "Password Administrator", "Privileged Authentication Administrator",` "Cloud Application Administrator", "Helpdesk Administrator", "Privileged Role Administrator", "User Account Administrator") $sensitiveDirectoryAdmins = @("SharePoint Service Administrator", "Exchange Service Administrator",` "Conditional Access Administrator", "Security Administrator") $subscriptionAdmins = @("Owner","Contributor", "User Access Administrator") $RBACRoleAdminName = $roleDict[$RoleId].Name if ($ClassicAdmin) { $ClassicAdministrator = $PrivilegeReason $PrivilegeType = "Azure Subscription Full Admin" $RoleId = "Classic Subscription Admin" } else { if ($PrivilegeReason -eq "Company Administrator") { $roleDict[$RoleId].DisplayName = "Global Administrator" $PrivilegeType = "Azure Directory Full Admin" } elseif ($fullDirectoryAdmins -contains $PrivilegeReason) { $PrivilegeType = "Azure Directory Shadow Admin" } elseif ($sensitiveDirectoryAdmins -contains $PrivilegeReason) { $PrivilegeType = "Azure Directory Sensitive Admin" } elseif ($subscriptionAdmins -contains $PrivilegeReason) { $PrivilegeType = "Azure Subscription Full Admin" } elseif ($PrivilegeReason -eq "Privileged Group Owner") { $PrivilegeType = "Azure Subscription Shadow Admin" $RBACRoleAdminName = "Privileged Group Owner" }elseif ($SubscriptionID) { $PrivilegeType = "Azure Subscription Role Admin" } } if ($entityDict[$entityId].ExtensionProperty.createdDateTime) { $EntityCreationDate = Get-Date ($entityDict[$entityId].ExtensionProperty.createdDateTime) -Format "yyyMMdd" } $customRole = "" if ($roleDict[$RoleId].IsCustom) { $PrivilegeType = "Azure Subscription Shadow Admin" $customRole = $True } If($GroupPrivilege){ $Assignment = $PrivilegeGroup }else{ $Assignment = 'DirectAssignment' } $entityLine = [PSCustomObject][ordered] @{ PrivilegeType = [string]$PrivilegeType EntityDisplayName = [string]$entityDict[$entityId].DisplayName EntityPrincipalName = [string]$entityDict[$entityId].UserPrincipalName EntityType = [string]$entityDict[$entityId].ObjectType DirectoryRoleAdminName = [string]$roleDict[$RoleId].DisplayName ClassicSubscriptionAdmin = [string]$ClassicAdministrator RBACRoleAdminName = [string]$RBACRoleAdminName SubscriptionName = [string]$subscriptionDict[$SubscriptionID].Name SubscriptionID = [string]$SubscriptionID SubscriptionStatus = [string]$subscriptionDict[$SubscriptionID].State TenantDisplayName = [string]$tenantDict[$TenantId].DisplayName TenantInitialName = [string]$tenantDict[$TenantId].InitialDomainName DirectoryTenantID = [string]$DirectoryTenantID EntityCreationDate = [string]$EntityCreationDate EntityId = [string]$entityDict[$entityId].ObjectId EntityHasPhoto = [string]$entityDict[$entityId].EntityHasPhoto UserEnabled = [string]$entityDict[$entityId].AccountEnabled OnPremisesSID = [string]$entityDict[$entityId].OnPremisesSecurityIdentifier RoleIsCustom = [string]$customRole RoleId = [string]$RoleId Assignment = [string]$Assignment Scope = [string]$scope } if ($RoleId) { $entityRand = [string]($entityDict[$entityId].ObjectId) + "-" + [string]$RoleId +"-" + $subscriptionId } else { $rand = $SubscriptionID + $PrivilegeReason $entityRand = [string]($entityDict[$entityId].ObjectId) + "-" + [string]$rand } if (-not $privilegedAzEntitiesDict.contains($entityRand)) { $privilegedAzEntitiesDict.add($entityRand,$entityLine) } } # Add the detected Entity to the Dictionary function Add-EntityToDict { [CmdletBinding()] param( $AzEntityObject, [switch] $externalUser, [switch] $NestedInGroup, [string] $CustomResultsFolder = $customOutputDirectory ) if ($externalUser){ $externalUserObject = [PSCustomObject][ordered] @{ DisplayName = $AzEntityObject UserPrincipalName = $AzEntityObject ObjectType = "User" ObjectId = "ExternalUser-" + $AzEntityObject } $entityDict.add($AzEntityObject, $externalUserObject) } else { $EntityId = $AzEntityObject.ObjectId if (-not $entityDict.contains($EntityId)) { if($CustomResultsFolder){ $resultsFolder = $CustomResultsFolder }else{ $resultsFolder = Join-path -Path $PSScriptRoot -ChildPath ("Results-" + $resultsTime) } $usersPhotoFolder = Join-path -Path $resultsFolder -ChildPath "PrivilegedUserPhotos" $entityHasPhoto = "" if ((-not $CloudShellMode) -and (-not $fullUserReconList)) { if ($AzEntityObject.ExtensionProperty."thumbnailPhoto@odata.mediaEditLink") { $entityHasPhoto = $true $resultsFolderExists = Test-Path -Path $resultsFolder if (-not $resultsFolderExists) { New-Item -ItemType directory -Path $resultsFolder > $null } $usersPhotoFolderExists = Test-Path -Path $usersPhotoFolder if (-not $usersPhotoFolderExists) { New-Item -ItemType directory -Path $usersPhotoFolder > $null } try { Get-AzureADUserThumbnailPhoto -ObjectId $EntityId -FilePath $usersPhotoFolder -ErrorAction SilentlyContinue > $null $entityHasPhoto = $true } catch {} } else { $entityHasPhoto = $false } } $AzEntityObject | Add-Member EntityHasPhoto $entityHasPhoto $entityDict.add($EntityId, $AzEntityObject) } } } # Add the detected Role to the Dictionary function Add-RoleToDict { [CmdletBinding()] param( $RoleObject, [switch] $RbacRole ) if ($RbacRole) { $RoleId = $RoleObject.Id } else { $RoleId = $RoleObject.ObjectId } if (-not $roleDict.contains($RoleId)) { $roleDict.add($RoleId, $RoleObject) } } # Check for directory roles and add to the Dictionary function Check-DirectoryRolesEntities { [CmdletBinding()] param( [string] $directoryRoleName ) $role = Get-AzureADDirectoryRole | Where-Object {$_.displayName -eq $directoryRoleName} if ($role) { Add-RoleToDict -RoleObject $role $globalAdminDB = Get-AzureADDirectoryRoleMember -ObjectId $role.ObjectId | Get-AzureADUser $globalAdminDB | foreach { Add-EntityToDict -AzEntityObject $_ Add-PrivilegeAzureEntity -entityId $_.ObjectId -DirectoryTenantID $TenantId -PrivilegeReason $directoryRoleName -RoleId $role.ObjectId } } } # Scan each tenant for its Azure Admins function Run-TenantScan { [CmdletBinding()] param( [string] $TenantId, [string] $UsedUserPrincipalName, [string] $UsedUserId ) $tenantObject = Get-AzureADTenantDetail if (-not $tenantDict.contains($TenantId)) { $initialDomainName = $tenantObject.VerifiedDomains | Where-Object {$_.Initial} | select Name $tenantObject | Add-Member "InitialDomainName" $initialDomainName.Name $tenantDict.add($TenantId, $tenantObject) } if ($fullUserReconList){ $usersLimit = 200000 $allUsers = Get-AzureADUser -Top $usersLimit $allUsersNumber = $allUsers.count Write-Host " Retrieving information on $allUsersNumber Azure AD users, great reconnaissance, check the results file in the end" $allUsers | foreach { Add-EntityToDict -AzEntityObject $_ } } <# 1. Global Administrator / Company Administrator - Can manage all aspects of Azure AD and Microsoft services that use Azure AD identities. 2. Application Administrator - Users in this role can create and manage all aspects of enterprise applications. 3. Authentication Administrator - Users with this role can set or reset non-password credentials. 4. Cloud Application Administrator - Users in this role have the same permissions as the Application Administrator role, excluding the ability to manage application proxy. 5. Password Administrator / Helpdesk Administrator - Users with this role can change passwords, invalidate refresh tokens, manage service requests. 6. Privileged Role Administrator - Users with this role can manage role assignments in Azure Active Directory. 7. User Account Administrator - Can manage all aspects of users and groups, including resetting passwords for limited admins. #> $privilegedDirectoryRoles = @("Company Administrator","Application Administrator", "Authentication Administrator",` "Password Administrator", "Privileged Authentication Administrator", "Cloud Application Administrator",` "Helpdesk Administrator", "Privileged Role Administrator", "User Account Administrator") $sensitiveDirectoryRoles = @("SharePoint Service Administrator", "Exchange Service Administrator","Conditional Access Administrator", "Security Administrator") $privilegedDirectoryRoles | foreach { Check-DirectoryRolesEntities -directoryRoleName $_ } $sensitiveDirectoryRoles | foreach { Check-DirectoryRolesEntities -directoryRoleName $_ } } # Scan each subscription for its Azure Admins function Run-SubscriptionScan { [CmdletBinding()] param( [string] $subscriptionId, [bool] $AllRoles = $false, [bool] $AllScopes = $false ) if (-not $subscriptionDict.contains($subscriptionId)) { $subscriptionObject = Get-AzSubscription -SubscriptionId $subscriptionId $subscriptionDict.add($subscriptionId, $subscriptionObject) } $tenantId = $subscriptionDict[$subscriptionId].TenantId <# RABC privileged roles names: 1. Owner 2. Contributor 3. User Access Administrator #> $privilegedSubscriptionRoles = @("Owner","Contributor", "User Access Administrator") $privilegedRBACPermissions = @("Microsoft.Authorization/*","Microsoft.Authorization/*/Write",` "Microsoft.Authorization/roleAssignments/*", "Microsoft.Authorization/roleDefinition/*",` "Microsoft.Authorization/roleDefinitions/*", "Microsoft.Authorization/elevateAccess/Action",` "Microsoft.Authorization/roleDefinition/write", "Microsoft.Authorization/roleDefinitions/write",` "Microsoft.Authorization/roleAssignments/write","Microsoft.Authorization/classicAdministrators/write") $privilegedRbacRoles = @() $allRbacRoles = Get-AzRoleDefinition if($AllRoles -eq $true){ $privilegedRbacRoles = $allRbacRoles foreach($r in $privilegedRbacRoles){ Add-RoleToDict -Verbose $r -RbacRole } } else{ $allRbacRoles | foreach { # If this is a built-in RBAC role if (-not $_.IsCustom) { if ($privilegedSubscriptionRoles -contains $_.Name) { Add-RoleToDict -RoleObject $_ -RbacRole $privilegedRbacRoles += $_ } } # If this RBAC role is custom made else { $customRole = $_ $_.Actions | foreach { if ($privilegedRBACPermissions -contains $_) { Add-RoleToDict -RoleObject $customRole -RbacRole $privilegedRbacRoles += $customRole } } } } } # Get the entities with the privileged RBAC roles # handle the error case of "no classic admin could be queried" (if the subscription isn't legacy) try { $subscriptionRoleAssignments = Get-AzRoleAssignment -IncludeClassicAdministrators -ErrorAction Stop } catch { $subscriptionRoleAssignments = Get-AzRoleAssignment } # Check classic administrators: $subscriptionRoleAssignments | Where-Object {-not $_.RoleAssignmentId} | foreach { $PrivilegeReason = $_.RoleDefinitionName $userPrincipalName = $_.SignInName $AzEntityObject = Get-AzureADUser -Filter "userPrincipalName eq '$userPrincipalName'" # Check if the user is an external user if (-not $AzEntityObject) { Add-EntityToDict -AzEntityObject $userPrincipalName -externalUser Add-PrivilegeAzureEntity -entityId $userPrincipalName -SubscriptionID $subscriptionId -PrivilegeReason $PrivilegeReason -DirectoryTenantID $TenantId -ClassicAdmin #-RoleId $role.ObjectId } else { if (-not $entityDict.contains($AzEntityObject.ObjectId)){ Add-EntityToDict -AzEntityObject $AzEntityObject } Add-PrivilegeAzureEntity -entityId $AzEntityObject.ObjectId -SubscriptionID $subscriptionId -PrivilegeReason $PrivilegeReason -DirectoryTenantID $TenantId -ClassicAdmin #-RoleId $role.ObjectId } } # Check for privileged RBAC roles $subscriptionRoleAssignments | Where-Object {$privilegedRbacRoles.Id -contains $_.RoleDefinitionId} | foreach { $rbacPrivilegedEntities = @() $PrivilegeReason = $_.RoleDefinitionName $roleId = $_.RoleDefinitionId [string]$scope = "/subscriptions/" + $subscriptionId if ([string]$_.scope -eq $scope) { if ($_.ObjectType -eq "User") { $rbacPrivilegedEntities += $_.ObjectId } elseif ($_.ObjectType -eq "Group") { $newGroupCount = 1 $firstGroup = $true $firstGroupName = $_.DisplayName $groupFromGroups = @() Do { if ($firstGroup) { try { $groupMembers = Get-AzureADGroupMember -ObjectId $_.ObjectId } catch { Write-Verbose "Error with a specific group, maybe the Group is a foreign group and can't be queried" } } else { $groupMembers = $groupFromGroups | Get-AzureADGroupMember -ObjectId $_.ObjectId } $firstGroup = $false $usersFromGroup = $groupMembers | where {$_.ObjectType -eq "User"} $usersFromGroup | foreach { #Adding Users to PrivilegeAzureEntity Dict to include Group ID Add-PrivilegeAzureEntity -entityId $_.ObjectId -SubscriptionID $subscriptionId ` -PrivilegeReason $PrivilegeReason -RoleId $roleId -DirectoryTenantID $TenantId ` -PrivilegeGroup $firstGroupName -GroupPrivilege -scope $scope } $groupFromGroups = $groupMembers | where {$_.ObjectType -eq "Group"} $newGroupCount = $groupFromGroups.count #Add groups found in first group $ownersOfGroup = @() try { $ownersOfGroup = Get-AzureADGroupOwner -ObjectId $_.ObjectId $ownersOfGroup | foreach { if (-not $entityDict.contains($_.ObjectId)){ $AzEntityObject = Get-AzureADUser -ObjectId $_.ObjectId Add-EntityToDict -AzEntityObject $AzEntityObject } Add-PrivilegeAzureEntity -entityId $_.ObjectId -SubscriptionID $subscriptionId ` -PrivilegeReason "Privileged Group Owner" -RoleId $roleId -DirectoryTenantID $TenantId ` -PrivilegeGroup $firstGroupName -GroupPrivilege -scope $scope } } catch { Write-Verbose "Error with a specific group, maybe the Group is a foreign group and can't be queried" } } While ($newGroupCount -ne 0) } } $rbacPrivilegedEntities | foreach { if (-not $entityDict.contains($_)){ $AzEntityObject = Get-AzureADUser -ObjectId $_ Add-EntityToDict -AzEntityObject $AzEntityObject } Add-PrivilegeAzureEntity -entityId $_ -SubscriptionID $subscriptionId ` -PrivilegeReason $PrivilegeReason -RoleId $roleId -DirectoryTenantID $TenantId -scope $scope } if($AllScopes -and ([string]$_.scope -ne $scope)){ $aScope = $_.scope if ($_.ObjectType -eq "User") { Add-PrivilegeAzureEntity -entityId $_.ObjectId -SubscriptionID $subscriptionId ` -PrivilegeReason $_.RoleDefinitionName -RoleId $roleId -DirectoryTenantID $TenantId -scope $aScope } elseif ($_.ObjectType -eq "Group") { $newGroupCount = 1 $firstGroup = $true $firstGroupName = $_.DisplayName $groupFromGroups = @() Do { if ($firstGroup) { try { $groupMembers = Get-AzureADGroupMember -ObjectId $_.ObjectId } catch { Write-Verbose "Error with a specific group, maybe the Group is a foreign group and can't be queried" } } else { $groupMembers = $groupFromGroups | Get-AzureADGroupMember -ObjectId $_.ObjectId } $firstGroup = $false $usersFromGroup = $groupMembers | where {$_.ObjectType -eq "User"} $usersFromGroup | foreach { #Adding Users to PrivilegeAzureEntity Dict to include Group ID Add-PrivilegeAzureEntity -entityId $_.ObjectId -SubscriptionID $subscriptionId ` -PrivilegeReason $PrivilegeReason -RoleId $roleId -DirectoryTenantID $TenantId ` -PrivilegeGroup $firstGroupName -GroupPrivilege -scope $aScope } $groupFromGroups = $groupMembers | where {$_.ObjectType -eq "Group"} $newGroupCount = $groupFromGroups.count #Add groups found in first group $ownersOfGroup = @() try { $ownersOfGroup = Get-AzureADGroupOwner -ObjectId $_.ObjectId $ownersOfGroup | foreach { if (-not $entityDict.contains($_.ObjectId)){ $AzEntityObject = Get-AzureADUser -ObjectId $_.ObjectId Add-EntityToDict -AzEntityObject $AzEntityObject } Add-PrivilegeAzureEntity -entityId $_.ObjectId -SubscriptionID $subscriptionId ` -PrivilegeReason "Privileged Group Owner" -RoleId $roleId -DirectoryTenantID $TenantId ` -PrivilegeGroup $firstGroupName -GroupPrivilege -scope $aScope } } catch { Write-Verbose "Error with a specific group, maybe the Group is a foreign group and can't be queried" } } While ($newGroupCount -ne 0) } } } } # Building the results file function Write-AzureReconInfo { param( [string] $ResultsFolder, [switch] $CloudShellMode ) $usersInfoPath = join-path -path $resultsFolder -childPath "AzureUsers-Info.csv" $directoryInfoPath = join-path -path $resultsFolder -childPath "AzureDirectory-Info.csv" $ofs = ',' $entityReconOutput = @() $entityDict.Values | foreach { $entityReconLine = [PSCustomObject][ordered] @{ UserPrincipalName = [string]$_.UserPrincipalName DisplayName = [string]$_.DisplayName ObjectType = [string]$_.ObjectType UserType = [string]$_.UserType AccountEnabled = [string]$_.AccountEnabled JobTitle = [string]$_.JobTitle Department = [string]$_.Department Mail = [string]$_.Mail Mobile = [string]$_.Mobile TelephoneNumber = [string]$_.TelephoneNumber PreferredLanguage = [string]$_.PreferredLanguage MailNickName = [string]$_.MailNickName GivenName = [string]$_.GivenName Surname = [string]$_.Surname EntityHasMailPhoto = [string]$_.EntityHasPhoto CreatedDateTime = [string]$_.ExtensionProperty.createdDateTime OnPremisesSecurityIdentifier = [string]$_.OnPremisesSecurityIdentifier DirSyncEnabled = [string]$_.DirSyncEnabled LastDirSyncTime = [string]$_.LastDirSyncTime RefreshTokensValidFromDateTime = [string]$_.RefreshTokensValidFromDateTime UsageLocation = [string]$_.UsageLocation CompanyName = [string]$_.CompanyName Country = [string]$_.Country State = [string]$_.State City = [string]$_.City StreetAddress = [string]$_.StreetAddress PostalCode = [string]$_.PostalCode PhysicalDeliveryOfficeName = [string]$_.PhysicalDeliveryOfficeName FacsimileTelephoneNumber = [string]$_.FacsimileTelephoneNumber IsCompromised = [string]$_.IsCompromised ImmutableId = [string]$_.ImmutableId CreationType = [string]$_.CreationType PasswordPolicies = [string]$_.PasswordPolicies PasswordProfile = [string]$_.PasswordProfile ShowInAddressList = [string]$_.ShowInAddressList SipProxyAddress = [string]$_.SipProxyAddress DeletionTimestamp = [string]$_.DeletionTimestamp ObjectId = [string]$_.ObjectId } $entityReconOutput += $entityReconLine } $entityReconOutput | Sort-Object UserPrincipalName | Export-Csv -path $usersInfoPath -NoTypeInformation $tenantReconOutput = @() $tenantDict.Values | foreach { $tenantReconLine = [PSCustomObject][ordered] @{ InitialDomainName = [string]$_.InitialDomainName DisplayName = [string]$_.DisplayName ObjectType = [string]$_.ObjectType DirSyncEnabled = [string]$_.DirSyncEnabled CompanyLastDirSyncTime = [string]$_.CompanyLastDirSyncTime Country = [string]$_.Country CountryLetterCode = [string]$_.CountryLetterCode PreferredLanguage = [string]$_.PreferredLanguage State = [string]$_.State City = [string]$_.City PostalCode = [string]$_.PostalCode Street = [string]$_.Street TelephoneNumber = [string]$_.TelephoneNumber MarketingNotificationEmails = [string]$_.MarketingNotificationEmails TechnicalNotificationMails = [string]$_.TechnicalNotificationMails SecurityComplianceNotificationMails = [string]$_.SecurityComplianceNotificationMails SecurityComplianceNotificationPhones = [string]$_.SecurityComplianceNotificationPhones AssignedPlans = [string]$_.AssignedPlans ProvisionedPlans = [string]$_.ProvisionedPlans ProvisioningErrors = [string]$_.ProvisioningErrors DeletionTimestamp = [string]$_.DeletionTimestamp ObjectId = [string]$_.ObjectId } $tenantReconOutput += $tenantReconLine } $tenantReconOutput | Sort-Object InitialDomainName | Export-Csv -path $directoryInfoPath -NoTypeInformation } # Output the results file function Write-AzureStealthResults { [CmdletBinding()] param( [switch] $CloudShellMode, [string] $customResultsFolder = $customOutputDirectory ) $azureAdminsResults = $privilegedAzEntitiesDict.Values | Sort-Object -Descending EntityType | Sort-Object PrivilegeType, EntityDisplayName, RoleId $azureAdminsList = $azureAdminsResults | select EntityDisplayName $azureAdminsList = $azureAdminsList.EntityDisplayName | Sort-Object | Get-Unique $numberAdmins = $azureAdminsList.count Write-Host "`n [+] Discovered $numberAdmins Azure Admins! Check them out :)" -ForegroundColor Yellow if (-not $cloudShellMode) { if($customResultsFolder){ $resultsFolder = $customResultsFolder }else{ $resultsFolder = Join-Path -path $PSScriptRoot -childPath ("Results-" + $resultsTime) $resultsFolderExists = Test-Path -Path $resultsFolder } if (-not $resultsFolderExists) { New-Item -ItemType directory -Path $resultsFolder > $null } $mainResultsPath = Join-Path -path $resultsFolder -ChildPath "AzureStealth-Results.csv" $azureAdminsResults | Export-Csv -path $mainResultsPath -NoTypeInformation Write-AzureReconInfo -ResultsFolder $resultsFolder Write-Host "`n [+] Completed the scan, the AzureStealth results should be presented in a new window" Write-Host "`n To get the results files - go to the results folder - in the following location:`n `"$resultsFolder`"" # In addition, present the results in an automated gridview using Out-GridView $resultsForGridView = @() $azureAdminsResults | foreach {$resultsForGridView += $_} $resultsForGridView | Out-GridView -Title "AzureStealth Results" } else { $cloudDriveInfo = Get-CloudDrive $localCloudShellPath = $cloudDriveInfo.MountPoint $resultsFolder = join-path -path (Join-path -path $localCloudShellPath -childpath "AzureStealth") -childpath ("Results-" + $resultsTime) $resultsFolderExists = Test-Path -Path $resultsFolder if (-not $resultsFolderExists) { New-Item -ItemType directory -Path $resultsFolder > $null } $resultCSVpath = Join-path -path $resultsFolder -childpath "AzureStealthScan-Results.csv" $azureAdminsResults | Export-Csv -path $resultCSVpath -NoTypeInformation Write-AzureReconInfo -ResultsFolder $resultsFolder -CloudShellMode $resultsZipPath = Join-path -path (Join-path -path $localCloudShellPath -childpath "AzureStealth") ` -childpath ("Results-" + $resultsTime +".zip") Compress-Archive -Path $resultsFolder -CompressionLevel Optimal -DestinationPath $resultsZipPath -Update Export-File -Path $resultsZipPath $storageName = $cloudDriveInfo.Name $fileShareName = $cloudDriveInfo.FileShareName Write-Host "`n [+] Completed the scan - the results zip file was created and available at:`n $resultsZipPath`n" Write-Host "`n [+] You can also use the Azure Portal to view the results files:" Write-Host " Go to => `"The Storage Accounts' main view`" => `"$storageName`" => `"Files view`"" Write-Host " Choose the File Share: `"$fileShareName`"" Write-Host " In this File Share:" Write-Host " Open the folders => `"AzureStealth`" and `"Results-"$resultsTime"`"`n" } } # Main function of AzureStealth scanning module function Scan-AzureAdmins { [CmdletBinding()] param( [switch] $UseCurrentCred, [switch] $GetPrivilegedUserPhotos, [switch] $ScanAllAzureRBACRoles, [switch] $ScanAllAzureScopes, [string] $ScanSubscriptionId, [string] $CustomOutputDirectory ) $CloudShellMode = $false try { $cloudShellRun = Get-CloudDrive if ($cloudShellRun){ $CloudShellMode = $true } } catch { $CloudShellMode = $false } $AzModule = $true if (-not $CloudShellMode) { $AzModule = Check-AzureModule } if ($AzModule -eq $false) { Return } if (-not $UseCurrentCred) { $AzConnection = Connect-AzureEnvironment if ($AzConnection -eq $false) { Return } $currentAzContext = Get-AzContext } else { $currentAzContext = Get-AzContext } if ($CloudShellMode) { try { Connect-AzureADservice } catch { Write-Host "Couldn't connect using the `"Connect-AzureADservice`" API call,`nThe tool will connect with `"Connect-AzureActiveDirectory `" call" $AzConnection = Connect-AzureActiveDirectory -AzContext $currentAzContext } } else { $AzConnection = Connect-AzureActiveDirectory -AzContext $currentAzContext } if ($AzConnection -eq $false) { $scanTheDirectory = $false Write-host "Couldn't connect to the target Directory, the scan will continue but there might be errors" -ForegroundColor Yellow } $privilegedAzEntitiesOutput = @() $privilegedAzEntitiesDict = @{} $entityDict = @{} $tenantDict = @{} $subscriptionDict = @{} $roleDict = @{} [string]$resultsTime = Get-Date -Format "yyyyMMdd" # Output to a result file all the information that was collected on all the AAD users if ($GetPrivilegedUserPhotos){ $fullUserReconList = $false } else { $fullUserReconList = $true } try { Write-host "`n [+] Running the scan with user: "$currentAzContext.Account $tenantList = Get-AzTenant Write-Host "`nAvailable Tenant ID/s:`n" Write-Host " "($tenantList.Id | Format-Table | Out-String) if($ScanSubscriptionId){ $subscriptionList = Get-AzSubscription -subscriptionId $ScanSubscriptionId }else{ $subscriptionList = Get-AzSubscription | select Name, Id, TenantId } if ($subscriptionList) { Write-Host "Available Subscription\s:" Write-Host ($subscriptionList | Format-Table | Out-String) -NoNewline } } catch { Write-Host "Encountered an error - check again the inserted Azure Credentials" -BackgroundColor red Write-Host "There was a problem when trying to access the target Azure Tenant\Subscription" -BackgroundColor Red Write-Host "Please try again.." Write-Host "You can also try different Azure user credentials or test the scan on a different environment" Return } $AzContextAutosave = (Get-AzContextAutosaveSetting).CacheDirectory if ($AzContextAutosave -eq "None") { Enable-AzContextAutosave } # Scan all the available tenant\s $tenantList| foreach { Write-Host " [+] Scanning tenant ID: "$_.Id Set-AzContext -Tenant $_.id > $null $usedUser = Get-AzADUser -UserPrincipalName $currentAzContext.Account Run-TenantScan -TenantId $_.id -UsedUserPrincipalName $usedUser.UserPrincipalName -UsedUserId $usedUser.Id } # Scan all the available subscription\s $subscriptionList | foreach { Write-Host "`n [+] Scanning Subscription Name: "$($_.Name)", ID: $($_.Id)" Set-AzContext -SubscriptionId $_.id > $null $paramSplat =@{} $paramSplat.add('subscriptionId',$_.id) if($ScanAllAzureRBACRoles){ $paramSplat.add('AllRoles',$true) } if($ScanAllAzureScopes){ $paramSplat.add('AllScopes',$true) } Run-SubscriptionScan @paramSplat } Write-Host "`n [+] Working on the results files" if ($CloudShellMode) { Write-AzureStealthResults -CloudShellMode } else { Write-AzureStealthResults } if ($AzContextAutosave -eq "None") { Disable-AzContextAutosave } Write-Host "`n" } # Alias function for starting the AzureStealth scan function Scan-AzureShadowAdmins { Scan-AzureAdmins }