T1671

Cloud Application Integration

Persistence Last updated:

This detection identifies adversaries achieving persistence in SaaS environments by abusing OAuth application integrations. Attackers register malicious applications, hijack existing integrations, or consent to adversary-controlled apps from high-privileged accounts to maintain access even after account compromise or password resets. Detection focuses on anomalous OAuth consent grants, new application registrations, service principal creation, and permission escalation events in Microsoft 365, Azure AD/Entra ID, and Google Workspace environments. Particular attention is paid to admin consent grants for high-privilege scopes, application registrations from non-admin users, and OAuth grants that occur outside normal business workflows.

What is T1671 Cloud Application Integration?

Cloud Application Integration (T1671) maps to the Persistence tactic — the adversary is trying to maintain their foothold in MITRE ATT&CK.

This page provides production-ready detection logic for Cloud Application Integration, covering the data sources and telemetry it touches: Microsoft Entra ID, Microsoft Sentinel, Azure AD Audit Logs. The queries below are rated high severity at high confidence, and ship for 7 SIEM platforms — KQL, SPL, Elastic, QRadar, Sumo, YARA-L, LogScale.

MITRE ATT&CK

Tactic
Persistence
Technique
T1671 Cloud Application Integration
Canonical reference
https://attack.mitre.org/techniques/T1671/
Microsoft Sentinel / Defender
kusto
let SuspiciousPermissions = dynamic(["Mail.Read", "Mail.ReadWrite", "Files.Read.All", "Files.ReadWrite.All", "User.Read.All", "Directory.Read.All", "Directory.ReadWrite.All", "RoleManagement.ReadWrite.Directory", "Application.ReadWrite.All", "full_access_as_app"]);
let LookbackPeriod = 30d;
AuditLogs
| where TimeGenerated > ago(LookbackPeriod)
| where OperationName in (
    "Consent to application",
    "Add application",
    "Add service principal",
    "Add OAuth2PermissionGrant",
    "Add delegated permission grant",
    "Update application",
    "Add app role assignment to service principal",
    "Add app role assignment grant to user"
  )
| extend InitiatedByUser = tostring(InitiatedBy.user.userPrincipalName)
| extend InitiatedByApp = tostring(InitiatedBy.app.displayName)
| extend InitiatedByIPAddress = tostring(InitiatedBy.user.ipAddress)
| extend TargetAppName = tostring(TargetResources[0].displayName)
| extend TargetAppId = tostring(TargetResources[0].id)
| extend TargetAppType = tostring(TargetResources[0].type)
| extend ModifiedProperties = TargetResources[0].modifiedProperties
| mv-expand ModifiedProperties
| extend PropName = tostring(ModifiedProperties.displayName)
| extend PropNewValue = tostring(ModifiedProperties.newValue)
| where PropName in ("ConsentType", "Permissions", "DelegatedPermissionGrant.Scope", "AppRoles") or OperationName in ("Add application", "Add service principal")
| extend IsAdminConsent = iff(PropName == "ConsentType" and PropNewValue has "AllPrincipals", true, false)
| extend HasSuspiciousPermission = iff(PropNewValue has_any (SuspiciousPermissions), true, false)
| where IsAdminConsent == true or HasSuspiciousPermission == true or OperationName in ("Add application", "Add service principal")
| summarize
    EventCount = count(),
    Operations = make_set(OperationName),
    GrantedPermissions = make_set(PropNewValue),
    FirstSeen = min(TimeGenerated),
    LastSeen = max(TimeGenerated)
  by InitiatedByUser, InitiatedByApp, InitiatedByIPAddress, TargetAppName, TargetAppId, IsAdminConsent, HasSuspiciousPermission
| extend RiskScore = case(
    IsAdminConsent == true and HasSuspiciousPermission == true, "Critical",
    IsAdminConsent == true, "High",
    HasSuspiciousPermission == true, "High",
    "Medium"
  )
