Privilege Escalation via storage accounts

Rogier Dijkman
8 min readJan 10, 2023

--

In this blog I will explain the risk of storage accounts and how to abuse them for lateral movement.

Looking at the title of this blog, you probably think that I was able to find a set of credentials in a storage account and used these to login to the victims environment. Well No…

Okay, but in that case a storage account has no managed identity nor a credential object that we can abuse, so how would that work?

Good Question 👍

Starting the journey

I won’t go in to the nitty gritty details of a storage account and explain how they should be setup. The focus is mainly what information are you able to get from a storage account and how can this be used to escalate our privileges and eventually move lateral through the environment.

In our architecture we have the following components that are all playing a role in our part.

created with bicep visualizer

When creating a Function App resource in Microsoft Azure, some other resources are also created including a storage account that is used for logs and other data used by the Function App.

When opening the storage account with test my user Captain Kirk, which has the Storage Account Contributor role, we can see the associated content. What directly stands out is a folder named azure-webjobs-secrets
This definitely triggers my curiosity.

default content storage account for function app

When taking a deeper look at the contents of this folder two interesting files can be found. host.json and, in our case httpTrigger1.json which is the name of our function within the function app.

When calling the API endpoint of an Azure Function an application key is required to trigger the function code. This so called authorization level is to protect against unauthorized users, and is configured when creating the function.

When looking at the file content of the host.json we can see that there are values in this file for the masterKey and the functionKeys which can be one or more.

contents of host.json

Luckily these values are encrypted, because we don’t want anyone to have access to a masterKey of functionKeys and especially not a user that has no permissions to access to the Azure function right?!

Exploiting the Function App

To add a bit more context to what we just found I will give a brief explanation of these secrets. Within a Function App multiple secrets are stored in different places. Some of these values are stored as encrypted values in the App Settings while other secrets like found previously are stored encrypted in files.

The App Keys that are used to authorize the API call can be viewed in the Azure Portal in the App Keys section of the resource. From the portal these encrypted values are decrypted when a user has the required roles or permissions.

As shown in the image of the App Keys, there is a is value to authorize on host level _master, and another value related to the function level default.

host keys for a Function App

As discovered, the encrypted value that represent the master and default values are stored in the host.json file found in the azure-webjobs-secrets folder. So what would happen if we changed the masterKey and change the encrypted flag value from true to false ?

{
"masterKey": {
"name": "master",
"value": "NotSecureAnymore",
"encrypted": false
},
"functionKeys": [
{
"name": "default",
"value": "CfDJ8AAAAAAAAAAAAAAAAAAAAABp7XdLiKcSmN-ujGejBHaRse1fLDpUWM0lnohMstzMWiewiMddtUU0TwfmWX5NmfkEB9TnCSDUYmn46RPEFxsPHVK0Gd2VmBFZnPMrAU-aZY6AcDNkWrZZGFFDbbi34vPgA2JufgM62sZi11_yTp2ZXI8T2FyYsTHMn8WVDZokAw",
"encrypted": true
}
],
"systemKeys": [],
"hostName": "securehats-poc.azurewebsites.net",
"instanceId": "20ea833e4800cbfe732d6cf6a72d7549",
"source": "runtime",
"decryptionKeyId": "MACHINEKEY_DecryptionKey=X1P1bNFg/zTbqIkccMCSua6MoAiAmPldiD/xl9eL9iA=;"
}

Against all expectations Azure accepted this change and after waiting for a couple of minutes the changed master key value is also represented in the Azure Portal. Oh-No!

master key after changing the storage account file.

Now let’s check if this is actually going to work. So to see if my assumption is correct, I am first going to call the Function with the wrong credentials. As expected, this results in a 401 error (Unauthorized)

Now calling the function with our self created masterkey with the value NotSecureAnymore and B A M it works.

authenticate with newly created master key

Steal access token as Storage Account Contributor

As we now how can give our self permissions to call a function in a Function App, it is time to see if we can go one step further.

When using the portal experience for creating functions in a Function App, the source code is also stored in the storage account.
Instead of hosting these files in storage containers, these files are stored and in the Files shares section. The files can be found in the folder site\wwwroot\ followed by the name of the function, the actual API endpoint name of the function.

Interesting to note is that their also is a folder called .identityService which is telling us that the Function App has a managed identity.

In the HttpTrigger1 folder we can find the source code of our function. What Captain Kirk can do now as a Storage Account Contributor is either change the existing code, or create a new function by just creating a new folder with the name of the new function. But in our case we are going to change the existing code to keep it simple.

