Skip to content

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:

  1. 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).
  2. 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:

  1. Enumerates existing enterprise applications with high-privilege app roles.
  2. Generates a new password credential (client secret) or uploads an RSA certificate directly to the legitimate application object.
  3. 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.1
Host: login.microsoftonline.com
Content-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/.default

The 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 (upn or sub pointing 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.

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: ApplicationManagement
  • InitiatedBy: The user identity or existing service principal that performed the injection.
  • TargetResources[0].modifiedProperties: Contains KeyDescription, StartDate, EndDate, and KeyIdentifier.

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 desc

4.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 desc

4.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, Contabo
ServicePrincipalSignInLogs
| 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 desc

5. 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:

Terminal window
# 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”
Terminal window
# 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 $rogueKeyId
Write-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”