| project FirstSeen, LastSeen, InitiatedByUser, InitiatedByIPAddress, TargetAppName, TargetAppId, Operations, GrantedPermissions, IsAdminConsent, HasSuspiciousPermission, RiskScore, EventCount
| order by FirstSeen desc

Detects suspicious OAuth application consent grants and registrations in Azure AD/Entra ID by monitoring AuditLogs for admin consent events, new application registrations, service principal creation, and permission grants involving high-privilege scopes such as Mail.Read, Directory.ReadWrite.All, and Application.ReadWrite.All. Results are scored by risk based on consent type and permissions granted.

high severity high confidence

Data Sources

Microsoft Entra ID Microsoft Sentinel Azure AD Audit Logs

Required Tables

AuditLogs

False Positives

  • Legitimate IT administrators deploying enterprise applications that require admin consent for business-critical permissions
  • Productivity application onboarding during organizational rollouts (e.g., deploying a new CRM, ITSM, or HR integration)
  • Third-party security vendors requiring Mail.Read or Directory.Read.All for legitimate CASB, DLP, or threat protection services
  • Developers registering applications in development tenants or sandbox environments for testing purposes
  • Microsoft-published first-party applications being re-consented after permission scope changes in product updates

Sigma rule & cross-platform mapping

The detection logic for Cloud Application Integration (T1671) above is provided in a vendor-neutral form so you can deploy it on any SIEM. The same logic is shipped here as native KQL (Microsoft Sentinel / Defender), SPL (Splunk), Elastic (Elastic Security (EQL)), QRadar (IBM QRadar (AQL)), Sumo (Sumo Logic CSE), YARA-L (Google Chronicle / SecOps), LogScale (CrowdStrike LogScale (CQL)) queries. In Sigma terms, this detection targets the following logsource:

logsource:
  product: azure

Browse the community-maintained Sigma rules for this technique:


Testing Methodology

Validate this detection against 3 adversary techniques from Atomic Red Team. Each test below lists the behaviour to exercise and the telemetry you should expect to see. Executable commands and cleanup steps are available with Pro.

  1. Test 1Register Malicious OAuth Application in Azure AD

    Expected signal: AuditLogs entries: OperationName='Add application' and OperationName='Add service principal' with the new App ID in TargetResources. OperationName='Add delegated permission grant' showing Graph API permission additions.

  2. Test 2Grant Admin Consent to Existing Application via PowerShell

    Expected signal: AuditLogs: OperationName='Add OAuth2PermissionGrant' with ConsentType='AllPrincipals' and Scope containing 'Mail.Read'. InitiatedBy will show the Global Administrator account. ResultType should be 'Success'.

  3. Test 3Add Client Secret to Existing Service Principal for Persistent API Access

    Expected signal: AuditLogs: OperationName='Add password to service principal' with the target App ID in TargetResources and the credential display name 'AtomicTest-PersistentSecret'. AADSignInLogs: Service principal sign-in using client_credentials grant type showing the App ID authenticating to Microsoft Graph.


Response Playbook

Triage

  1. Step 1: Identify the consenting user account — retrieve UPN, account age, and role memberships from Azure AD. Determine if this is a Global Administrator, Application Administrator, or Cloud Application Administrator, as these roles can perform admin consent without approval workflows.
  2. Step 2: Examine the application that received consent — look up the Application ID in Azure AD App Registrations and Enterprise Applications. Determine if the app publisher is verified (Microsoft Partner Network), unverified, or external to your tenant. Check the redirect URIs for suspicious domains.
  3. Step 3: Review the specific permissions granted — catalog all OAuth scopes and app roles. Flag any permissions beyond what the stated business purpose requires. Pay particular attention to Mail.Read, Files.ReadWrite.All, Directory.ReadWrite.All, and any permissions with '.All' suffix.
  4. Step 4: Check the timeline of events — correlate the consent event with other AuditLog events from the same user within the preceding 24 hours. Look for password resets, MFA changes, role assignments, or impossible travel (SigninLogs with unusual geolocations).
  5. Step 5: Verify if the application was used after consent — query AADSignInLogs or OfficeActivity for activity from the consented application's Service Principal ID to determine if the attacker has already leveraged the integration to access data.
  6. Step 6: Check if the consenting account was itself compromised — review SigninLogs for the consenting user for authentication anomalies (unfamiliar IP, new country, Tor exit node, impossible travel) in the 24-48 hours preceding the consent event.
  7. Step 7: Search for related service principal credential additions — query AuditLogs for 'Add password to service principal' or 'Add key to service principal' events associated with the application, which would give the attacker long-term API access.

