diff --git a/docs/attack-techniques/azure/azure.privilege-escalation.root-user-access-administrator.md b/docs/attack-techniques/azure/azure.privilege-escalation.root-user-access-administrator.md new file mode 100755 index 000000000..ad5bae830 --- /dev/null +++ b/docs/attack-techniques/azure/azure.privilege-escalation.root-user-access-administrator.md @@ -0,0 +1,105 @@ +--- +title: Elevate to User Access Administrator at Root Scope +--- + +# Elevate to User Access Administrator at Root Scope + + + + +Platform: Azure + +## Mappings + +- MITRE ATT&CK + - Privilege Escalation + + + +## Description + + +Elevates the current principal to the User Access Administrator role at root scope (/), +by abusing the "Access management for Azure resources" capability available to Global Administrators in Entra ID. + +This technique allows a Global Administrator to gain control over all Azure subscriptions and management groups +in the tenant, enabling arbitrary role assignments across all Azure resources. + +Warm-up: None + +Detonation: + +- Call the elevateAccess REST API endpoint, which assigns the User Access Administrator role at root scope (/) to the current principal + +Revert: + +- Remove the User Access Administrator role assignment at root scope (/) +If you are getting a 403 error when attempting to revert, you may need to refresh your credentials with az login + +References: + +- https://learn.microsoft.com/en-us/azure/role-based-access-control/elevate-access-global-admin +- https://microsoft.github.io/Azure-Threat-Research-Matrix/PrivilegeEscalation/AZT402/AZT402/ +- https://www.invictus-ir.com/nieuws/the-azure-log-you-probably-didnt-know-existed +- https://permiso.io/blog/azures-apex-permissions-elevate-access-the-logs-security-teams-overlook + + +## Instructions + +```bash title="Detonate with Stratus Red Team" +stratus detonate azure.privilege-escalation.root-user-access-administrator +``` +## Detection + + +Identify when the elevateAccess action is called through either the Entra ID Audit Logs or the Azure Activity Log. + +Sample Entra ID Audit Log entry: + +``` json hl_lines="2 5" +{ + "operationName": "User has elevated their access to User Access Administrator for their Azure Resources", + "category": "AuditLogs", + "properties": { + "category": "AzureRBACRoleManagementElevateAccess", + "activityDisplayName": "User has elevated their access to User Access Administrator for their Azure Resources", + "loggedByService": "Azure RBAC (Elevated Access)", + "result": "success", + "initiatedBy": { + "user": { + "id": "00000000-0000-0000-0000-000000000000", + "userPrincipalName": "user@example.com", + "ipAddress": "1.2.3.4" + } + } + } +} + +``` + +Sample Azure Activity Log entry: + +``` json hl_lines="3 8" +{ + "authorization": { + "action": "Microsoft.Authorization/elevateAccess/action", + "scope": "/providers/Microsoft.Authorization" + }, + "caller": "user@example.com", + "operationName": { + "value": "Microsoft.Authorization/elevateAccess/action", + "localizedValue": "Assigns the caller to User Access Administrator role" + }, + "properties": { + "statusCode": "OK", + "eventCategory": "Administrative", + "entity": "/providers/Microsoft.Authorization", + "message": "Microsoft.Authorization/elevateAccess/action", + } + "status": { + "value": "Succeeded" + } +} +``` + + diff --git a/docs/attack-techniques/azure/index.md b/docs/attack-techniques/azure/index.md index 45a565425..9fc59e672 100755 --- a/docs/attack-techniques/azure/index.md +++ b/docs/attack-techniques/azure/index.md @@ -16,6 +16,11 @@ Note that some Stratus attack techniques may correspond to more than a single AT - [Create Azure VM Bastion shareable link](./azure.persistence.create-bastion-shareable-link.md) +## Privilege Escalation + + - [Elevate to User Access Administrator at Root Scope](./azure.privilege-escalation.root-user-access-administrator.md) + + ## Exfiltration - [Export Disk Through SAS URL](./azure.exfiltration.disk-export.md) diff --git a/docs/attack-techniques/list.md b/docs/attack-techniques/list.md index 5e3685595..37e3da007 100755 --- a/docs/attack-techniques/list.md +++ b/docs/attack-techniques/list.md @@ -61,6 +61,7 @@ This page contains the list of all Stratus Attack Techniques. | [Azure ransomware via Storage Account Blob deletion](./azure/azure.impact.blob-ransomware-individual-file-deletion.md) | [Azure](./azure/index.md) | Impact | | [Delete Azure resource lock](./azure/azure.impact.resource-lock.md) | [Azure](./azure/index.md) | Impact | | [Create Azure VM Bastion shareable link](./azure/azure.persistence.create-bastion-shareable-link.md) | [Azure](./azure/index.md) | Persistence | +| [Elevate to User Access Administrator at Root Scope](./azure/azure.privilege-escalation.root-user-access-administrator.md) | [Azure](./azure/index.md) | Privilege Escalation | | [Create Admin EKS Access Entry](./EKS/eks.lateral-movement.create-access-entry.md) | [EKS](./EKS/index.md) | Lateral Movement | | [Backdoor aws-auth EKS ConfigMap](./EKS/eks.persistence.backdoor-aws-auth-configmap.md) | [EKS](./EKS/index.md) | Persistence, Privilege Escalation | | [Backdoor Entra ID application through service principal](./entra-id/entra-id.persistence.backdoor-application-sp.md) | [Entra ID](./entra-id/index.md) | Persistence, Privilege Escalation | diff --git a/docs/attack-techniques/mitre-attack-coverage-matrices.md b/docs/attack-techniques/mitre-attack-coverage-matrices.md index f85359429..d1e73b39f 100644 --- a/docs/attack-techniques/mitre-attack-coverage-matrices.md +++ b/docs/attack-techniques/mitre-attack-coverage-matrices.md @@ -43,11 +43,11 @@ This provides coverage matrices of MITRE ATT&CK tactics and techniques currently

