Purpose. This case study documents the design, controlled replay, evidence review, limitations, and operationalisation of a high-signal identity persistence detection. Tenant-specific identifiers and secrets have been omitted so the document can be shared safely with other analysts.
Contents
1. Problem and threat model
Application credentials can provide non-interactive access without a user session or MFA challenge. A compromised service principal that writes a secret or certificate onto another trusted application can establish persistence under a different application identity. Resetting only the original application's credential may therefore fail to remove the planted access route.
2. Detection logic
- Source table:
AuditLogs. - Successful operation whose name starts with
Update applicationand containsCertificates and secrets management. - The initiator is an application/service principal, proven by a populated
InitiatedBy.app.servicePrincipalId. - The target resource type is
Application. - The initiating application and target application are not treated as the same application.
- Credential metadata is extracted from the
KeyDescriptionmodified property.
3. Controlled lab design
The test used two disposable Entra application registrations in an isolated development tenant:
Authenticated to Microsoft Graph using app-only credentials and initiated the credential write.
Received a temporary password credential named DET-CrossApp-Test.
The writer service principal was made an owner of the target application and granted the least-broad lab permission used for the test: Application.ReadWrite.OwnedBy. The victim application was not granted powerful Microsoft Graph permissions because impact simulation was unnecessary for validating this analytic.
4. Test procedure
- Enable telemetry. Route the Entra
AuditLogsdiagnostic category to the Log Analytics workspace connected to the test Microsoft Sentinel instance. - Prove ingestion. Generate a harmless application change and confirm that the event appears in both the Entra audit interface and the Sentinel
AuditLogstable. - Create two test applications. Use a writer application and a separate victim application so actor-to-target relationships are unambiguous.
- Establish constrained ownership. Add the writer service principal as an owner of the victim application through Microsoft Graph, because the portal owner picker exposed only users in this test.
- Use app-only authentication. Authenticate to Microsoft Graph as the writer application, not as an administrator, ensuring the resulting event is recorded under
InitiatedBy.app. - Run the positive test. Add the temporary password credential
DET-CrossApp-Testto the victim application's application object. - Run the negative test. Review known self-management events where the writer updates its own credentials and confirm they are excluded.
- Inspect raw evidence. Expand
InitiatedBy,TargetResources, andmodifiedPropertiesrather than relying only on projected display fields.
5. Observed evidence
| Test | Observed actor | Observed target | Expected | Result |
|---|---|---|---|---|
| Cross-application credential addition | TESTING-hacker-app | TESTING-victim-app | Returned by analytic | PASS |
| Application self-management | TESTING-hacker-app | TESTING-hacker-app | Suppressed | PASS |
The positive replay exposed the following credential metadata in TargetResources[0].modifiedProperties:
{
"displayName": "KeyDescription",
"oldValue": "[]",
"newValue": "[\"[KeyIdentifier=<redacted-guid>,KeyType=Password,KeyUsage=Verify,DisplayName=DET-CrossApp-Test]\"]"
}
The replay also showed that InitiatedBy.app.appId was null, while InitiatedBy.app.servicePrincipalId was populated. The target record exposed the target application's object ID. Because these are different object types, directly comparing actor service-principal object ID with target application object ID would be invalid.
6. Production candidate KQL
AuditLogs
| where OperationName startswith "Update application"
| where OperationName has "Certificates and secrets management"
| where Result =~ "success"
| extend
ActorAppName = tostring(InitiatedBy.app.displayName),
ActorSpObjectId = tostring(InitiatedBy.app.servicePrincipalId),
TargetAppName = tostring(TargetResources[0].displayName),
TargetAppObjectId = tostring(TargetResources[0].id),
TargetType = tostring(TargetResources[0].type),
ModifiedProperties = TargetResources[0].modifiedProperties
| where isnotempty(ActorSpObjectId)
| where TargetType =~ "Application"
// Suppress applications managing their own credentials.
| where tolower(trim(" ", ActorAppName))
!= tolower(trim(" ", TargetAppName))
| mv-apply Property = ModifiedProperties on
(
where tostring(Property.displayName) == "KeyDescription"
| extend
OldCredentialValue = tostring(Property.oldValue),
NewCredentialValue = tostring(Property.newValue)
)
| extend
CredentialDisplayName = extract(@"DisplayName=([^\]\\""]+)", 1, NewCredentialValue),
CredentialKeyIdentifier = extract(@"KeyIdentifier=([0-9a-fA-F-]{36})", 1, NewCredentialValue),
CredentialType = extract(@"KeyType=([^,\]\\""]+)", 1, NewCredentialValue),
CredentialUsage = extract(@"KeyUsage=([^,\]\\""]+)", 1, NewCredentialValue)
| project
TimeGenerated,
ActorAppName,
ActorSpObjectId,
TargetAppName,
TargetAppObjectId,
CredentialDisplayName,
CredentialKeyIdentifier,
CredentialType,
CredentialUsage,
OperationName,
Result,
CorrelationId,
InitiatedBy,
TargetResources
| order by TimeGenerated desc
7. Operational guidance
| Suggested rule name | Entra ID - Application Credential Added by Different Service Principal |
|---|---|
| Severity | High |
| Suggested schedule | Run every 15 minutes with a 30-minute lookback; use alert grouping or event identifiers to manage overlap. |
| MITRE ATT&CK | T1098.001 - Additional Cloud Credentials |
| Tuning key | Approved actor service-principal object ID + target application object ID relationship. |
Analyst triage sequence
- Confirm the actor and target are different application identities.
- Validate whether the actor is authorised to manage the target and whether a corresponding change record exists.
- Review
KeyDescriptionfor credential type, identifier, usage, and display name. - Assess permissions and directory roles held by both actor and target.
- Review nearby audit activity for ownership, consent, role, or API-permission changes.
- Correlate with service-principal sign-in telemetry to establish where the actor authenticated from and whether the target subsequently authenticated.
- If unauthorised, remove the planted credential, rotate affected credentials, revoke sessions/tokens where applicable, and investigate both identities.
8. Limitations and improvement path
- A malicious or coincidental matching display name could cause a false negative.
- Legitimate cross-application automation can produce true-positive-but-authorised events.
- Broadly excluding a trusted writer is dangerous because compromise of that writer would blind the detection.
- The stronger design is to maintain an identity inventory that resolves actor service-principal object IDs to their underlying application object IDs, then perform an immutable ID comparison.
- Approved automation should be tuned by the exact actor-target pair, with an owner and review date.
Engineering outcome
This exercise converted an external detection idea into a tenant-tested analytic. The work identified a Unicode operation-name pitfall, proved that actor and target IDs represented different Entra object types, validated both positive and negative cases, extracted useful credential evidence, and documented the residual weakness rather than concealing it.
Sharing note: All tenant IDs, object IDs, client IDs, certificate details, and secret values are intentionally excluded or redacted. The test application names are disposable lab labels.