Containment

  1. Immediately revoke the OAuth consent grant via Azure AD Portal: Enterprise Applications > [App Name] > Permissions > Revoke admin consent. Alternatively use PowerShell: Remove-AzureADOAuth2PermissionGrant -ObjectId <GrantId>.
  2. Disable the malicious application by setting its sign-in status to disabled: Azure AD > Enterprise Applications > [App Name] > Properties > Enabled for users to sign in = No.
  3. Delete the application registration if it is adversary-controlled: Azure AD > App Registrations > [App Name] > Delete. Document the App ID before deletion for forensic records.
  4. If the consenting user account is suspected to be compromised, immediately reset credentials, revoke all active sessions via 'Revoke sign-in sessions', and disable account temporarily pending investigation.
  5. Remove any service principal credentials (secrets or certificates) added by the attacker: Azure AD > App Registrations > [App] > Certificates & secrets > delete adversary-added entries.
  6. If the application accessed SharePoint, OneDrive, or Exchange data, engage the data owner and assess scope of potential data access using Microsoft Purview Content Search or eDiscovery to identify what data was accessed.
  7. Enable Conditional Access policies to block application-only access (service principal sign-ins) from the affected application if business continuity allows.

Evidence Collection

  1. Export all AuditLogs for the 72-hour window surrounding the consent event, filtered to the consenting user UPN and the target Application ID. Include OperationName, InitiatedBy, TargetResources, and CorrelationId fields.
  2. Export SigninLogs for the consenting user account for the 7 days preceding and following the event. Include IPAddress, Location, DeviceDetail, RiskLevel, and ConditionalAccessStatus fields.
  3. Retrieve the full OAuth consent grant details using Microsoft Graph: GET /servicePrincipals/{id}/oauth2PermissionGrants to document all permissions at the time of collection.
  4. Collect all OfficeActivity and AuditLogs entries where the application's Service Principal ObjectId appears as the actor to build a timeline of data access post-consent.
  5. Export the application manifest (JSON) from Azure AD App Registrations to document registered permissions, redirect URIs, and API access configurations before remediation.
  6. Retrieve credential creation events for the application's service principal: AuditLogs | where OperationName in ('Add password to service principal', 'Add key to service principal') to identify all attacker-added credentials.
  7. Preserve unified audit log records via Microsoft Purview Compliance Portal (Security & Compliance Center) as they have longer retention than AuditLogs and include user activity against the consented application.

Escalation Criteria

  • ! Escalate immediately if the consenting account holds Global Administrator, Privileged Role Administrator, or Application Administrator roles — admin consent from these accounts grants the broadest possible access.
  • ! Escalate if the application was granted permissions including RoleManagement.ReadWrite.Directory, Application.ReadWrite.All, or full_access_as_app — these permissions enable full tenant takeover.
  • ! Escalate if post-consent activity shows the application accessed executive mailboxes, HR data stores, security team resources, or sensitive SharePoint sites.
  • ! Escalate if the application's registered redirect URI resolves to an infrastructure outside the organization's known IP ranges or contains domain-squatting patterns mimicking legitimate services.
  • ! Escalate if the consenting user account shows signs of prior compromise (risky sign-ins flagged by Azure Identity Protection, password spray patterns, or impossible travel events).
  • ! Escalate if more than one user or service account was used to consent to the same adversary-controlled application — this indicates a coordinated campaign targeting multiple privileged accounts.
  • ! Escalate to legal and compliance team if the compromised application accessed data subject to regulatory requirements (HIPAA, GDPR, PCI-DSS, SOC 2) as breach notification obligations may apply.