System-assigned Managed Identity token

The authentication of the Managed Identity in a Function App is generally configured in the profile.ps1 file. this file can be found on the location site/www/root/profile.ps1

By adding the following code snippet to the function it is possible to request the Access Token of the system-assigned Managed Identity of the Function App.

$resourceURI  = 'https://management.azure.com'
$apiVersion = '2019-08-01'

$tokenAuthURI = {0}?resource={1}&api-version={2} -f `
$env:IDENTITY_ENDPOINT, $resourceURI, $apiVersion

$tokenResponse = Invoke-RestMethod `
-Uri $tokenAuthURI `
-Headers @{"X-IDENTITY-HEADER" = "$env:IDENTITY_HEADER"}

$body = $tokenResponse

More information about this can be found in the Microsoft documentation

User-assigned Managed Identity token

If a User-Assigned Managed Identity is used we need a different approach to get the access token. The first step is to find the accountId of the user-assigned managed identity. This value can also be found in the profile.ps1 file or in the function code itself.

if ($env:MSI_SECRET) {
Disable-AzContextAutosave -Scope Process | Out-Null
# System Assigned
Connect-AzAccount -Identity
# User Assigned
Connect-AzAccount -Identity -AccountId <your-client-id>
}

Once we have found what the clientId of the managed identity, we can provide this and use it to generate an access token. by adding the snippet below to the function code.

$resourceURI  = 'https://management.azure.com'
$clientId = '<your-client-id>'
$apiVersion = '2019-08-01'

$tokenAuthURI = {0}?resource={1}&client_id={2}&api-version={3} -f `
$env:IDENTITY_ENDPOINT, $resourceURI, $clientId, $apiVersion

$tokenResponse = Invoke-RestMethod `
-Uri $tokenAuthURI `
-Headers @{"X-IDENTITY-HEADER" = "$env:IDENTITY_HEADER"}

$body = $tokenResponse

If everything works as expected we will get an access token when calling the the function from our local machine. And remember, we are still Captain Kirk with only the Reader and the Storage Account Contributor role assigned.

Get access token as low privileged user.

As we can see, by modifying the host.json file and the source code files in the storage account, we are able to get an access token that can be used for lateral movement.

Abusing the Access Token

After requesting the access token for the the Function App we can start digging deeper in the environment. The first thing that we want to know is what Role Assignment the managed identity has.

To get this information we need to have the principalId of our managed identity. We can either modify our function code to retrieve this information, or use the acquired access token to login from our local machine. In this case I will will use my local machine and authenticate with the access token.

Connect-AzAccount `
-AccessToken $response.access_token `
-AccountId $response.client_id

From a logging perspective, we are now logged in as the managed identity which makes it hard for a security analyst to detect that it is actually a bad actor that has now logged in.

Once the connection to the Azure environment has been established we can retrieve the principalId of our account.

(Get-AzResource -Name 'securehats-poc' `
-ResourceType 'Microsoft.Web/sites').identity.userassignedIdentities

Owh but that is Cool! we have now found out that our Function App has multiple managed identities assigned. For now we are interested in the first principalId value that belongs to our clientId.

Now that we have this value we can explore the assigned permissions by entering the code shown below.

Get-AzRoleAssignment -ObjectId <principalId>

If you were unable to get a principalId value, it probably means that the managed identity has limited permissions on resources. In that case it is still possible to check the permissions.

Get-AzRoleassignment
Roles assigned to user assigned identity

Our managed identity has both Contributor permissions on subscription level and Key Vault Contributor permissions. But as we have noticed in the there was also another managed identity associated to this Function App. Let's see what permissions this has.

Other user assigned identity

Bingo! Captain Kirk now has access to an account with Owner permissions on the subscription and is therefor king of our castle.

If you want to use the API endpoints of Azure, it is also possible to create a HTTP header containing the access token.

$header = @{
Authorization = "Bearer $($tokenResponse.access_token)"
}

$mngtURI = 'https://management.azure.com'
$endpoint = 'subscriptions'
$apiVersion = '2022-06-01'

$uri = {0}/{1}?api-version={2} -f $$mngtURI, $endpoint, $apiVersion

Invoke-RestMethod -Uri $uri -Headers $headers

References

--

--

Rogier Dijkman

Microsoft Security MVP | Azure | GitHub | Cloud Security Architect | Marathoner | passionate about Microsoft Security