T1666

Modify Cloud Resource Hierarchy

Defense Evasion Last updated:

This detection identifies adversarial modification of cloud resource hierarchy structures in IaaS environments, including AWS Organizations and Azure Management Groups and Subscriptions. Adversaries with elevated privileges may create new AWS accounts within an organization to bypass Service Control Policies, call LeaveOrganization to sever an account from its parent organization and remove guardrails, transfer Azure subscriptions between tenants to abuse victim compute resources without generating logs on the victim tenant (subscription hijacking), or create new Azure subscriptions under compromised Global Administrator accounts. These actions enable adversaries to operate in environments with reduced policy enforcement, evade centralized detection controls, and consume cloud resources at the victim's expense.

What is T1666 Modify Cloud Resource Hierarchy?

Modify Cloud Resource Hierarchy (T1666) maps to the Defense Evasion tactic — the adversary is trying to avoid being detected in MITRE ATT&CK.

This page provides production-ready detection logic for Modify Cloud Resource Hierarchy, covering the data sources and telemetry it touches: Azure Monitor, Microsoft Entra ID (Azure AD), Microsoft Defender for Cloud. The queries below are rated critical severity at high confidence, and ship for 7 SIEM platforms — KQL, SPL, Elastic, QRadar, Sumo, YARA-L, LogScale.

MITRE ATT&CK

Tactic
Defense Evasion
Technique
T1666 Modify Cloud Resource Hierarchy
Canonical reference
https://attack.mitre.org/techniques/T1666/
Microsoft Sentinel / Defender
kusto
let AzureHierarchyOps = AzureActivity
| where TimeGenerated > ago(1d)
| where OperationNameValue in~ (
    "MICROSOFT.SUBSCRIPTION/SUBSCRIPTIONS/WRITE",
    "MICROSOFT.SUBSCRIPTION/SUBSCRIPTIONS/DELETE",
    "MICROSOFT.MANAGEMENT/MANAGEMENTGROUPS/WRITE",
    "MICROSOFT.MANAGEMENT/MANAGEMENTGROUPS/DELETE",
    "MICROSOFT.MANAGEMENT/MANAGEMENTGROUPS/SUBSCRIPTIONS/WRITE",
    "MICROSOFT.MANAGEMENT/MANAGEMENTGROUPS/SUBSCRIPTIONS/DELETE",
    "MICROSOFT.BILLING/TRANSFERS/ACCEPT",
    "MICROSOFT.BILLING/TRANSFERS/INITIATE"
)
| extend CallerClaims = parse_json(Claims)
| extend UserPrincipalName = tostring(CallerClaims["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn"])
| extend TenantId = tostring(CallerClaims["tid"])
| project
    TimeGenerated,
    Source = "AzureActivity",
    Operation = OperationNameValue,
    Status = ActivityStatus,
    CallerIpAddress,
    Caller,
    UserPrincipalName,
    SubscriptionId,
    ResourceGroup,
    ResourceId,
    TenantId;
let AzureAuditHierarchy = AuditLogs
| where TimeGenerated > ago(1d)
| where OperationName in~ (
    "Add subscription",
    "Delete subscription",
    "Update subscription",
    "Add management group",
    "Delete management group",
    "Move subscription to management group",
    "Remove subscription from management group"
)
| extend InitiatedByUser = tostring(parse_json(tostring(InitiatedBy))["user"]["userPrincipalName"])
| extend InitiatedByIP = tostring(parse_json(tostring(InitiatedBy))["user"]["ipAddress"])
| project
    TimeGenerated,
    Source = "AuditLogs",
    Operation = OperationName,
    Status = Result,
    CallerIpAddress = InitiatedByIP,
    Caller = InitiatedByUser,
    UserPrincipalName = InitiatedByUser,
    SubscriptionId = "",
    ResourceGroup = "",
    ResourceId = TargetResources,
    TenantId = TenantId;
