Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions hunting/azure/docs/entra_rare_actions_by_service_principal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Microsoft Entra ID Rare Service Principal Activity from Multiple IPs

---

## Metadata

- **Author:** Elastic
- **Description:** This hunting query identifies service principal activity across Microsoft Entra ID, Microsoft 365, and Graph API logs that is both rare and originates from multiple IP addresses. Adversaries may abuse service principals to persist access, move laterally, or access sensitive APIs. This hunt surfaces service principals performing unusual or infrequent actions from more than one IP, which could indicate credential misuse or stolen token replay.
- **UUID:** `91f4e8e6-7d35-45e1-89c5-8c77e78ef5c1`
- **Integration:** [azure](https://docs.elastic.co/integrations/azure), [o365](https://docs.elastic.co/integrations/o365)
- **Language:** `[ES|QL]`
- **Source File:** [Microsoft Entra ID Rare Service Principal Activity from Multiple IPs](../queries/entra_rare_actions_by_service_principal.toml)

## Query

```sql
FROM logs-azure.*, logs-o365.audit-*
| WHERE
event.dataset in ("azure.auditlogs", "azure.signinlogs", "o365.audit", "azure.graphactivitylogs")
AND (
(azure.signinlogs.properties.service_principal_name IS NOT NULL OR
azure.auditlogs.properties.initiated_by.app.servicePrincipalId IS NOT NULL OR
azure.graphactivitylogs.properties.service_principal_id IS NOT NULL) OR
`o365`.audit.ExtendedProperties.extendedAuditEventCategory == "ServicePrincipal"
)
| EVAL
service_principal_name = COALESCE(
azure.auditlogs.properties.initiated_by.app.displayName,
azure.signinlogs.properties.service_principal_name,
`o365`.audit.UserId
),
service_principal_id = COALESCE(
azure.auditlogs.properties.initiated_by.app.servicePrincipalId,
azure.graphactivitylogs.properties.service_principal_id,
`o365`.audit.UserId,
azure.signinlogs.properties.service_principal_id
),
timestamp_day_bucket = DATE_TRUNC(1 day, @timestamp)
| WHERE source.ip IS NOT NULL
// filter for unexpected service principal and IP address patterns
// OR NOT CIDR_MATCH(source.ip, "127.0.0.2/32")
| STATS
event_count = COUNT(),
ips = VALUES(source.ip),
distinct_ips = COUNT_DISTINCT(source.ip),
datasets = VALUES(event.dataset),
service_principal_ids = VALUES(service_principal_id),
event_actions = VALUES(event.action),
daily_action_count = COUNT()
BY event.action, service_principal_name
| WHERE (daily_action_count <= 5 and distinct_ips >= 2)
| SORT daily_action_count ASC
```

## Notes

- This is an ES|QL query returning results in a tabular format. Analysts should pivot from any column value (e.g., `event.action`, `service_principal_name`, `service_principal_id`, or `source.ip`) into raw event data to inspect the full scope of the activity.
- This hunt looks for service principals performing rare or low-frequency actions (≤ 5 per day) from multiple IPs (≥ 2), which could indicate replayed tokens, stolen credentials, or unusual automation.
- The `service_principal_name` field is populated using the display name or user ID, depending on the log source.
- The `service_principal_id` is used to correlate actions across datasets such as Azure Audit Logs, Sign-In Logs, M365 Audit Logs, and Graph Activity Logs.
- Check the `source.ip` field for anomalies in geolocation or ASN. If the same SP is used from geographically distant locations or via unexpected ISPs, this may indicate compromise.
- Review the `event.action` field to determine what the service principal was doing — uncommon API calls, login attempts, resource creation, or changes should be reviewed.
- Rare service principal behavior may be legitimate (e.g., new integration) but should always be validated against expected automation and deployment activity.
- This technique has been observed in attacks involving abuse of OAuth apps, Microsoft Graph API access, and stolen tokens for lateral movement or persistent access.

## MITRE ATT&CK Techniques

- [T1098.001](https://attack.mitre.org/techniques/T1098/001)

## References

- https://www.cisa.gov/news-events/alerts/2025/05/22/advisory-update-cyber-threat-activity-targeting-commvaults-saas-cloud-application-metallic

## License

- `Elastic License v2`
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Microsoft Entra ID Credentials Added to Rare Service Principal
# Microsoft Entra ID Uncommon IP Adding Credentials to Service Principal

---

Expand All @@ -9,44 +9,55 @@
- **UUID:** `d2dd0288-0a8c-11f0-b738-f661ea17fbcc`
- **Integration:** [azure](https://docs.elastic.co/integrations/azure)
- **Language:** `[ES|QL]`
- **Source File:** [Microsoft Entra ID Credentials Added to Rare Service Principal](../queries/entra_service_principal_credentials_added_to_rare_app.toml)
- **Source File:** [Microsoft Entra ID Uncommon IP Adding Credentials to Service Principal](../queries/entra_service_principal_credentials_added_to_rare_app.toml)

## Query

```sql
FROM logs-azure.auditlogs*
| WHERE @timestamp > now() - 60 day
| WHERE
// filter on Microsoft Entra Audit Logs
// filter for service principal credentials being added
event.dataset == "azure.auditlogs"
and azure.auditlogs.operation_name == "Add service principal credentials"
and event.outcome == "success"
AND azure.auditlogs.operation_name == "Add service principal credentials"
AND event.outcome == "success"
| EVAL
// SLICE n0 of requests values for specific Client App ID
// Cast Client App ID to STRING type
// Extract appId from additional_details
azure.auditlogs.properties.additional_details.appId = MV_SLICE(azure.auditlogs.properties.additional_details.value, 0)::STRING
| WHERE
// REGEX on Client APP ID for UUIDv4
// Ensure appId is UUIDv4 format
azure.auditlogs.properties.additional_details.appId RLIKE """[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}"""
// Use the below filter to limit results to credential additions associated with known service principals (e.g. Commvault)
// AND (azure.auditlogs.properties.target_resources.`0`.modified_properties.`0`.new_value LIKE "*Commvault*" OR azure.auditlogs.properties.target_resources.`0`.modified_properties.`0`.old_value LIKE "*Commvault*")
| EVAL
// BUCKET events weekly
// Bucket events by each week
timestamp_week_bucket = DATE_TRUNC(7 day, @timestamp)
| STATS
// Aggregate weekly occurrences by Client App ID, User ID
weekly_user_app_occurrence_count = COUNT_DISTINCT(timestamp_week_bucket) BY
azure.auditlogs.properties.additional_details.appId,
azure.auditlogs.properties.initiated_by.user.id
| WHERE weekly_user_app_occurrence_count == 1
operation = VALUES(azure.auditlogs.operation_name),
app_id = VALUES(azure.auditlogs.properties.additional_details.appId),
correlation_id = VALUES(azure.auditlogs.properties.correlation_id),
identity = VALUES(azure.auditlogs.properties.identity),
initiated_by_id = VALUES(azure.auditlogs.properties.initiated_by.user.id),
user_principal_name = VALUES(azure.auditlogs.properties.initiated_by.user.userPrincipalName),
tenant_id = VALUES(azure.auditlogs.properties.tenantId),
modified_properties_new = VALUES(azure.auditlogs.properties.target_resources.`0`.modified_properties.`0`.new_value),
modified_properties_old = VALUES(azure.auditlogs.properties.target_resources.`0`.modified_properties.`0`.old_value),
weekly_occurrence_count = COUNT_DISTINCT(timestamp_week_bucket)
BY source.ip, azure.auditlogs.properties.additional_details.appId
| WHERE weekly_occurrence_count <= 5
```

## Notes

- This is an ES|QL query, therefore results are returned in a tabular format. Pivot into related events using the `azure.auditlogs.properties.initiated_by.user.id`
- Review `azure.auditlogs.properties.additional_details.appId` to verify the Client App ID. This should be a known application in your environment. Check if it is an Azure-managed application, custom application, or a third-party application.
- The `azure.auditlogs.properties.additional_details.appId` value will be available in `azure.auditlogs.properties.additional_details.value` when triaging the original events.
- The `azure.auditlogs.properties.initiated_by.user.id` may be a hijacked account with elevated privileges. Review the user account to determine if it is a known administrative account or a compromised account.
- Review `azure.auditlogs.properties.target_resources.0.display_name` to verify the service principal name. This correlates directly to the `azure.auditlogs.properties.additional_details.appId` value.
- Identify potential authentication events from the service principal the credentials were added to. This may indicate that the service principal is being used to access resources in your environment.
- This is an ES|QL query returning results in a tabular format. Analysts should pivot from any column value (e.g., `app_id`, `initiated_by_id`, `source.ip`, or `correlation_id`) into raw event data to inspect the full scope of the activity.
- The operation `Add service principal credentials` indicates a credential (e.g., password or certificate) was added to a service principal. This is often legitimate but can be abused for persistence, especially if the service principal was compromised or created by a threat actor.
- Investigate the value of `azure.auditlogs.properties.additional_details.appId`. Determine whether this service principal belongs to a Microsoft-managed application, a known third-party tool like Commvault, or an unknown application.
- Review `azure.auditlogs.properties.target_resources.0.display_name` or its equivalent in the raw logs to verify the name of the service principal receiving credentials.
- Examine `modified_properties_new` and `modified_properties_old` to understand how many credentials were added. Look for suspicious patterns, such as multiple credentials added at once or display names like `Commvault`.
- Pivot on the `initiated_by_id` and `user_principal_name` to determine if the activity was expected or if the account may be compromised.
- Check the `source.ip` for geolocation, VPN/proxy usage, or unfamiliar ISP origin. Uncommon IPs for specific 3rd-party service principals may indicate adversarial activity.
- A low `weekly_occurrence_count` (e.g., 1) suggests the activity is rare for the given service principal and IP, making it worthy of further investigation.
- Review activity linked via any of the `correlation_id` values to see what actions followed credential addition. This may include sign-ins, Graph API calls, or resource access.
- Search for downstream activity from the `app_id`, such as token usage, service principal logins, or cloud resource actions that may indicate abuse or persistence.

## MITRE ATT&CK Techniques

Expand All @@ -56,6 +67,7 @@ FROM logs-azure.auditlogs*

- https://cloud.google.com/blog/topics/threat-intelligence/remediation-and-hardening-strategies-for-microsoft-365-to-defend-against-unc2452
- https://dirkjanm.io/azure-ad-privilege-escalation-application-admin/
- https://www.cisa.gov/news-events/alerts/2025/05/22/advisory-update-cyber-threat-activity-targeting-commvaults-saas-cloud-application-metallic

## License

Expand Down
63 changes: 63 additions & 0 deletions hunting/azure/queries/entra_rare_actions_by_service_principal.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
[hunt]
author = "Elastic"
description = """This hunting query identifies service principal activity across Microsoft Entra ID, Microsoft 365, and Graph API logs that is both rare and originates from multiple IP addresses. Adversaries may abuse service principals to persist access, move laterally, or access sensitive APIs. This hunt surfaces service principals performing unusual or infrequent actions from more than one IP, which could indicate credential misuse or stolen token replay."""
integration = ["azure", "o365"]
uuid = "91f4e8e6-7d35-45e1-89c5-8c77e78ef5c1"
name = "Microsoft Entra ID Rare Service Principal Activity from Multiple IPs"
language = ["ES|QL"]
license = "Elastic License v2"
notes = [
"This is an ES|QL query returning results in a tabular format. Analysts should pivot from any column value (e.g., `event.action`, `service_principal_name`, `service_principal_id`, or `source.ip`) into raw event data to inspect the full scope of the activity.",
"This hunt looks for service principals performing rare or low-frequency actions (≤ 5 per day) from multiple IPs (≥ 2), which could indicate replayed tokens, stolen credentials, or unusual automation.",
"The `service_principal_name` field is populated using the display name or user ID, depending on the log source.",
"The `service_principal_id` is used to correlate actions across datasets such as Azure Audit Logs, Sign-In Logs, M365 Audit Logs, and Graph Activity Logs.",
"Check the `source.ip` field for anomalies in geolocation or ASN. If the same SP is used from geographically distant locations or via unexpected ISPs, this may indicate compromise.",
"Review the `event.action` field to determine what the service principal was doing — uncommon API calls, login attempts, resource creation, or changes should be reviewed.",
"Rare service principal behavior may be legitimate (e.g., new integration) but should always be validated against expected automation and deployment activity.",
"This technique has been observed in attacks involving abuse of OAuth apps, Microsoft Graph API access, and stolen tokens for lateral movement or persistent access."
]
mitre = ['T1098.001']
references = [
"https://www.cisa.gov/news-events/alerts/2025/05/22/advisory-update-cyber-threat-activity-targeting-commvaults-saas-cloud-application-metallic"
]
query = [
'''
FROM logs-azure.*, logs-o365.audit-*
| WHERE @timestamp > now() - 30 day
| WHERE
event.dataset in ("azure.auditlogs", "azure.signinlogs", "o365.audit", "azure.graphactivitylogs")
AND (
(azure.signinlogs.properties.service_principal_name IS NOT NULL OR
azure.auditlogs.properties.initiated_by.app.servicePrincipalId IS NOT NULL OR
azure.graphactivitylogs.properties.service_principal_id IS NOT NULL) OR
`o365`.audit.ExtendedProperties.extendedAuditEventCategory == "ServicePrincipal"
)
| EVAL
service_principal_name = COALESCE(
azure.auditlogs.properties.initiated_by.app.displayName,
azure.signinlogs.properties.service_principal_name,
`o365`.audit.UserId
),
service_principal_id = COALESCE(
azure.auditlogs.properties.initiated_by.app.servicePrincipalId,
azure.graphactivitylogs.properties.service_principal_id,
`o365`.audit.UserId,
azure.signinlogs.properties.service_principal_id
),
timestamp_day_bucket = DATE_TRUNC(1 day, @timestamp)
| WHERE source.ip IS NOT NULL
// filter for unexpected service principal and IP address patterns
// OR NOT CIDR_MATCH(source.ip, "127.0.0.2/32")
| STATS
event_count = COUNT(),
ips = VALUES(source.ip),
distinct_ips = COUNT_DISTINCT(source.ip),
datasets = VALUES(event.dataset),
service_principal_ids = VALUES(service_principal_id),
event_actions = VALUES(event.action),
daily_action_count = COUNT()
BY event.action, service_principal_name
| WHERE (daily_action_count <= 5 and distinct_ips >= 2)
| SORT daily_action_count ASC
'''
]
Loading
Loading