Adversaries may be embedding malicious URLs from unknown domains into applications to exfiltrate data or execute code. SOC teams should proactively hunt for this behavior in Azure Sentinel to identify potential command and control channels or data exfiltration attempts.
KQL Query
let domains =
SigninLogs
| where ResultType == 0
| extend domain = split(UserPrincipalName, "@")[1]
| extend domain = tostring(split(UserPrincipalName, "@")[1])
| summarize by tolower(tostring(domain));
AuditLogs
| where Category =~ "ApplicationManagement"
| where Result =~ "success"
| where OperationName =~ 'Update Application'
| mv-expand TargetResources
| mv-expand TargetResources.modifiedProperties
| where TargetResources_modifiedProperties.displayName =~ "AppAddress"
| extend Key = tostring(TargetResources_modifiedProperties.displayName)
| extend NewValue = TargetResources_modifiedProperties.newValue
| extend OldValue = TargetResources_modifiedProperties.oldValue
| where isnotempty(Key) and isnotempty(NewValue)
| project-reorder Key, NewValue, OldValue
| extend NewUrls = extract_all('"Address":([^,]*)', tostring(NewValue))
| extend OldUrls = extract_all('"Address":([^,]*)', tostring(OldValue))
| extend AddedUrls = set_difference(NewUrls, OldUrls)
| where array_length(AddedUrls) > 0
| extend UserAgent = iif(tostring(AdditionalDetails[0].key) == "User-Agent", tostring(AdditionalDetails[0].value), "")
| extend InitiatingAppName = tostring(InitiatedBy.app.displayName)
| extend InitiatingAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId)
| extend InitiatingUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
| extend InitiatingAadUserId = tostring(InitiatedBy.user.id)
| extend InitiatingIPAddress = tostring(InitiatedBy.user.ipAddress)
| extend InitiatedBy = tostring(iff(isnotempty(InitiatingUserPrincipalName),InitiatingUserPrincipalName, InitiatingAppName))
| extend AppDisplayName = tostring(TargetResources.displayName)
| where isnotempty(AddedUrls)
| mv-expand AddedUrls
| extend AddedUrls = trim(@'"', tostring(AddedUrls))
| extend Domain = extract("^(?:https?:\\/\\/)?(?:[^@\\/\\n]+@)?(?:www\\.)?([^:\\/?\\n]+)/", 1, replace_string(tolower(AddedUrls), '"', ""))
| where isnotempty(Domain)
| extend Domain = strcat(split(Domain, ".")[-2], ".", split(Domain, ".")[-1])
| where Domain !in (domains)
| project-reorder TimeGenerated, AppDisplayName, AddedUrls, InitiatedBy, UserAgent, InitiatingIPAddress
| extend InitiatingAccountName = tostring(split(InitiatingUserPrincipalName, "@")[0]), InitiatingAccountUPNSuffix = tostring(split(InitiatingUserPrincipalName, "@")[1])
id: 017e095a-94d8-430c-a047-e51a11fb737b
name: URL Added to Application from Unknown Domain
description: |
'Detects a URL being added to an application where the domain is not one that is associated with the tenant.
The query uses domains seen in sign in logs to determine if the domain is associated with the tenant.
Applications associated with URLs not controlled by the organization can pose a security risk.
Ref: https://learn.microsoft.com/en-gb/entra/architecture/security-operations-applications#application-configuration-changes'
severity: High
requiredDataConnectors:
- connectorId: AzureActiveDirectory
dataTypes:
- AuditLogs
queryFrequency: 2h
queryPeriod: 2h
triggerOperator: gt
triggerThreshold: 0
tactics:
- Persistence
- PrivilegeEscalation
relevantTechniques:
- T1078.004
tags:
- AADSecOpsGuide
query: |
let domains =
SigninLogs
| where ResultType == 0
| extend domain = split(UserPrincipalName, "@")[1]
| extend domain = tostring(split(UserPrincipalName, "@")[1])
| summarize by tolower(tostring(domain));
AuditLogs
| where Category =~ "ApplicationManagement"
| where Result =~ "success"
| where OperationName =~ 'Update Application'
| mv-expand TargetResources
| mv-expand TargetResources.modifiedProperties
| where TargetResources_modifiedProperties.displayName =~ "AppAddress"
| extend Key = tostring(TargetResources_modifiedProperties.displayName)
| extend NewValue = TargetResources_modifiedProperties.newValue
| extend OldValue = TargetResources_modifiedProperties.oldValue
| where isnotempty(Key) and isnotempty(NewValue)
| project-reorder Key, NewValue, OldValue
| extend NewUrls = extract_all('"Address":([^,]*)', tostring(NewValue))
| extend OldUrls = extract_all('"Address":([^,]*)', tostring(OldValue))
| extend AddedUrls = set_difference(NewUrls, OldUrls)
| where array_length(AddedUrls) > 0
| extend UserAgent = iif(tostring(AdditionalDetails[0].key) == "User-Agent", tostring(AdditionalDetails[0].value), "")
| extend InitiatingAppName = tostring(InitiatedBy.app.displayName)
| extend InitiatingAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId)
| extend InitiatingUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
| extend InitiatingAadUserId = tostring(InitiatedBy.user.id)
| extend InitiatingIPAddress = tostring(InitiatedBy.user.ipAddress)
| extend InitiatedBy = tostring(iff(isnotempty(InitiatingUserPrincipalName),InitiatingUserPrincipalName, InitiatingAppName))
| extend AppDisplayName = tostring(TargetResources.displayName)
| where isnotempty(AddedUrls)
| mv-expand AddedUrls
| extend AddedUrls = trim(@'"', tostring(AddedUrls))
| extend Domain = extract("^(?:https?:\\/\\/)?(?:[^@\\/\\n]+@)?(?:www\\.)?([^:\\/?\\n]+)/", 1, replace_string(tolower(AddedUrls), '"', ""))
| where isnotempty(Domain)
| extend Domain = strcat(split(Domain, ".")[-2], ".",
| Sentinel Table | Notes |
|---|---|
AuditLogs | Ensure this data connector is enabled |
SigninLogs | Ensure this data connector is enabled |
Scenario: Scheduled Job Fetching External API Data
Description: A scheduled job in a tool like Power Automate or Azure Logic Apps is configured to fetch data from a third-party API that uses a domain not associated with the tenant.
Filter/Exclusion: Exclude URLs that match known external API endpoints used by the organization (e.g., api.example.com), or use a custom field to tag URLs from trusted external services.
Scenario: User Accessing a Public Documentation Site
Description: A user is accessing a public documentation site (e.g., GitHub Pages, Confluence, or Atlassian Documentation) via a browser or a tool like Postman for reference.
Filter/Exclusion: Exclude domains that are known public documentation sites, or use a field like url_category to filter out “documentation” or “public” categories.
Scenario: Admin Task to Update External Configuration
Description: An admin is manually updating a configuration file or a tool like Azure DevOps or Jenkins that references an external domain for integration purposes.
Filter/Exclusion: Exclude URLs that are part of known admin tasks or configuration updates, or use a field like user_role to filter out admin actions.
Scenario: Integration with a Third-Party SaaS Tool
Description: A tool like Salesforce, Zendesk, or HubSpot is integrated with another SaaS application, and the integration requires accessing a domain that is not part of the tenant’s domain.
Filter/Exclusion: Exclude domains that are known to be part of third-party integrations, or use a field like integration_name to identify legitimate integration traffic.
Scenario: User Accessing a Legitimate External Service via Bookmark
Description: A user clicks on a bookmark or