Detection rules › Kusto

Subscription moved to another tenant

Severity
low
Time window
20m
Source
github.com/Azure/Azure-Sentinel

This detection uses AzureActivity logs (Security category) to identify when a subscription is moved to another tenant. A threat actor may move a subscription into their own tenant to circumvent local resource deployment and logging policies. Once moved, threat actors may deploy resources and perform malicious activities such as crypto mining. This is a technique known as "subscription hijacking". More information can be found here: https://techcommunity.microsoft.com/t5/microsoft-365-defender-blog/hunt-for-compromised-azure-subscriptions-using-microsoft/ba-p/3607121

MITRE ATT&CK coverage

TacticTechniques
ImpactT1496 Resource Hijacking

Event coverage

Rule body kusto

id: 48c026d8-7f36-4a95-9568-6f1420d66e37
kind: Scheduled
name: Subscription moved to another tenant
description: |
  'This detection uses AzureActivity logs (Security category) to identify when a subscription is moved to another tenant.
  A threat actor may move a subscription into their own tenant to circumvent local resource deployment and logging policies.
  Once moved, threat actors may deploy resources and perform malicious activities such as crypto mining.
  This is a technique known as "subscription hijacking". More information can be found here: https://techcommunity.microsoft.com/t5/microsoft-365-defender-blog/hunt-for-compromised-azure-subscriptions-using-microsoft/ba-p/3607121'
severity: Low
requiredDataConnectors:
  - connectorId: AzureActivity
    dataTypes:
      - AzureActivity
queryPeriod: 20m
queryFrequency: 5m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Impact
relevantTechniques:
  - T1496
query: |
  let queryFrequency = 5m;
  let eventCapture = "moved from tenant ([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}) to tenant ([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})";
  AzureActivity
  | where ingestion_time() > ago(queryFrequency)
  | where CategoryValue =~ "Security"
  | where OperationNameValue =~ "Microsoft.Subscription/updateTenant/action"
  | extend Properties_d = coalesce(parse_json(Properties), Properties_d)
  | where isnotempty(Properties_d)
  | extend Summary = tostring(Properties_d.message)
  | extend EventCapture = extract_all(eventCapture, Summary)
  | extend SourceTenantId = iff(isnotempty(EventCapture), EventCapture[0][0], "")
  | extend DestinationTenantId = iff(isnotempty(EventCapture), EventCapture[0][1], "")
  | extend 
      Name = split(Caller, "@", 0)[0],
      UPNSuffix = split(Caller, "@", 1)[0]
eventGroupingSettings:
  aggregationKind: SingleAlert
entityMappings:
  - entityType: AzureResource
    fieldMappings: 
      - identifier: ResourceId
        columnName: _ResourceId                 
  - entityType: Account
    fieldMappings: 
      - identifier: FullName
        columnName: Caller
      - identifier: Name
        columnName: Name
      - identifier: UPNSuffix
        columnName: UPNSuffix
customDetails:
  DestinationTenantId: DestinationTenantId
  SourceTenantId: SourceTenantId
alertDetailsOverride:
  alertDescriptionFormat: |
    The user {{Caller}} moved a subscription:
    
    {{Summary}}
    
    If this was not expected, it may indicate a subscription hijacking event.
  alertDisplayNameFormat: |
    Subscription {{SubscriptionId}} changed tenants
version: 1.0.1

Stages and Predicates

Parameters

let queryFrequency = 5m;

Let binding: eventCapture

let eventCapture = "moved from tenant ([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}) to tenant ([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})";

Stage 1: source

AzureActivity

Stage 2: where

| where ingestion_time() > ago(queryFrequency)

Stage 3: where

| where CategoryValue =~ "Security"

Stage 4: where

| where OperationNameValue =~ "Microsoft.Subscription/updateTenant/action"

Stage 5: extend

| extend Properties_d = coalesce(parse_json(Properties), Properties_d)

Stage 6: where

| where isnotempty(Properties_d)

Stage 7: extend (5 consecutive steps)

| extend Summary = tostring(Properties_d.message)
| extend EventCapture = extract_all(eventCapture, Summary)
| extend SourceTenantId = iff(isnotempty(EventCapture), EventCapture[0][0], "")
| extend DestinationTenantId = iff(isnotempty(EventCapture), EventCapture[0][1], "")
| extend 
    Name = split(Caller, "@", 0)[0],
    UPNSuffix = split(Caller, "@", 1)[0]

References eventCapture (defined above).

Indicators

Each row is a field, operator, and value that the rule matches. The corpus column counts how many other rules in the catalog look for the same combination: high numbers point to widely-used, community-vetted indicators. Blank or 1 shows that the indicator is specific to this rule.

FieldKindValues
CategoryValueeq
  • Security
OperationNameValueeq
  • Microsoft.Subscription/updateTenant/action
Properties_dis_not_null
  • (no value, null check)

Output fields

Fields the rule emits when it matches. Chronicle authors list these in the outcome block; they appear on the detection and $risk_score drives alerting. Sentinel / Defender XDR rules build them up through project / summarize / extend stages. Sentinel maps these into alert fields via entityMappings and customDetails; Defender XDR custom detections surface them as alert fields directly.

FieldSource
Properties_dextend
Summaryextend
EventCaptureextend
SourceTenantIdextend
DestinationTenantIdextend
Nameextend
UPNSuffixextend