OAuth Consent, Malicious Enterprise Apps & Service Principals
While end-user consent phishing (examined in Fiche 23) tricks individuals into granting delegated permissions, administrative-level application manipulation represents the pinnacle of cloud persistence. By compromising or elevating to administrative roles (such as Application Administrator or Cloud Application Administrator), threat actors transition from ephemeral user sessions to autonomous, tenant-wide programmatic backdoors.
Adversaries achieve this persistence through two distinct vectors:
- Service Principal Credential Hijacking: Injecting new client secrets or asymmetric certificates into legitimate, pre-existing high-privilege enterprise applications (such as backup systems, security scanners, or CI/CD pipelines).
- Rogue Service Principal Deployment: Registering new applications endowed with tenant-wide Application Permissions (
Directory.ReadWrite.All,RoleManagement.ReadWrite.Directory,Mail.ReadWrite) authorized via tenant-wide Administrator Consent.
These programmatic backdoors operate entirely via the OAuth 2.0 Client Credentials Grant (RFC 6749 Section 4.4). They require no user interaction, do not prompt for MFA, bypass interactive Conditional Access policies, and emit telemetry exclusively to non-interactive service principal logs.
This guide details the architectural mechanics of service principal backdoors, decodes Entra audit logs and ServicePrincipalSignInLogs, and provides production-grade KQL hunting queries and PowerShell cleanup scripts.
1. Architectural Vectors: Secret Injection vs Rogue App Deployment
Section titled “1. Architectural Vectors: Secret Injection vs Rogue App Deployment”graph TD ADMIN[Adversary Obtains App Admin Privileges] --> VECTOR{Persistence Strategy}
VECTOR -->|Strategy A: Trojanize Existing App| TROJAN[Inject New Client Secret into Existing SP<br/>Target: Legitimate Backup / Monitoring / IT App<br/>Pre-existing permissions: Directory.ReadWrite.All] VECTOR -->|Strategy B: Deploy Rogue App| ROGUE[Create New Application & Service Principal<br/>New-MgApplication / New-MgServicePrincipal<br/>Grant Tenant-Wide Admin Consent]
TROJAN --> AUTH_FLOW[OAuth 2.0 Client Credentials Grant<br/>POST /token with client_id + client_secret] ROGUE --> AUTH_FLOW
AUTH_FLOW --> TOKEN_ISSUE[Microsoft STS Issues App-Only JWT Token<br/>AppId: Legitimate or Rogue GUID<br/>Roles: High-Privilege Application Scopes]
TOKEN_ISSUE --> DIRECT_ACCESS[Direct Microsoft Graph API Access<br/>No User Context / No MFA / Completely Autonomous]1.1 Strategy A: Trojanizing Existing Enterprise Applications
Section titled “1.1 Strategy A: Trojanizing Existing Enterprise Applications”This is the most stealthy cloud persistence technique available. Enterprise tenants typically host dozens of third-party enterprise applications authorized by previous IT administrators. Many of these applications possess extensive, tenant-wide application permissions.
Instead of creating a new, suspicious application that SOC alerts might flag, the attacker:
- Enumerates existing enterprise applications with high-privilege app roles.
- Generates a new password credential (client secret) or uploads an RSA certificate directly to the legitimate application object.
- Uses the newly minted secret to authenticate as that application from external infrastructure.
1.2 Strategy B: Deploying Rogue Enterprise Applications
Section titled “1.2 Strategy B: Deploying Rogue Enterprise Applications”If no suitable pre-existing application is available, the attacker creates a new application object, assigns application roles, and grants tenant-wide admin consent:
- Grants
RoleManagement.ReadWrite.Directory: Allows the application to elevate any user to Global Administrator programmatically. - Grants
Mail.ReadWrite(Application type): Allows the application to read, modify, or delete emails in every single mailbox across the entire tenant, not just a single compromised user.
2. Authentication Mechanics: The Client Credentials Flow
Section titled “2. Authentication Mechanics: The Client Credentials Flow”Programmatic service principal persistence operates via the OAuth 2.0 Client Credentials Grant:
POST /<tenant-id>/oauth2/v2.0/token HTTP/1.1Host: login.microsoftonline.comContent-Type: application/x-www-form-urlencoded
grant_type=client_credentials&client_id=11111111-2222-3333-4444-555555555555&client_secret=ATTACKER_INJECTED_SECRET_VALUE&scope=https://graph.microsoft.com/.defaultThe Resulting Application Token (JWT Payload):
Section titled “The Resulting Application Token (JWT Payload):”{ "aud": "https://graph.microsoft.com", "iss": "https://sts.windows.net/8f3b6a9c-2d1e-4b5a-9f8e-7c6b5a4d3e2f/", "appid": "11111111-2222-3333-4444-555555555555", "app_displayname": "Enterprise Cloud Backup Service", "roles": [ "Directory.ReadWrite.All", "Mail.ReadWrite", "Files.ReadWrite.All" ], "idtyp": "app", "tid": "8f3b6a9c-2d1e-4b5a-9f8e-7c6b5a4d3e2f"}Critical forensic insight:
idtyp: app: Confirms that this is an application-only token. There is no user context (upnorsubpointing to an employee is absent).roles: Contains the full array of authorized tenant-wide application permissions.- This token can be used from any IP on earth to execute arbitrary administrative actions via Microsoft Graph.
3. Telemetry & Log Artifacts in Entra ID
Section titled “3. Telemetry & Log Artifacts in Entra ID”Investigating service principal backdoors requires interrogating two distinct Entra ID log tables:
graph TD BACKDOOR[Attacker Injects Credential or Creates App] --> AUDIT[Entra ID AuditLogs Table] EXPLOIT[Attacker Authenticates Using App Secret] --> SP_SIGNIN[Entra ID ServicePrincipalSignInLogs Table]
AUDIT --> A1[Operation: Update application - Certificates and secrets management<br/>InitiatedBy: Admin UPN / IP<br/>Target: Application DisplayName & AppId] AUDIT --> A2[Operation: Add app role assignment to service principal<br/>Target: App Role Name e.g. Directory.ReadWrite.All]
SP_SIGNIN --> S1[ServicePrincipalName / AppId<br/>IPAddress: Attacker VPS / Residential Proxy<br/>AuthenticationProcessingDetails: Client Secret / Certificate]3.1 Entra Audit Log Signature: Secret Injection
Section titled “3.1 Entra Audit Log Signature: Secret Injection”When an attacker adds a client secret via PowerShell or the Azure Portal, AuditLogs captures:
OperationName:"Update application - Certificates and secrets management"OR"Add service principal credentials"Category:ApplicationManagementInitiatedBy: The user identity or existing service principal that performed the injection.TargetResources[0].modifiedProperties: ContainsKeyDescription,StartDate,EndDate, andKeyIdentifier.
4. Production KQL Hunting Queries
Section titled “4. Production KQL Hunting Queries”4.1 Detecting New Client Secrets or Certificates Added to Applications
Section titled “4.1 Detecting New Client Secrets or Certificates Added to Applications”Flag whenever a new credential is added to an application, highlighting who added it and the target application:
AuditLogs| where TimeGenerated >= ago(30d)| where OperationName in ( "Update application - Certificates and secrets management", "Add service principal credentials", "Add service principal", "Add application")| extend InitiatorUPN = tostring(InitiatedBy.user.userPrincipalName), InitiatorIP = tostring(InitiatedBy.user.ipAddress), InitiatorApp = tostring(InitiatedBy.app.displayName)| extend TargetAppName = tostring(TargetResources[0].displayName), AppId = tostring(TargetResources[0].id)| extend KeyDetails = tostring(TargetResources[0].modifiedProperties)| project TimeGenerated, OperationName, InitiatorUPN, InitiatorApp, InitiatorIP, TargetAppName, AppId, KeyDetails| sort by TimeGenerated desc4.2 Detecting High-Privilege Application Role Assignments
Section titled “4.2 Detecting High-Privilege Application Role Assignments”Identify when an application is granted critical administrative API permissions:
let HighRiskRoles = dynamic([ "Directory.ReadWrite.All", "RoleManagement.ReadWrite.Directory", "AppRoleAssignment.ReadWrite.All", "Mail.ReadWrite", "Files.ReadWrite.All", "User.ReadWrite.All"]);AuditLogs| where TimeGenerated >= ago(30d)| where OperationName == "Add app role assignment to service principal"| extend Initiator = tostring(InitiatedBy.user.userPrincipalName)| extend TargetApp = tostring(TargetResources[0].displayName)| extend ModifiedProps = TargetResources[0].modifiedProperties| mv-expand ModifiedProps| where ModifiedProps.displayName == "AppRole.Value"| extend GrantedRole = tostring(ModifiedProps.newValue)| where GrantedRole has_any (HighRiskRoles)| project TimeGenerated, OperationName, Initiator, TargetApp, GrantedRole| sort by TimeGenerated desc4.3 Hunting Anomalous ServicePrincipalSignInLogs
Section titled “4.3 Hunting Anomalous ServicePrincipalSignInLogs”Detect service principal logins originating from unfamiliar geographic locations, hosting providers, or unexpected external IP addresses:
let HostingASNs = dynamic([14061, 24940, 63949, 16276, 51167]); // DigitalOcean, Hetzner, Linode, OVH, ContaboServicePrincipalSignInLogs| where TimeGenerated >= ago(14d)| where ResultType == 0| where AutonomousSystemNumber in (HostingASNs) or Location != "US" // Adjust baseline location| project TimeGenerated, ServicePrincipalName, AppId, IPAddress, Location, AutonomousSystemNumber, ResourceDisplayName| summarize Signins = count(), Resources = make_set(ResourceDisplayName), IPs = make_set(IPAddress) by ServicePrincipalName, AppId, Location, AutonomousSystemNumber| sort by Signins desc5. Forensic PowerShell Audit & Remediation Playbook
Section titled “5. Forensic PowerShell Audit & Remediation Playbook”5.1 Identifying Recently Created Application Secrets Across Tenant
Section titled “5.1 Identifying Recently Created Application Secrets Across Tenant”Run this script to inventory all client secrets created within the past 30 days:
# Prerequisites: Microsoft.Graph.Applications module# Connect-MgGraph -Scopes "Application.Read.All","AppRoleAssignment.ReadWrite.All"
Write-Host "[*] Auditing all application secrets and certificates..." -ForegroundColor Cyan
$recentThreshold = (Get-Date).AddDays(-30)$allApps = Get-MgApplication -All$compromisedCandidates = @()
foreach ($app in $allApps) { # Check Password Credentials (Secrets) foreach ($pwd in $app.PasswordCredentials) { if ($pwd.StartDateTime -ge $recentThreshold) { $compromisedCandidates += [PSCustomObject]@{ AppDisplayName = $app.DisplayName AppId = $app.AppId ObjectId = $app.Id CredentialType = "Client Secret" KeyId = $pwd.KeyId CreatedDate = $pwd.StartDateTime ExpirationDate = $pwd.EndDateTime Hint = $pwd.Hint } } }
# Check Key Credentials (Certificates) foreach ($cert in $app.KeyCredentials) { if ($cert.StartDateTime -ge $recentThreshold) { $compromisedCandidates += [PSCustomObject]@{ AppDisplayName = $app.DisplayName AppId = $app.AppId ObjectId = $app.Id CredentialType = "Certificate" KeyId = $cert.KeyId CreatedDate = $cert.StartDateTime ExpirationDate = $cert.EndDateTime Hint = "Certificate Thumbprint: $($cert.CustomKeyIdentifier)" } } }}
if ($compromisedCandidates) { Write-Warning "[!] Found $($compromisedCandidates.Count) credentials created in the last 30 days:" $compromisedCandidates | Format-Table AppDisplayName, CredentialType, CreatedDate, ExpirationDate, KeyId} else { Write-Host "[+] No recently created application credentials found." -ForegroundColor Green}5.2 Revoking Injected Secrets & Purging Rogue Apps
Section titled “5.2 Revoking Injected Secrets & Purging Rogue Apps”# To revoke a specific rogue secret on an application:$appObjectId = "11111111-2222-3333-4444-555555555555"$rogueKeyId = "99999999-8888-7777-6666-555555555555"
Remove-MgApplicationPassword -ApplicationId $appObjectId -KeyId $rogueKeyIdWrite-Host "[+] Rogue client secret successfully revoked." -ForegroundColor Green
# To delete a rogue Service Principal completely:# Remove-MgServicePrincipal -ServicePrincipalId <ServicePrincipal-ObjectId>6. Cross-Reference & Investigation Navigation
Section titled “6. Cross-Reference & Investigation Navigation”- Previous Fiche: 28. Mail Forwarding, Transport Rules & Connector Abuse
- Next Fiche: 30. Authentication Methods Manipulation as Cloud Persistence
- Related Guides: