Detect access token theft/replay for Microsoft Entra Service Principals / Workload Identities.
ID: T1528.Entra.ServicePrincipalAccessTokenReplay
Author: Nicola Suter
License: MIT License
References: Link to medium post
Tactic: Credential Access (TA0006)
Technique: Steal Application Access Token (T1528)
Attackers can steal access tokens from service principals and use them to access resources for the valid duration of the token. This is an attack vector for service principals that are used in CI/CD pipelines and are already hardened by leveraging only short lived tokens with workload identity federation.
Access tokens can be exfiltrated by adding a simple step to the CI pipeline and sending the access token to an attacker controlled server. The access token can then be used to access resources for the valid duration of the token.
Access to the device or service where the service principal is being used, such as a CI/CD pipeline running on GitHub Actions.
As access tokens are issued after the service principal has authenticated, the IP address of the token issuance is different from the IP address of the token usage. This difference can be used to detect access token theft.
Event ID | Event Name | Log Provider | ATT&CK Data Source |
---|---|---|---|
- | MicrosoftGraphActivityLogs | Entra ID | Cloud Service |
- | AADServicePrincipalSignInLogs | Entra ID | Cloud Service |
- | AzureActivity | Azure | Cloud Service |
FP Rate: Low
Source: Entra ID
Description: This detection looks at differences within the IP adresses between the access token issuance and usage.
Query:
// Hunt for differences between the token issuance and token usage of entra service principals based on the public IP address
let lookback = 30d;
union
(MicrosoftGraphActivityLogs
| where ResponseStatusCode between (100 .. 300) // only include HTTP success status codes
| extend UniqueTokenIdentifier = SignInActivityId
| extend ActivityIPAddress = IPAddress
),
(AzureActivity
| extend UniqueTokenIdentifier = tostring(Claims_d.uti)
| extend ActivityIPAddress = CallerIpAddress
)
| where ingestion_time() > ago(lookback)
| lookup kind=inner AADServicePrincipalSignInLogs on UniqueTokenIdentifier
| extend SigninInIPAddress = IPAddress1
| where SigninInIPAddress != ActivityIPAddress
| where isnotempty(SigninInIPAddress)
| project-away *1
| project
TimeGenerated,
ServicePrincipalName,
SigninInIPAddress,
ActivityIPAddress,
ServicePrincipalId,
ServicePrincipalCredentialThumbprint,
ServicePrincipalCredentialKeyId
- Only ActivityLogs with a corresponding SignIn are considered (due to inner join).
- The
AADManagedIdentitySignInLogs
do not contain theIPAddress
field, therefore onlyAADServicePrincipalSignInLogs
are considered.
False positives are unlikely but could occur in the following cases:
- Multiple public IP addresses are pooled and used by the same service principal (e.g. NAT gateways) after access token retrieval.
- The service principal passes the access token to another service principal with a different public IP address.
- When the access token is reused behind the same Public IP address, this detection will not work as it relies on the public IP.
- The detection only covers the Microsoft Graph and Azure Activity APIs, other APIs are not covered.