Investigation Guide

Forensic Artifacts

  • > Azure AD Audit Logs: OperationName='Consent to application' events with full TargetResources payload containing granted permissions and ConsentType
  • > Azure AD Enterprise Applications: OAuth2PermissionGrants for the malicious service principal listing all delegated permission scopes
  • > Microsoft Graph API: Service principal credential entries (passwords and certificates) with creation timestamps identifying attacker-added credentials
  • > Office 365 Unified Audit Log: Application activity records showing which user data (mailboxes, files, calendar) was accessed post-consent
  • > Azure AD Sign-in Logs: Service principal sign-ins from the malicious application showing IP addresses, access times, and resources accessed
  • > Microsoft Purview / Compliance Center: Content search results for data accessed by the application during its active period
  • > Azure AD App Registration manifest: JSON document capturing redirect URIs, required permissions, and API access declarations at time of collection

Tuning Guidance

Start by building an allowlist of known-good enterprise applications and their App IDs using the Organizations > Enterprise Applications blade — export this as a watchlist in Sentinel. Suppress alerts for Microsoft-published applications (verified publisher) with stable permission sets. Tune the 'SuspiciousPermissions' list to match your organization's legitimate SaaS footprint — if your helpdesk tool legitimately has Mail.Read, add its App ID to an allowlist. Filter by ConsentType to focus on AllPrincipals (admin consent) events first, as these carry the highest risk. Consider implementing an approval workflow via Azure AD admin consent workflow to reduce noise — this also hardens your environment. For the hunting queries, baseline normal service principal sign-in volumes per application to identify volume anomalies more reliably than static thresholds.


Hunting Queries

Hunts for newly registered applications or service principals that appeared in the last 30 days but had no presence in the prior 180 days, identifying potential adversary-registered apps not yet flagged by consent detection.

Hunting — KQL
kql
// Hunt for applications with broad permissions that have never been seen before in the tenant
let KnownApps = AuditLogs
| where TimeGenerated between(ago(180d) .. ago(30d))
| where OperationName in ("Add application", "Add service principal")
| extend AppId = tostring(TargetResources[0].id)
| summarize by AppId;
AuditLogs
| where TimeGenerated > ago(30d)
| where OperationName in ("Add application", "Add service principal", "Add OAuth2PermissionGrant", "Consent to application")
| extend AppId = tostring(TargetResources[0].id)
| extend AppName = tostring(TargetResources[0].displayName)
| extend Actor = tostring(InitiatedBy.user.userPrincipalName)
| extend ActorIP = tostring(InitiatedBy.user.ipAddress)
| where AppId !in (KnownApps)
| project TimeGenerated, OperationName, Actor, ActorIP, AppId, AppName
| order by TimeGenerated desc
Hunting — SPL
spl
index=* sourcetype="o365:management:activity"
| search Operation IN ("Add application.", "Add service principal.", "Add OAuth2PermissionGrant.", "Consent to application")
| eval app_id = coalesce(ObjectId, 'Target{0}.ID')
| eval actor = UserId
| stats min(_time) as first_seen, count as event_count, values(Operation) as operations, values(ClientIP) as source_ips BY app_id, actor
| eval first_seen_days_ago = round((now() - first_seen) / 86400, 1)
| where first_seen_days_ago < 30
| eval first_seen_str = strftime(first_seen, "%Y-%m-%d %H:%M:%S")
| sort + first_seen
| table first_seen_str, actor, app_id, operations, event_count, source_ips, first_seen_days_ago

Hunts for service principals where credentials were added AND app roles were assigned in the same 14-day window — a pattern indicative of attacker-controlled service principal setup for persistent API access.

Hunting — KQL
kql
// Hunt for service principals with recently added credentials (secrets/certs) that also have high-privilege app roles
AuditLogs
| where TimeGenerated > ago(14d)
| where OperationName in ("Add password to service principal", "Add key credentials to service principal")
| extend Actor = tostring(InitiatedBy.user.userPrincipalName)
| extend ActorApp = tostring(InitiatedBy.app.displayName)
| extend TargetSPN = tostring(TargetResources[0].displayName)
| extend TargetSPNId = tostring(TargetResources[0].id)
| project TimeGenerated, OperationName, Actor, ActorApp, TargetSPN, TargetSPNId, CorrelationId
| join kind=inner (
    AuditLogs
    | where TimeGenerated > ago(14d)
    | where OperationName == "Add app role assignment to service principal"
    | extend SPNId = tostring(TargetResources[0].id)
    | extend RoleGranted = tostring(TargetResources[0].modifiedProperties[0].newValue)
    | project SPNId, RoleGranted, RoleGrantTime=TimeGenerated
  ) on $left.TargetSPNId == $right.SPNId
| project TimeGenerated, Actor, ActorApp, TargetSPN, TargetSPNId, RoleGranted, RoleGrantTime, CorrelationId
| order by TimeGenerated desc
Hunting — SPL
spl
index=* sourcetype="o365:management:activity"
| search Operation IN ("Add password to service principal.", "Add key credentials to service principal.", "Add app role assignment to service principal.")
| eval target_spn = coalesce(ObjectId, 'Target{0}.ID')
| eval actor = UserId
| eval op_type = case(
    match(Operation, "password|key credentials"), "credential_added",
    match(Operation, "app role assignment"), "role_assigned",
    true(), "other"
  )
| stats values(op_type) as operation_types, values(ClientIP) as ips, min(_time) as first_seen, max(_time) as last_seen, count as events BY actor, target_spn
| where mvcount(operation_types) > 1
| eval first_seen=strftime(first_seen, "%Y-%m-%d %H:%M:%S"), last_seen=strftime(last_seen, "%Y-%m-%d %H:%M:%S")
| table first_seen, last_seen, actor, target_spn, operation_types, events, ips

Hunts for service principal sign-ins to sensitive Microsoft 365 resources (Graph API, Exchange, SharePoint, Teams) occurring outside business hours — a behavioral pattern of adversary-operated OAuth integrations performing automated data collection while reducing visibility.

Hunting — KQL
kql
// Hunt for OAuth sign-ins from service principals to sensitive resources outside business hours
AADSignInLogs
| where TimeGenerated > ago(7d)
| where AppId != "00000002-0000-0000-c000-000000000000"  // Exclude AAD Graph
| where UserType == "ServicePrincipal" or isempty(UserId)
| extend Hour = datetime_part('hour', TimeGenerated)
| where Hour !between (7 .. 19)  // Outside business hours (UTC)
| extend ResourceAccessed = ResourceDisplayName
| where ResourceAccessed in ("Microsoft Graph", "Office 365 Exchange Online", "SharePoint", "Microsoft Teams")
| extend SourceIP = IPAddress
| extend RiskLevel = RiskLevelAggregated
| where ResultType == 0  // Successful sign-in
| summarize
    AccessCount = count(),
    ResourcesAccessed = make_set(ResourceAccessed),
    SourceIPs = make_set(SourceIP),
    FirstAccess = min(TimeGenerated),
    LastAccess = max(TimeGenerated)
  by AppId, AppDisplayName, RiskLevel
| where AccessCount > 5
| order by AccessCount desc
Hunting — SPL
spl
index=* sourcetype="azure:aad:signin" OR sourcetype="o365:management:activity"
| eval hour = strftime(_time, "%H")
| where (hour < "07" OR hour > "19")
| search (ResultType="0" OR Status="Success")
| eval resource = coalesce(ResourceDisplayName, Workload)
| search resource IN ("Microsoft Graph", "Office 365 Exchange Online", "SharePoint", "Microsoft Teams")
| eval app_id = coalesce(AppId, ClientAppId)
| where NOT app_id IN ("00000002-0000-0000-c000-000000000000", "00000003-0000-0000-c000-000000000000")
| stats count AS access_count, values(resource) AS resources, values(ClientIP) AS source_ips, min(_time) AS first_seen BY app_id, AppDisplayName
| where access_count > 5
| eval first_seen=strftime(first_seen, "%Y-%m-%d %H:%M:%S")
| sort - access_count
| table app_id, AppDisplayName, resources, access_count, source_ips, first_seen

Atomic Red Team Tests

Test 1 Register Malicious OAuth Application in Azure AD
linux

Simulates an adversary registering a new OAuth application in Azure AD with high-privilege Microsoft Graph permissions. Requires Azure CLI authenticated as a user with Application Developer or Application Administrator role.

Command

bash
# Step 1: Create the application registration
az ad app create --display-name 'ArgusTest-MaliciousOAuthApp' --sign-in-audience AzureADMyOrg --output json > /tmp/app_registration.json

# Step 2: Extract Application ID
APP_ID=$(cat /tmp/app_registration.json | python3 -c "import sys,json; print(json.load(sys.stdin)['appId'])")
echo "App ID: $APP_ID"

# Step 3: Add high-privilege Graph API permissions (Mail.Read, Files.Read.All)
# Microsoft Graph resource ID: 00000003-0000-0000-c000-000000000000
# Mail.ReadBasic: e1d2612f-c2bc-4599-8e7b-d874eaca1ee1
# Files.Read.All: df85f4d6-205c-4ac5-a5ea-6bf408dba283
az ad app permission add --id $APP_ID \
  --api 00000003-0000-0000-c000-000000000000 \
  --api-permissions e1d2612f-c2bc-4599-8e7b-d874eaca1ee1=Scope df85f4d6-205c-4ac5-a5ea-6bf408dba283=Scope

# Step 4: Create service principal for the application
az ad sp create --id $APP_ID

echo "Application $APP_ID registered with Mail and Files permissions"

Cleanup

bash
APP_ID=$(cat /tmp/app_registration.json | python3 -c "import sys,json; print(json.load(sys.stdin)['appId'])")
az ad app delete --id $APP_ID
rm -f /tmp/app_registration.json
echo "Malicious application removed"

Expected Telemetry

AuditLogs entries: OperationName='Add application' and OperationName='Add service principal' with the new App ID in TargetResources. OperationName='Add delegated permission grant' showing Graph API permission additions.

Expected Detection

Alert on 'Add application' event creating an unverified application with subsequent high-privilege permission grants. Risk score should be 'High' due to sensitive permission scopes requested.

Test 2 Grant Admin Consent to Existing Application via PowerShell
windows

Simulates an adversary with Global Administrator privileges granting tenant-wide admin consent to an OAuth application, bypassing per-user consent requirements and enabling access to all users' data. Requires PowerShell with Microsoft.Graph module and Global Admin credentials.

Command

powershell
# Install required module if not present
Install-Module Microsoft.Graph -Scope CurrentUser -Force -AllowClobber

# Connect to Microsoft Graph
Connect-MgGraph -Scopes 'Application.ReadWrite.All', 'DelegatedPermissionGrant.ReadWrite.All'

# Target an existing application (use the test app from Atomic Test 1, or specify another App ID)
$AppId = 'YOUR_APP_ID_HERE'  # Replace with target application ID

# Get the service principal for this app
$SP = Get-MgServicePrincipal -Filter "appId eq '$AppId'"

# Get Microsoft Graph service principal (resource)
$GraphSP = Get-MgServicePrincipal -Filter "appId eq '00000003-0000-0000-c000-000000000000'"

# Grant admin consent for Mail.Read (AllPrincipals consent)
$Params = @{
    ClientId = $SP.Id
    ConsentType = 'AllPrincipals'
    ResourceId = $GraphSP.Id
    Scope = 'Mail.Read Files.Read.All'
}
New-MgOauth2PermissionGrant -BodyParameter $Params

Write-Host "Admin consent granted for application $AppId - scope covers ALL users in tenant"

Cleanup

powershell
# Remove the OAuth2 permission grant
$AppId = 'YOUR_APP_ID_HERE'
$SP = Get-MgServicePrincipal -Filter "appId eq '$AppId'"
$Grant = Get-MgOauth2PermissionGrant -Filter "clientId eq '$($SP.Id)'"
Remove-MgOauth2PermissionGrant -OAuth2PermissionGrantId $Grant.Id
Disconnect-MgGraph
Write-Host "Admin consent revoked"

Expected Telemetry

AuditLogs: OperationName='Add OAuth2PermissionGrant' with ConsentType='AllPrincipals' and Scope containing 'Mail.Read'. InitiatedBy will show the Global Administrator account. ResultType should be 'Success'.

Expected Detection

Critical severity alert on admin consent grant (AllPrincipals ConsentType) with high-privilege Mail.Read and Files.Read.All permissions. Should trigger IsAdminConsent=true and HasSuspiciousPermission=true in the KQL detection.

Test 3 Add Client Secret to Existing Service Principal for Persistent API Access
linux

Simulates an adversary adding a new client secret to an existing application's service principal, enabling them to authenticate as that application even after the original OAuth consent flow. This models the post-compromise persistence phase where attackers add long-lived credentials to maintain access. Requires Azure CLI and Application Administrator permissions.

Command

bash
# Authenticate to Azure CLI
az login

# Get list of existing service principals (to pick a target)
az ad app list --display-name 'ArgusTest-MaliciousOAuthApp' --output table

# Add a new client secret with 1-year expiry to the target application
APP_ID='YOUR_APP_ID_HERE'  # Replace with actual App ID
END_DATE=$(date -d '+365 days' '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null || date -v +365d '+%Y-%m-%dT%H:%M:%SZ')

az ad app credential reset \
  --id $APP_ID \
  --append \
  --display-name 'AtomicTest-PersistentSecret' \
  --end-date $END_DATE \
  --output json > /tmp/app_secret.json

# Extract credentials for attacker use
TENANT_ID=$(az account show --query tenantId -o tsv)
CLIENT_SECRET=$(cat /tmp/app_secret.json | python3 -c "import sys,json; print(json.load(sys.stdin)['password'])")

echo "Persistent credential added. Attacker can now authenticate as:"
echo "  Tenant: $TENANT_ID"
echo "  App ID: $APP_ID"
echo "  Secret expires: $END_DATE"

# Test authentication using the new credential
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=$APP_ID&client_secret=$CLIENT_SECRET&scope=https://graph.microsoft.com/.default&grant_type=client_credentials" \
  | python3 -c "import sys,json; d=json.load(sys.stdin); print('Token obtained! Expires in:', d.get('expires_in', 'error'))"

Cleanup

bash
APP_ID='YOUR_APP_ID_HERE'
# List and remove the test credential by its key ID
KEY_ID=$(cat /tmp/app_secret.json | python3 -c "import sys,json; print(json.load(sys.stdin)['keyId'])")
az ad app credential delete --id $APP_ID --key-id $KEY_ID
rm -f /tmp/app_secret.json
echo "Persistent credential removed"

Expected Telemetry

AuditLogs: OperationName='Add password to service principal' with the target App ID in TargetResources and the credential display name 'AtomicTest-PersistentSecret'. AADSignInLogs: Service principal sign-in using client_credentials grant type showing the App ID authenticating to Microsoft Graph.

Expected Detection

Alert on new credential added to service principal in AuditLogs hunting query. The subsequent client_credentials token request should appear in AADSignInLogs and trigger the after-hours service principal sign-in hunting query if run outside business hours.

Related Detections