Detection rules › Kusto

Detect changes to Connect Sync Application

Group by
CorrelationId
Author
Robbe Van den Daele
Source
github.com/HybridBrothers/Hunting-Queries-Detection-Rules

This detection flags any changes happening on the Connect Sync Application in Entra ID. Since this is a very interesting account for attackers to abuse when moving laterally between AD DS and Entra ID, any change to the account should be investigated. We try to exclude legitimate certificate renewal processes, and a new account onboarding in the detection rule.

MITRE ATT&CK coverage

References

Telemetry coverage

Rule body

// Flag everything except a renewal process and onboarding
let base = materialize (
    AuditLogs
    // Search events happening on the Sync Account
    | extend AppName = tostring(TargetResources[0].displayName)
    | where AppName startswith "ConnectSyncProvisioning_"
    // Only get cretificate or secret changes
    | where OperationName contains "Update application – Certificates and secrets management"
    // Expand the target resources and modified properties, and only use events ralted to KeyDescription
    | mv-expand TargetResources
    | extend ModifiedProperties = TargetResources.modifiedProperties
    | mv-expand ModifiedProperties
    | where ModifiedProperties.displayName == "KeyDescription"
    // Save the old and new values of the secrets on the application
    | extend OldValue = tostring(ModifiedProperties.oldValue), 
        NewValue = tostring(ModifiedProperties.newValue)
    // Save the old and new credential names in an array
    | extend OldCredentialNames = extract_all(@"DisplayName=([^\]]+)", dynamic([1]), OldValue),
        NewCredentialNames = extract_all(@"DisplayName=([^\]]+)", dynamic([1]), NewValue)
);
let newCredsAdd = (
    base
    // Flag when there are more new credentials than old ones
    | where array_length(OldCredentialNames) < array_length(NewCredentialNames)
    | project-rename NewCredAddTimeGenerated = TimeGenerated
);
let credRemove = (
    base
    // Get events where credentials are being removed
    | where array_length(OldCredentialNames) > array_length(NewCredentialNames)
    | project-rename CredRemoveTimeGenerated = TimeGenerated
);
let credRenewal = (
    // Find legitimate credential renewals
    newCredsAdd
    | join kind=leftouter credRemove on AppName
    | where CredRemoveTimeGenerated - NewCredAddTimeGenerated <= 1m
);
AuditLogs
| extend AppName = tostring(TargetResources[0].displayName)
| where AppName startswith "ConnectSyncProvisioning_"
| join kind=leftanti credRenewal on CorrelationId
// Exclude cred removes (duplicate / not interesting)
| join kind=leftanti credRemove on CorrelationId
// Exclude new deployment
| where OperationName !in ("Add service principal", "Add application")

Stages and Predicates

Let binding: base

let base = materialize (
    AuditLogs
    | extend AppName = tostring(TargetResources[0].displayName)
    | where AppName startswith "ConnectSyncProvisioning_"
    | where OperationName contains "Update application – Certificates and secrets management"
    | mv-expand TargetResources
    | extend ModifiedProperties = TargetResources.modifiedProperties
    | mv-expand ModifiedProperties
    | where ModifiedProperties.displayName == "KeyDescription"
    | extend OldValue = tostring(ModifiedProperties.oldValue), 
        NewValue = tostring(ModifiedProperties.newValue)
    | extend OldCredentialNames = extract_all(@"DisplayName=([^\]]+)", dynamic([1]), OldValue),
        NewCredentialNames = extract_all(@"DisplayName=([^\]]+)", dynamic([1]), NewValue)
);

Let binding: newCredsAdd

let newCredsAdd = (
    base
    | where array_length(OldCredentialNames) < array_length(NewCredentialNames)
    | project-rename NewCredAddTimeGenerated = TimeGenerated
);

Let binding: credRemove used in Stage 5

let credRemove = (
    base
    | where array_length(OldCredentialNames) > array_length(NewCredentialNames)
    | project-rename CredRemoveTimeGenerated = TimeGenerated
);

Let binding: credRenewal used in Stage 4

let credRenewal = (
    newCredsAdd
    | join kind=leftouter credRemove on AppName
    | where CredRemoveTimeGenerated - NewCredAddTimeGenerated <= 1m
);

Stage 1: source

AuditLogs

Stage 2: extend

| extend AppName = tostring(TargetResources[0].displayName)

Stage 3: where

| where AppName startswith "ConnectSyncProvisioning_"

Stage 4: join (negated)

| join kind=leftanti credRenewal on CorrelationId

Stage 5: join (negated)

| join kind=leftanti credRemove on CorrelationId

Stage 6: where

| where OperationName !in ("Add service principal", "Add application")

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
AppNamestarts_withConnectSyncProvisioning_excludes:AppName field:"AppName" value:"ConnectSyncProvisioning_"
OldCredentialNamescross_field_compareNewCredentialNamesexcludes:OldCredentialNames field:"OldCredentialNames" value:"NewCredentialNames"
OperationNamecontainsUpdate application – Certificates and secrets managementexcludes:OperationName field:"OperationName" value:"Update application – Certificates and secrets management"
displayNameeqKeyDescriptionexcludes:displayName field:"displayName" value:"KeyDescription"
OperationNameinAdd application, Add service principalexcludes:OperationName field:"OperationName" value:"Add application" field:"OperationName" value:"Add service principal"

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
AppNamestarts_with
  • ConnectSyncProvisioning_
field:"AppName" kind:starts_with value:"ConnectSyncProvisioning_"

Output fields

These fields are emitted when the rule matches.

FieldSource
AppNameextend