union AzureHierarchyOps, AzureAuditHierarchy
| extend RiskScore = case(
    Operation has_any ("TRANSFERS", "Transfer"), 100,
    Operation has_any ("DELETE", "Delete"), 85,
    Operation has_any ("MANAGEMENTGROUPS/SUBSCRIPTIONS", "Remove subscription"), 80,
    Operation has_any ("SUBSCRIPTIONS/WRITE", "Add subscription"), 70,
    Operation has_any ("MANAGEMENTGROUPS/WRITE", "Add management group"), 65,
    60
)
| where RiskScore >= 65
| order by RiskScore desc, TimeGenerated desc

Detects modifications to Azure Management Group and Subscription hierarchy, including subscription creation/deletion, management group changes, subscription-to-management-group moves, and billing transfer operations indicative of subscription hijacking. Unions AzureActivity and AuditLogs to capture both ARM-level and Azure AD directory-level hierarchy changes.

critical severity high confidence

Data Sources

Azure Monitor Microsoft Entra ID (Azure AD) Microsoft Defender for Cloud

Required Tables

AzureActivity AuditLogs

False Positives

  • Legitimate cloud governance teams reorganizing subscriptions into new management groups as part of planned landing zone migrations
  • Authorized finance or billing administrators transferring pay-as-you-go subscriptions between company-owned tenants during corporate restructuring
  • DevOps teams creating new Azure subscriptions for new product environments under an approved enterprise agreement

Sigma rule & cross-platform mapping

The detection logic for Modify Cloud Resource Hierarchy (T1666) above is provided in a vendor-neutral form so you can deploy it on any SIEM. The same logic is shipped here as native KQL (Microsoft Sentinel / Defender), SPL (Splunk), Elastic (Elastic Security (EQL)), QRadar (IBM QRadar (AQL)), Sumo (Sumo Logic CSE), YARA-L (Google Chronicle / SecOps), LogScale (CrowdStrike LogScale (CQL)) queries. In Sigma terms, this detection targets the following logsource:

logsource:
  product: azure

Browse the community-maintained Sigma rules for this technique:


Testing Methodology

Validate this detection against 3 adversary techniques from Atomic Red Team. Each test below lists the behaviour to exercise and the telemetry you should expect to see. Executable commands and cleanup steps are available with Pro.

  1. Test 1AWS Organization Account Departure (LeaveOrganization)

    Expected signal: AWS CloudTrail event: eventName=LeaveOrganization, eventSource=organizations.amazonaws.com, userIdentity.accountId=<member-account-id>. No errorCode if permissions are correct.

  2. Test 2AWS Create New Organization Account

    Expected signal: AWS CloudTrail events: CreateAccount (async, requestParameters.accountName='AtomicTest-T1666') followed by CreateAccountResult with responseElements.createAccountStatus.state=SUCCEEDED.

  3. Test 3Azure Management Group Subscription Move

    Expected signal: AzureActivity records with OperationNameValue: MICROSOFT.MANAGEMENT/MANAGEMENTGROUPS/SUBSCRIPTIONS/WRITE and MICROSOFT.MANAGEMENT/MANAGEMENTGROUPS/SUBSCRIPTIONS/DELETE. Caller will be the authenticated principal's UPN.


Response Playbook

Triage

  1. Step 1: Identify the actor — retrieve the full identity of the account that performed the operation. For AWS, extract userIdentity.arn, userIdentity.type (IAM user vs. AssumedRole vs. root), and the roleSessionName if AssumedRole. For Azure, capture the UserPrincipalName, ObjectId, and TenantId from AuditLogs/AzureActivity.
  2. Step 2: Verify whether the action was pre-authorized — check your change management system (ServiceNow, Jira, etc.) for an approved ticket corresponding to this operation. Subscription moves, org account creation, and LeaveOrganization events should require CAB approval in most enterprises.
  3. Step 3: Assess timing and context — determine whether the action was performed during business hours from a known corporate IP or VPN exit node. Actions from residential IPs, Tor exit nodes, cloud provider IPs from unexpected regions, or at unusual hours (nights/weekends) are high-priority indicators.
  4. Step 4: For AWS LeaveOrganization — immediately determine which Service Control Policies were applied to the account before departure. Query AWS Organizations API or CloudTrail: `aws organizations list-policies-for-target --target-id <account-id> --filter SERVICE_CONTROL_POLICY`. Enumerate what guardrails were removed.
  5. Step 5: For Azure subscription hijacking — verify the target subscription's current tenant via Azure portal or `az account show --subscription <id>`. If the subscription has been transferred, it will no longer appear in the victim tenant's subscription list. Check billing contacts on the subscription.
  6. Step 6: Check for subsequent activity — after a hierarchy modification, look for resource creation spikes in the modified account/subscription within the next 2 hours. In AWS, query CloudTrail for EC2 RunInstances, IAM CreateUser/CreateRole, S3 CreateBucket in the affected account. In Azure, check AzureActivity for VM creation, role assignments.