Azure

- + - - - + + +
ExecutionPersistenceExfiltrationImpact
ExecutionPersistencePrivilege EscalationExfiltrationImpact
Execute Command on Virtual Machine using Custom Script ExtensionCreate Azure VM Bastion shareable linkExport Disk Through SAS URLAzure Blob Storage ransomware through Encryption Scope using client-managed Key Vault key
Execute Commands on Virtual Machine using Run CommandExfiltrate Azure Storage via public accessAzure ransomware via Storage Account Blob deletion
Exfiltrate Azure Storage through SAS URLDelete Azure resource lock
Execute Command on Virtual Machine using Custom Script ExtensionCreate Azure VM Bastion shareable linkElevate to User Access Administrator at Root ScopeExport Disk Through SAS URLAzure Blob Storage ransomware through Encryption Scope using client-managed Key Vault key
Execute Commands on Virtual Machine using Run CommandExfiltrate Azure Storage via public accessAzure ransomware via Storage Account Blob deletion
Exfiltrate Azure Storage through SAS URLDelete Azure resource lock
diff --git a/docs/index.yaml b/docs/index.yaml index 8c19bc7ed..047df9155 100644 --- a/docs/index.yaml +++ b/docs/index.yaml @@ -746,6 +746,14 @@ Azure: - Persistence platform: Azure isIdempotent: false + Privilege Escalation: + - id: azure.privilege-escalation.root-user-access-administrator + name: Elevate to User Access Administrator at Root Scope + isSlow: false + mitreAttackTactics: + - Privilege Escalation + platform: Azure + isIdempotent: false Entra ID: Persistence: - id: entra-id.persistence.backdoor-application-sp diff --git a/v2/go.mod b/v2/go.mod index 995d3b1af..f2c4661da 100644 --- a/v2/go.mod +++ b/v2/go.mod @@ -63,6 +63,7 @@ require ( cloud.google.com/go/compute/metadata v0.6.0 // indirect cloud.google.com/go/iam v1.3.1 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect + github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v2 v2.2.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0 // indirect github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 // indirect github.com/ProtonMail/go-crypto v1.1.0-alpha.2 // indirect diff --git a/v2/go.sum b/v2/go.sum index 0b9ae35a3..0b3794ab5 100644 --- a/v2/go.sum +++ b/v2/go.sum @@ -23,6 +23,8 @@ github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+ github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8= github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA= github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v2 v2.2.0 h1:Hp+EScFOu9HeCbeW8WU2yQPJd4gGwhMgKxWe+G6jNzw= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v2 v2.2.0/go.mod h1:/pz8dyNQe+Ey3yBp/XuYz7oqX8YDNWVpPB0hH3XWfbc= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v4 v4.2.1 h1:UPeCRD+XY7QlaGQte2EVI2iOcWvUYA2XY8w5T/8v0NQ= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v4 v4.2.1/go.mod h1:oGV6NlB0cvi1ZbYRR2UN44QHxWFyGk+iylgD0qaMXjA= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal v1.1.2 h1:mLY+pNLjCUeKhgnAJWAKhEUQM+RJQo2H1fuGSw1Ky1E= diff --git a/v2/internal/attacktechniques/azure/privilege-escalation/root-user-access-administrator/main.go b/v2/internal/attacktechniques/azure/privilege-escalation/root-user-access-administrator/main.go new file mode 100644 index 000000000..9fcabc194 --- /dev/null +++ b/v2/internal/attacktechniques/azure/privilege-escalation/root-user-access-administrator/main.go @@ -0,0 +1,224 @@ +package azure + +import ( + "context" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "log" + "strings" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v2" + "github.com/datadog/stratus-red-team/v2/pkg/stratus" + "github.com/datadog/stratus-red-team/v2/pkg/stratus/mitreattack" +) + +// Built-in User Access Administrator role definition ID (constant across all tenants) +const userAccessAdminRoleDefinitionID = "18d7d88d-d35e-4fb5-a5c3-7773c20a72d9" + +func init() { + const codeBlock = "```" + stratus.GetRegistry().RegisterAttackTechnique(&stratus.AttackTechnique{ + ID: "azure.privilege-escalation.root-user-access-administrator", + FriendlyName: "Elevate to User Access Administrator at Root Scope", + Platform: stratus.Azure, + MitreAttackTactics: []mitreattack.Tactic{ + mitreattack.PrivilegeEscalation, + }, + Description: ` +Elevates the current principal to the User Access Administrator role at root scope (/), +by abusing the "Access management for Azure resources" capability available to Global Administrators in Entra ID. + +This technique allows a Global Administrator to gain control over all Azure subscriptions and management groups +in the tenant, enabling arbitrary role assignments across all Azure resources. + +Warm-up: None + +Detonation: + +- Call the elevateAccess REST API endpoint, which assigns the User Access Administrator role at root scope (/) to the current principal + +Revert: + +- Remove the User Access Administrator role assignment at root scope (/) +If you are getting a 403 error when attempting to revert, you may need to refresh your credentials with az login + +References: + +- https://learn.microsoft.com/en-us/azure/role-based-access-control/elevate-access-global-admin +- https://microsoft.github.io/Azure-Threat-Research-Matrix/PrivilegeEscalation/AZT402/AZT402/ +- https://www.invictus-ir.com/nieuws/the-azure-log-you-probably-didnt-know-existed +- https://permiso.io/blog/azures-apex-permissions-elevate-access-the-logs-security-teams-overlook +`, + Detection: ` +Identify when the elevateAccess action is called through either the Entra ID Audit Logs or the Azure Activity Log. + +Sample Entra ID Audit Log entry: + +` + codeBlock + ` json hl_lines="2 5" +{ + "operationName": "User has elevated their access to User Access Administrator for their Azure Resources", + "category": "AuditLogs", + "properties": { + "category": "AzureRBACRoleManagementElevateAccess", + "activityDisplayName": "User has elevated their access to User Access Administrator for their Azure Resources", + "loggedByService": "Azure RBAC (Elevated Access)", + "result": "success", + "initiatedBy": { + "user": { + "id": "00000000-0000-0000-0000-000000000000", + "userPrincipalName": "user@example.com", + "ipAddress": "1.2.3.4" + } + } + } +} + +` + codeBlock + ` + +Sample Azure Activity Log entry: + +` + codeBlock + ` json hl_lines="3 8" +{ + "authorization": { + "action": "Microsoft.Authorization/elevateAccess/action", + "scope": "/providers/Microsoft.Authorization" + }, + "caller": "user@example.com", + "operationName": { + "value": "Microsoft.Authorization/elevateAccess/action", + "localizedValue": "Assigns the caller to User Access Administrator role" + }, + "properties": { + "statusCode": "OK", + "eventCategory": "Administrative", + "entity": "/providers/Microsoft.Authorization", + "message": "Microsoft.Authorization/elevateAccess/action", + } + "status": { + "value": "Succeeded" + } +} +` + codeBlock + ` +`, + IsIdempotent: false, + Detonate: detonate, + Revert: revert, + }) +} + +func detonate(_ map[string]string, providers stratus.CloudProviders) error { + ctx := context.Background() + cred := providers.Azure().GetCredentials() + clientOptions := providers.Azure().ClientOptions + + globalAdminClient, err := armauthorization.NewGlobalAdministratorClient(cred, clientOptions) + if err != nil { + return fmt.Errorf("failed to create global administrator client: %w", err) + } + + log.Println("Elevating access to User Access Administrator at root scope (/)") + _, err = globalAdminClient.ElevateAccess(ctx, nil) + if err != nil { + return fmt.Errorf("failed to call elevateAccess: %w", err) + } + + log.Println("Successfully elevated access: User Access Administrator role assigned at root scope (/)") + return nil +} + +func revert(_ map[string]string, providers stratus.CloudProviders) error { + ctx := context.Background() + cred := providers.Azure().GetCredentials() + clientOptions := providers.Azure().ClientOptions + + // Get a management token to extract the current principal's object ID + token, err := cred.GetToken(ctx, policy.TokenRequestOptions{ + Scopes: []string{"https://management.azure.com/.default"}, + }) + if err != nil { + return fmt.Errorf("failed to get access token: %w", err) + } + principalID, err := getOIDFromToken(token.Token) + if err != nil { + return fmt.Errorf("failed to extract principal object ID from access token: %w", err) + } + log.Println("Current principal object ID: " + principalID) + + roleAssignmentsClient, err := armauthorization.NewRoleAssignmentsClient("", cred, clientOptions) + if err != nil { + return fmt.Errorf("failed to create role assignments client: %w", err) + } + + // List all role assignments at root scope and filter by principal ID and role definition ID + log.Println("Listing role assignments at root scope (/) for principal " + principalID) + pager := roleAssignmentsClient.NewListForScopePager("/", &armauthorization.RoleAssignmentsClientListForScopeOptions{ + Filter: to.Ptr("atScope()"), + }) + + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + return fmt.Errorf("failed to list role assignments at root scope: %w", err) + } + for _, ra := range page.Value { + if *ra.Properties.PrincipalID != principalID { + continue + } + if !strings.HasSuffix(*ra.Properties.RoleDefinitionID, userAccessAdminRoleDefinitionID) { + continue + } + log.Println("Found User Access Administrator role assignment at root scope, deleting it") + _, err := roleAssignmentsClient.DeleteByID(ctx, *ra.ID, nil) + if err != nil { + var respErr *azcore.ResponseError + if errors.As(err, &respErr) && respErr.StatusCode == 403 { + return fmt.Errorf("failed to delete role assignment - got 403 Forbidden. Your token may no longer reflect the elevated role assignment. Run 'az login' to refresh your credentials and try again: %w", err) + } + return fmt.Errorf("failed to delete role assignment %s: %w", *ra.ID, err) + } + log.Println("Successfully removed User Access Administrator role assignment at root scope") + return nil + } + } + + return fmt.Errorf("user access administrator role assignment at root scope not found") +} + +func getOIDFromToken(tokenString string) (string, error) { + parts := strings.Split(tokenString, ".") + if len(parts) != 3 { + return "", fmt.Errorf("invalid JWT token format: expected 3 parts, got %d", len(parts)) + } + + // JWT uses base64url encoding without padding - add padding as needed + payload := parts[1] + switch len(payload) % 4 { + case 2: + payload += "==" + case 3: + payload += "=" + } + + decoded, err := base64.URLEncoding.DecodeString(payload) + if err != nil { + return "", fmt.Errorf("failed to decode JWT payload: %w", err) + } + + var claims struct { + OID string `json:"oid"` + } + if err := json.Unmarshal(decoded, &claims); err != nil { + return "", fmt.Errorf("failed to parse JWT claims: %w", err) + } + + if claims.OID == "" { + return "", fmt.Errorf("oid claim not found in JWT token") + } + + return claims.OID, nil +} diff --git a/v2/internal/attacktechniques/main.go b/v2/internal/attacktechniques/main.go index 66134db43..68ecdf430 100644 --- a/v2/internal/attacktechniques/main.go +++ b/v2/internal/attacktechniques/main.go @@ -53,6 +53,7 @@ import ( _ "github.com/datadog/stratus-red-team/v2/internal/attacktechniques/azure/impact/blob-ransomware-individual-file-deletion" _ "github.com/datadog/stratus-red-team/v2/internal/attacktechniques/azure/impact/resource-lock" _ "github.com/datadog/stratus-red-team/v2/internal/attacktechniques/azure/persistence/create-bastion-shareable-link" + _ "github.com/datadog/stratus-red-team/v2/internal/attacktechniques/azure/privilege-escalation/root-user-access-administrator" _ "github.com/datadog/stratus-red-team/v2/internal/attacktechniques/eks/lateral-movement/create-access-entry" _ "github.com/datadog/stratus-red-team/v2/internal/attacktechniques/eks/persistence/backdoor-aws-auth-configmap" _ "github.com/datadog/stratus-red-team/v2/internal/attacktechniques/entra-id/persistence/backdoor-application"