Deployment Scripts, Managed Identities, and When Good Features Go Bad
- infiniteloop
- Dec 19, 2024
- 6 min read
Updated: Mar 9
A while back, I was messing around and testing things out in my own Azure tenant. During one of these sessions, I stumbled upon a pretty interesting lead. After a bit of searching, I came across a blog post by Karl Fosaaen, someone I’ve always considered a mentor (thanks for all the chats, brilliant ideas, and your endless generosity in sharing knowledge). His post connected the dots for me and sparked an idea: what if this could be pushed even further?
I pinged him to pick his brain. Turns out, he’d been working on this for a while and was kind enough to share some of the ideas and attack vectors he’d come up with. Our back-and-forth conversations helped me refine my approach as I tinkered away, trying to get it all to actually work. In the end, I even got a little credit, thanks again for everything, Karl! I've learned a lot from it, Much appreciated.

In addition to Karl's blogpost, I've also come across Rogier Dijkman's research blog post on the subject, huge props on the amazing research and tool. With Karl and Rogier's findings and a bit of my own experimentation, this post takes a closer look at Managed Identities, Deployment Scripts, and how it can be used to escalate privileges in Azure. Whether you’re curious about how these features work or you’re looking for creative ways to break your Azure environment (responsibly, of course), I hope you'll find something to chew on here.
Now, let’s dig into Azure managed identities, specifically, user-assigned managed identities (UAMIs). We’ll explore what they are, how they work, and how they can be leveraged for privilege escalation in Azure environments.
What's Actually Happening Behind the Scenes
Before we jump into the attack path, you need to understand what's happening under the hood. When you create a Deployment Script in Azure, you're actually getting:
A containerized Azure environment
A supporting storage account (look for "*azscripts")
Container Instance resources linked to your script
This is more than just a simple script execution, it's a full environment you can leverage.
Managed Identities 101
Managed identities in Azure let you authenticate to other services without worrying about passwords, keys, or other secrets. They come in two types: System-assigned managed identities and User-assigned managed identities (UAMIs).
Here's how they differ:
Property | System-assigned managed identity | User-assigned managed identity |
Creation | Created as part of an Azure resource (for example, Azure Virtual Machines or Azure App Service). | Created as a stand-alone Azure resource. |
Life cycle | Shared life cycle with the Azure resource that the managed identity is created with. When the parent resource is deleted, the managed identity is deleted as well. | Independent life cycle. Must be explicitly deleted. |
Sharing across Azure resources | Can’t be shared. It can only be associated with a single Azure resource. | Can be shared. The same user-assigned managed identity can be associated with more than one Azure resource. |
Common use cases | Workloads contained within a single Azure resource. Workloads needing independent identities. For example, an application that runs on a single virtual machine. | Workloads that run on multiple resources and can share a single identity. Workloads needing preauthorization to a secure resource, as part of a provisioning flow. Workloads where resources are recycled frequently, but permissions should stay consistent. For example, a workload where multiple virtual machines need to access the same resource. |
Quick recap:
System-assigned Managed Identity: This is like a disposable coffee cup handed out at a café. It’s created specifically for one customer (resource) and thrown away when the coffee is finished (the resource is deleted). It’s simple, single-use, and tied directly to that one order.
User-assigned Managed Identity: This is like a reusable travel mug. It’s not tied to any one café (resource) and can be used across multiple locations. It sticks around until you decide to retire it, making it perfect for situations where consistent and flexible access is needed.
Where Can MIs Be Attached?
UAMIs can be attached to various Azure resources, including:
Virtual Machines
Azure Container Registries (ACR)
Automation Accounts
App Services (like Function Apps)
Azure Kubernetes Service (AKS)
Data Factory
Logic Apps
Deployment Scripts
The Technical Stuff You Need to Know
Here's what makes this interesting, Deployment Scripts in Azure let you execute PowerShell, CLI, or Bash commands in a containerized environment. When you combine this with a UAMI that has elevated permissions, you've got a potential privilege escalation path.
A few technical points to keep in mind:
MIs can have permissions across multiple subscriptions
You might not see all assigned roles due to permission limitations
Quick way to check available roles: Get-AzUserAssignedIdentity | ForEach-Object { Get-AzRoleAssignment -ObjectId $_.PrincipalId }
How an Attacker Sees This
Let's put on our adversarial hats for a minute:
Recon
Search for Resource Groups where you have User Access Administrator/Owner permissions (Shamelessly plugging RoleCrawl - you can read more about it here, TLDR; it's a PowerShell tool I have created that helps enumerate Azure role assignments, perfect for understanding what permissions your user/compromised MI actually has.)
Within those RGs, look for resources that have MIs attached
Check what permissions these MIs have, often they'll have high-privileged roles like Owner or Contributor
Focus on finding MIs with permissions beyond their RG, especially subscription-level access
Access the MI
Once you find a high-privileged MI, deploy a script to get its token
That token inherits ALL the MI's permissions, for example: if the MI has Owner rights on a subscription, your token will too
Privilege Escalation
Use the MI's token to access resources you normally couldn't!
Example: MI has Owner on subscription A, but you only had User Access Admin on a single RG.
By getting the MI's token, you've escalated from RG-level access to subscription Owner
Common scenario: Developers give MIs Owner rights for "ease of use", this becomes your path to higher privileges
The Attack Chain:
Let's break down how an attacker could leverage this:
Initial Access
You have Owner or User Access Administrator over a Resource Group
There's a resource in that RG with a high-privileged UAMI attached
Preparation
Use User Access Administrator (for example) to give yourself "Owner" permission on the RG
This grants you the Microsoft.ManagedIdentity/userAssignedIdentities/*/assign/action permission
Execution
Deploy an ARM template with a Deployment Script
Attach the high-privileged UAMI
Get its token
Use the token to authenticate
There's actually more you can do, in addition you can also:
Run commands directly on VMs
Create new role assignments
Access Key Vaults and Storage accounts
Execute external PowerShell code
A quick note about token scopes: while we used management.azure.com in our example, you can request tokens for different resources:
- graph.microsoft.com for Graph API access
- vault.azure.net for Key Vault operations
- storage.azure.com for Storage access
Just modify the Get-AzAccessToken command with -ResourceUrl for the scope you need.
You can execute this attack manually through the Azure Portal or CLI, but for simplicity and/or in the case you have no access to the Azure portal, I've created a PowerShell script that automates the entire process and handles the cleanup (You can either run it interactively, or in the case you use a C2, you can just run with all the flags without any further interaction required). Link to the DeployMI Github Repository
Azure Portal




Azure CLI




All things Blue
If this attack path has your attention (as it should), let's see how it can be mitigated.
Start with the basics: lock down your MI permissions to absolute minimums and regularly audit who has access to what. Remember, deployment scripts are just tools; it's misconfigured permissions and overprivileged MIs that create the attack paths.
To effectively detect this privilege escalation attack, you could monitor both specific Resource Provider Operations and common attack patterns. Here's what to look for:
IoCs
Resource Provider Operations
Identity and Role Operations
Microsoft.ManagedIdentity/userAssignedIdentities/assign/action
Microsoft.Authorization/roleAssignments/write
Microsoft.ManagedIdentity/identities/getToken/action
Deployment Operations
Microsoft.Resources/deployments/validate/action
Microsoft.Resources/deployments/write
Microsoft.Resources/deploymentScripts/write
Resource Operations
Microsoft.Storage/storageAccounts/write
Microsoft.Storage/storageAccounts/listKeys/action
Microsoft.ContainerInstance/containerGroups/write
Microsoft.Resources/deploymentScripts/logs/read
Microsoft.Resources/deploymentScripts/outputs/read
Cleanup Operations
Microsoft.Resources/deploymentScripts/delete
Microsoft.Resources/deployments/delete
Suspicious Patterns
In addition to monitoring Resource Provider Operations, watch for these common attack patterns that might indicate privilege escalation attempts:
MI role assignments outside business hours
Deployment scripts in unexpected resource groups
Short-lived deployment scripts (quick create-use-delete pattern)
Cross-subscription activities after MI token acquisition
Multiple deployment scripts created/deleted rapidly
New role assignments following deployment script execution
For Azure Sentinel users, here's a few KQL queries to detect rapid deployment script creation/deletion, Monitor for MI token acquisition followed by role assignments, and Monitor for cross-subscription activity:
AzureActivity
| where OperationName in ("Microsoft.Resources/deploymentScripts/write", "Microsoft.Resources/deploymentScripts/delete")
| summarize Count = count() by bin(TimeGenerated, 1h), ResourceGroup, Caller
| where Count > 3
AzureActivity
| where TimeGenerated > ago(1h)
| where OperationName in ("Microsoft.ManagedIdentity/identities/getToken/action", "Microsoft.Authorization/roleAssignments/write")
| order by TimeGenerated asc
| summarize Operations=make_list(OperationName), Times=make_list(TimeGenerated) by Caller, ResourceGroup
AzureActivity
| where TimeGenerated > ago(1h)
| where OperationName == "Microsoft.ManagedIdentity/identities/getToken/action"
| join kind=inner (
AzureActivity
| where TimeGenerated > ago(1h)
| where OperationName has "write"
) on Caller
| where ResourceGroup != "ResourceGroup1"
Credits & References:
Komentar