Containment

  1. AWS LeaveOrganization: If the account has left the organization, immediately contact AWS Support to assist with rejoining. In parallel, log in directly to the departed account and revoke all IAM access keys and console passwords for any non-approved users. Apply a restrictive IAM permission boundary to all roles pending investigation.
  2. AWS CreateAccount: Suspend the newly created account via AWS Organizations console or CLI: `aws organizations close-account --account-id <new-account-id>`. Revoke all access keys generated in the new account.
  3. Azure subscription hijacking: If transfer is in progress, revoke it immediately by navigating to Cost Management + Billing → Transfer requests and declining/canceling. If already transferred, contact Microsoft Support (billing team) to initiate reversal — this is a documented process for subscription hijacking incidents.
  4. Revoke the compromised identity credentials: For AWS, disable the IAM user access keys or invalidate the STS session token. For Azure, revoke all refresh tokens for the Global Administrator account: `az ad user revoke-sign-in-sessions --id <objectId>` and enable the account's MFA requirement.
  5. Enable emergency SCP or Azure Policy: Apply a deny-all Service Control Policy to any AWS accounts that left and rejoined the org pending full review. In Azure, assign a restrictive Azure Policy initiative to the affected subscription locking down resource creation.

Evidence Collection

  1. AWS CloudTrail: Export the full CloudTrail event history for the actor's ARN across all regions for the 72 hours surrounding the event: `aws cloudtrail lookup-events --lookup-attributes AttributeKey=Username,AttributeValue=<username> --start-time <72h-ago> --end-time <now>`. Include management events and data events.
  2. AWS Organizations audit: Run `aws organizations describe-create-account-status --create-account-request-id <id>` and `aws organizations list-accounts` to document the state of the organization before and after the event.
  3. Azure Activity Log export: Export full AzureActivity for the actor's ObjectId for the 7 days surrounding the event. Include all subscriptions accessible to that principal. Archive to immutable storage (WORM-enabled blob container).
  4. Azure AD sign-in logs: Pull SigninLogs and AADNonInteractiveUserSignInLogs for the compromised Global Administrator account for the 30 days prior to the event — look for unfamiliar locations, new devices, suspicious app registrations, or Conditional Access policy bypass indicators.
  5. Capture current IAM/RBAC state: Document all role assignments on the affected subscription or AWS account at time of containment. Export: Azure — `az role assignment list --all --subscription <id>`; AWS — `aws iam get-account-authorization-details`.
  6. Preserve billing records: Download the cost explorer data and billing invoices for the affected AWS account or Azure subscription for the past 3 months. Compute resources consumed after the hierarchy modification may constitute evidence of resource hijacking.

Escalation Criteria

  • ! Escalate immediately to CISO and cloud security leadership if a subscription transfer (Azure) or LeaveOrganization (AWS) has already completed — these events remove the victim's ability to observe attacker activity and require executive-level engagement with cloud providers.
  • ! Escalate if the actor identity is a service principal or automated pipeline rather than a human — this indicates either a compromised CI/CD system or a planted backdoor with persistent org-level access.
  • ! Escalate if subsequent resource creation is detected in the modified account/subscription within 30 minutes of the hierarchy modification — this indicates active adversary operations using acquired infrastructure.
  • ! Escalate if the compromised account has Global Administrator (Azure) or AWS Organizations management account access — these are the highest-privilege tiers and indicate a severe breach of the cloud control plane.
  • ! Escalate if threat intelligence links the source IP to a known threat actor group (e.g., Peach Sandstorm/APT33 has been documented conducting Azure subscription hijacking).

Investigation Guide

