Back

Escalating from Logic App Contributor to Root Owner in Azure

In October 2021, I was performing an Azure penetration test. By the end of the test, I had gained Owner access at the Root level of the tenant. This blog post will provide a short walkthrough of what I found and disclosed to the Microsoft Security Response Center (MSRC).

What was the bug?

The short explanation is that having Contributor access to an Azure Resource Manager (ARM) API Connection would allow you to create arbitrary role assignments as the connected user. This was supposed to be limited to actions at the Resource Group level, but an attacker could escape to the Subscription or Root level with a path traversal payload.

How did I find it?

It’s fair to say that I have spent a lot of time hacking on Logic Apps, and I experience a lot of recency bias with the services that I’ve dug into. After I published my Logic App research, I started seeing Logic Apps popping up on my tests. 

To recap, Azure Logic Apps use API Connections to authenticate actions to services. In my blog, Illogical Apps – Exploring and Exploiting Azure Logic Apps, I discuss how to tease unintended functionality out of Logic Apps to perform actions as the authenticated user. The examples I use involve listing out additional key vault keys or adding users to Azure AD. 

When we create an API Connection, it requires a user to authenticate it. That authentication will persist for the lifetime of the API Connection, unless changes are made to it which will invalidate the connection. You can view the connection dialog below.

API Connection dialog

In this environment there was an Azure Resource Manager API Connection authenticated as a user with User Access Administrator rights at the Root level. If you’re not familiar with Azure terminology, the User Access Administrator role allows for creating new role assignments, and the Root level is the highest tier in an Azure tenant. I had not looked at the ARM connector in my prior research, but I was confident we could abuse this level of access.

Initial Recon

Generally, our goal is to escalate to the Owner role on a Subscription. This is similar to getting Domain Administrator (DA) on an internal network penetration test in the sense that it is a bit oversimplified, but very useful for demonstrating the severity of a finding. I started looking at the relevant ARM actions that I could use to achieve this. Consulting the Microsoft documentation, “Create or update a resource group” looked like a good starting point. But looking at the parameters for the action, the Subscription and Resource Group parameters are required.

Create or update a resource group

While they’re required, we can insert custom values. If we make the Resource Group blank, will that work? No. Here’s why: API Connections are just wrappers around an API as the name would suggest. These APIs are defined by Swagger docs, and we can pull down the whole Swagger definition by using an ARM API Connection in a Logic App and making a request to the following resource:

/subscriptions/{subscription}d/resourceGroups/{resourceGroupName}/
Providers/Microsoft.Logic/Workflows/{LogicAppName}?api-version=
2016-10-01&$expand=properties/swagger

Looking at the Swagger definition, the endpoint for this action is a PUT request to this path:

/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/
providers/{resourceProviderNamespace}/{shortResourceId}

So, we can reasonably assume that when we use a blank Resource Group name, a request is getting made to:

/subscriptions/{subscriptionId}/resourceGroups//providers/
[truncated]

And we get an error since this Resource Group does not exist. At this point, I verified that using a valid Subscription and Resource Group name, it was possible to create Role Assignments at the Resource Group level. This is because Role Assignments are created like any other resource in Azure. At a minimum I was able to (as a Contributor) give myself Owner rights on all the Resource Groups in a Subscription. Still not a bad privilege escalation, but we can do better.

You might have spotted where this is headed given the format of the path above. If we can include custom values for the Resource Group and Subscription, can we manipulate the final path to perform actions at different scopes? If we provide “..%2F” as the Resource Group name, then our path will match the right Swagger path, but the server will resolve the payload and our request will end up going to:

/subscription/{subscriptionId}/providers/[truncated]

Now we can create Role Assignments at the Subscription level! Taking this one step further, we can traverse the Subscription path too, and create Role Assignments at the Root level (if the connected user has sufficient access).

Unnecessary Optimizations

At this point I had a working exploit in my lab, and I went to reproduce it in the client environment. It went off without a hitch, and I was now a Subscription Owner. While I was setting up the Logic App, I noticed something that I hadn’t before: Since my lab environment is very small, when I clicked the Subscription dropdown menu, it was populated with Subscriptions that my account didn’t have access to. This meant that these Subscriptions were being fetched in the context of the API Connection user – but I hadn’t run a Logic App.

To track down the behavior, I fired up Burp Suite and found that a request was being made to the “dynamicInvoke” endpoint of the API Connection. The request payload looked like this:

{"request":{"method":"get","path":"/subscriptions","queries":
{"x-ms-api-version":"2016-06-01"},}}

And the response looked like this:

"response":{"statusCode":"OK","body":{"value":[{"id":
"/subscriptions/[REDACTED]","authorizationSource":"RoleBased",
"subscriptionId":"[REDACTED]","displayName":"temp_sub","state":
"Enabled","subscriptionPolicies":{"locationPlacementId":
"Public_2014-09-01","quotaId":"PayAsYouGo_2014-09-01",
"spendingLimit":"Off"}}]},

Another area that I’ve spent a lot of time looking at is Azure’s REST API. Given that the response JSON included a status code, I figured the request to the dynamicInvoke endpoint triggered the server into making a request in the context of the connected user. 

For those curious, my understanding is that the server makes a request to https://logic-apis-[region].token.azure-apim.net:443/tokens/logic-apis-[region]/[connectorname]/[connector-id]/exchange which returns a token to the server. 

You can verify this by sending malformed input in the path value to the dynamicInvoke endpoint and observing the output. I assume that the returned token is then used to access the relevant services as the connected user.

Anyways, we can just hit this endpoint directly to trigger our exploit instead of creating a Logic App. This is what the final payload looked like:

{
   'request':{
'method':'PUT','path':'/subscriptions/$subscriptionId/
resourceGroups/..%2Fproviders/Microsoft.Authorization/
roleAssignments%2F$guid',
        'queries':{'x-ms-api-version':'2015-07-01'},
        'body':{
            'properties':{
                'principalId': '$principalId',
                'roleDefinitionId': '/providers/
Microsoft.Authorization/roleDefinitions/$roleDefinitionId'
}}}

I also confirmed that trying to hit the Subscription directly (without the resourceGroups part) via this endpoint did not work, it would yield a 404 error. But if we included the path traversal payload, then a nice “201 Created” message was returned instead. This is important, because it is proof that this wasn’t an intended behavior. 

Conclusion

To summarize, I was able to escalate from a Subscription Contributor to Root Owner by abusing an API Connection. The root cause of this behavior was that a path traversal payload would meet the Swagger API definition, and the payload would be resolved by the server resulting in a request to an unintended scope. 

This issue was responsibly disclosed to MSRC and acknowledged by Microsoft in March 2022. They remediated the issue by filtering the method value to block the paths that include the path traversal payload. 

I would still recommend that anyone using API Connections should evaluate what users are authenticated for each connection. If any of the authenticated users are privileged, there may a possibility for abuse.