Forensic Artifacts

  • > AWS CloudTrail management events: LeaveOrganization, CreateAccount, MoveAccount, AcceptHandshake with full request/response parameters
  • > AWS Organizations API history: ListAccounts, ListPoliciesForTarget snapshots showing SCP state before and after
  • > Azure Activity Log: Microsoft.Subscription and Microsoft.Management resource provider operation records with caller identity and timestamps
  • > Azure AD Audit Logs: Subscription and management group changes under Directory category
  • > Azure billing transfer records: Accessible via Azure portal Cost Management + Billing → Transfer requests
  • > AWS IAM credential report: Documents all access keys, console passwords, and MFA devices for the actor account
  • > Azure AD sign-in logs: Conditional Access evaluation results, device compliance state, IP geolocation for the actor's sessions

Tuning Guidance

Start by building an allowlist of known cloud governance principals (service principals used by IaC pipelines like Terraform or Bicep, specific admin UPNs) and approved source IP ranges (corporate VPN, bastion hosts). For AWS, tag legitimate organization management actions with a specific IAM tag or session tag and filter on its absence. In Azure, integrate with PIM (Privileged Identity Management) — legitimate Global Administrators should have active PIM assignments visible in AuditLogs; hierarchy changes from accounts without a corresponding PIM activation are highly suspicious. Tune the minimum risk_score threshold in the SPL query based on your organization's use of AWS Organizations — orgs that frequently restructure OUs can lower alerting fidelity on MoveAccount and DetachPolicy events. For the Azure query, suppress known subscription-creation service principals (e.g., EA enrollment automation) by UPN or ObjectId in a watchlist.


Hunting Queries

Hunts for accounts that were recently granted Global Administrator (Azure) or assumed a high-privilege role (AWS) and then immediately performed cloud hierarchy modification operations, indicating privilege abuse shortly after role elevation.

Hunting — KQL
kql
// Hunt: Detect Azure Global Admins who recently gained the role and immediately performed hierarchy operations
let NewGlobalAdmins = AuditLogs
| where TimeGenerated > ago(30d)
| where OperationName == "Add member to role"
| where TargetResources has "Global Administrator"
| extend NewAdminObjectId = tostring(parse_json(tostring(TargetResources))[0]["id"])
| extend NewAdminUPN = tostring(parse_json(tostring(TargetResources))[0]["userPrincipalName"])
| project RoleGrantTime = TimeGenerated, NewAdminObjectId, NewAdminUPN;
let HierarchyOps = AzureActivity
| where TimeGenerated > ago(30d)
| where OperationNameValue in~ (
    "MICROSOFT.SUBSCRIPTION/SUBSCRIPTIONS/WRITE",
    "MICROSOFT.MANAGEMENT/MANAGEMENTGROUPS/WRITE",
    "MICROSOFT.MANAGEMENT/MANAGEMENTGROUPS/SUBSCRIPTIONS/WRITE",
    "MICROSOFT.BILLING/TRANSFERS/ACCEPT"
)
| extend ActorClaims = parse_json(Claims)
| extend ActorUPN = tostring(ActorClaims["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn"])
| project OpTime = TimeGenerated, OperationNameValue, ActorUPN, CallerIpAddress, ActivityStatus;
NewGlobalAdmins
| join kind=inner HierarchyOps on $left.NewAdminUPN == $right.ActorUPN
| where OpTime > RoleGrantTime and OpTime < datetime_add('hour', 72, RoleGrantTime)
| project RoleGrantTime, NewAdminUPN, OpTime, OperationNameValue, CallerIpAddress, ActivityStatus
| order by RoleGrantTime desc
Hunting — SPL
spl
index=* sourcetype="aws:cloudtrail" eventName IN ("LeaveOrganization", "CreateAccount", "MoveAccount")
| eval actor_arn=coalesce('userIdentity.arn', 'userIdentity.userName')
| eval account_id='userIdentity.accountId'
| join type=left actor_arn [
    search index=* sourcetype="aws:cloudtrail" eventName="AssumeRole"
    | eval actor_arn='responseElements.assumedRoleUser.arn'
    | eval assume_time=_time
    | table actor_arn, assume_time, sourceIPAddress
]
| eval time_since_assume=round((_time - assume_time) / 60, 1)
| where time_since_assume < 60 OR isnull(time_since_assume)
| table _time, eventName, actor_arn, account_id, sourceIPAddress, time_since_assume, userAgent
| sort -_time

Hunts for principals creating multiple cloud subscriptions (Azure) or AWS accounts in a short timeframe, which may indicate adversary provisioning of parallel environments to distribute malicious workloads or evade per-subscription billing limits.

Hunting — KQL
kql
// Hunt: Detect unusual subscription creation velocity — multiple new subscriptions from same principal
AzureActivity
| where TimeGenerated > ago(90d)
| where OperationNameValue =~ "MICROSOFT.SUBSCRIPTION/SUBSCRIPTIONS/WRITE"
| where ActivityStatus =~ "Succeeded"
| extend CallerClaims = parse_json(Claims)
| extend ActorUPN = tostring(CallerClaims["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn"])
| summarize SubscriptionCount = dcount(SubscriptionId), FirstSeen = min(TimeGenerated), LastSeen = max(TimeGenerated), IPs = make_set(CallerIpAddress) by ActorUPN
| where SubscriptionCount >= 2
| extend DaySpan = datetime_diff('day', LastSeen, FirstSeen)
| extend SubsPerDay = round(toreal(SubscriptionCount) / max_of(DaySpan, 1), 2)
| order by SubscriptionCount desc
Hunting — SPL
spl
index=* sourcetype="aws:cloudtrail" eventName="CreateAccount" errorCode!="*"
| eval actor=coalesce('userIdentity.arn', 'userIdentity.userName')
| bin _time span=24h
| stats count as accounts_created, values(requestParameters.accountName) as new_account_names, values(sourceIPAddress) as source_ips by actor, _time
| where accounts_created >= 2
| sort -accounts_created

Hunts for newly created cloud subscriptions or AWS accounts where logging/diagnostics have not been configured after 24 hours, which may indicate adversary-created environments intentionally left dark to avoid detection.

Hunting — KQL
kql
// Hunt: Azure subscriptions with no diagnostic settings (logs disabled) created recently
AzureActivity
| where TimeGenerated > ago(30d)
| where OperationNameValue =~ "MICROSOFT.SUBSCRIPTION/SUBSCRIPTIONS/WRITE"
| where ActivityStatus =~ "Succeeded"
| extend NewSubId = tostring(parse_json(Properties)["subscriptionId"])
| project CreationTime = TimeGenerated, NewSubId, Caller, CallerIpAddress
| join kind=leftanti (
    AzureActivity
    | where TimeGenerated > ago(30d)
    | where OperationNameValue has "diagnosticsettings" and ActivityStatus =~ "Succeeded"
    | project SubscriptionId
) on $left.NewSubId == $right.SubscriptionId
| project CreationTime, NewSubId, Caller, CallerIpAddress
| extend AgeHours = datetime_diff('hour', now(), CreationTime)
| where AgeHours > 24
| order by CreationTime desc
Hunting — SPL
spl
index=* sourcetype="aws:cloudtrail" eventName="CreateAccount"
| eval new_account_id=coalesce('responseElements.createAccountStatus.accountId', "pending")
| eval actor=coalesce('userIdentity.arn', 'userIdentity.userName')
[| search index=* sourcetype="aws:cloudtrail" eventName IN ("PutBucketLogging", "CreateTrail", "StartLogging") 
 | eval new_account_id='userIdentity.accountId'
 | eval has_logging=1
 | table new_account_id, has_logging]
| eval has_logging=coalesce(has_logging, 0)
| where has_logging=0
| eval age_hours=round((now() - strptime(_time, "%Y-%m-%dT%H:%M:%SZ")) / 3600, 1)
| where age_hours > 24
| table _time, new_account_id, actor, sourceIPAddress, age_hours
| sort -_time

Atomic Red Team Tests

Test 1 AWS Organization Account Departure (LeaveOrganization)
linux

Simulates an adversary removing an AWS account from its parent organization to escape Service Control Policies. Requires the target test account to be a member of an AWS Organization. WARNING: This action severs organizational guardrails — only run in a dedicated test account with no production resources.

Command

bash
# Prerequisites: AWS CLI configured with credentials for the member account (not management account)
# The member account must have the OrganizationsFullAccess or specific LeaveOrganization permission
aws organizations leave-organization --region us-east-1
# Verify departure:
aws organizations describe-organization 2>&1 | grep -E 'AWSOrganizationsNotInUseException|OrganizationId'

Cleanup

bash
# Rejoin requires action from the management account:
# aws organizations invite-account-to-organization --target Id=<account-id>,Type=ACCOUNT --notes 'Test cleanup'
# Then accept from the member account:
# aws organizations accept-handshake --handshake-id <handshake-id>

Expected Telemetry

AWS CloudTrail event: eventName=LeaveOrganization, eventSource=organizations.amazonaws.com, userIdentity.accountId=<member-account-id>. No errorCode if permissions are correct.

Expected Detection

SPL query should fire with risk_score=100. Alert: 'Critical — AWS Account Left Organization'

Test 2 AWS Create New Organization Account
linux

Simulates an adversary creating a new AWS account within a controlled organization to establish a new environment without existing SCPs applied. Requires AWS Organizations management account credentials with organizations:CreateAccount permission.

Command

bash
# Run from management account with organization admin permissions
aws organizations create-account \
  --email test-atomic-$(date +%s)@example.com \
  --account-name 'AtomicTest-T1666' \
  --region us-east-1
# Check creation status:
CREATE_ID=$(aws organizations list-create-account-status --states IN_PROGRESS --query 'CreateAccountStatuses[0].Id' --output text)
aws organizations describe-create-account-status --create-account-request-id $CREATE_ID

Cleanup

bash
# Close the test account (irreversible for 90 days — ensure this is truly a test account)
# ACCOUNT_ID=$(aws organizations describe-create-account-status --create-account-request-id $CREATE_ID --query 'CreateAccountStatus.AccountId' --output text)
# aws organizations close-account --account-id $ACCOUNT_ID

Expected Telemetry

AWS CloudTrail events: CreateAccount (async, requestParameters.accountName='AtomicTest-T1666') followed by CreateAccountResult with responseElements.createAccountStatus.state=SUCCEEDED.

Expected Detection

SPL query should fire with risk_score=70. Alert: 'High — New AWS Organization Account Created'

Test 3 Azure Management Group Subscription Move
linux

Simulates an adversary moving an Azure subscription between management groups to alter policy inheritance and potentially escape Azure Policy assignments. Requires Owner or Management Group Contributor role on both source and destination management groups.

Command

bash
# Prerequisites: Azure CLI authenticated as Global Administrator or Owner
# Set variables
SUBSCRIPTION_ID=$(az account show --query id -o tsv)
SOURCE_MG='test-source-mg'
DEST_MG='test-dest-mg'

# Create test management groups if they don't exist
az account management-group create --name $SOURCE_MG --display-name 'Atomic Test Source MG'
az account management-group create --name $DEST_MG --display-name 'Atomic Test Destination MG'

# Move subscription to source MG first
az account management-group subscription add --name $SOURCE_MG --subscription $SUBSCRIPTION_ID

# Now move to destination (simulates adversary reorganization)
az account management-group subscription add --name $DEST_MG --subscription $SUBSCRIPTION_ID
az account management-group subscription remove --name $SOURCE_MG --subscription $SUBSCRIPTION_ID

Cleanup

bash
az account management-group subscription remove --name $DEST_MG --subscription $SUBSCRIPTION_ID
az account management-group delete --name $SOURCE_MG
az account management-group delete --name $DEST_MG

Expected Telemetry

AzureActivity records with OperationNameValue: MICROSOFT.MANAGEMENT/MANAGEMENTGROUPS/SUBSCRIPTIONS/WRITE and MICROSOFT.MANAGEMENT/MANAGEMENTGROUPS/SUBSCRIPTIONS/DELETE. Caller will be the authenticated principal's UPN.

Expected Detection

KQL query should return events with RiskScore=80. Alert: 'Critical — Azure Management Group Hierarchy Modified'

Related Detections