In previous months we identified vulnerabilities in Microsoft Azure Network Watcher and Azure App Services, leading us to investigate other types of Azure compute infrastructure. We found a new vulnerability in Azure Functions, which would allow an attacker to escalate privileges and escape the Azure Functions Docker container to the Docker host.
We reported the vulnerability to Microsoft’s security team. They have determined the issue has no security impact on Azure Functions users. Although it is possible to escape from the function to the host, the Docker host itself is protected by a Hyper-V boundary. Based on our findings Microsoft has made changes to block /etc and /sys directories since this change has already been deployed.
Instances like this underscore that vulnerabilities are sometimes out of the cloud user’s control. Attackers can find a way inside through vulnerable third-party software. While you should focus on reducing the attack surface as much as possible, you also need to prioritize the runtime environment to make sure you don’t have any malicious code lurking in your systems.
What is Azure Functions?
Azure Functions is a serverless compute service that allows users to run code without having to provision or manage infrastructure. Azure Functions is Microsoft’s equivalent to Amazon Web Services’ well-known Lambda service.
Azure Functions can be triggered by HTTP requests and are meant to run for only a few minutes in order to handle the event. Behind the scenes, the user’s code is run on an Azure-managed container and served without requiring the user to manage their own infrastructure. In other words, if the user wants to take a shortcut they can, since it’s expected that Microsoft will do it for them. This code is segmented securely and is not intended to escape from its confined environment. However, we will soon demonstrate why this is not the case.
We created a demonstration of the vulnerability—mimicking an attacker having execution on Azure Functions and escalating privileges to achieve a full escape to the Docker host. Check it out below.
An Azure function requires no infrastructure management. It’s triggered by a user merely uploading their code, which enables seamlessly calling the Function. In our example, it’s invoked via HTTP: https://test11114117.azurewebsites.net
Figure 1: Example Azure Function handler code
As the user can upload any code of their choice, we abused this to gain a foothold over the Function container and further understand its internals. We wrote a reverse shell to connect to our control server once the Function was executed, so that we could operate an interactive shell.
Figure 2: Azure Function reverse shell
Once the shell was on our Function we noticed that we were running as a unprivileged ‘app’ user in an endpoint with a ‘SandboxHost’ hostname:
Figure 3: Connecting to the Function reverse shell
The environment was mostly sterile from utilities, so we added several useful tools—most notably nmap—to our Function directory and then reuploaded the new Function package.
Using nmap, we scanned localhost to familiarize ourselves with the server. As a result we spotted multiple open ports:
Figure 4: Running nmap on an Azure Function
Since our goal was to find an elevation of privilege vulnerability, it was important that we find sockets belonging to processes associated with root. After interrogating network-related /proc files, we were able to map the ports to their corresponding processes:
Figure 5: Mapping each open port to the process that owns it
We found three privileged processes with an open port. The first was NGINX, a thoroughly tested open-source project. The local NGINX version had no known vulnerabilities so this wouldn’t have helped us.
The MSI and Mesh processes offered better chances at finding potential problems as they are close-sourced, undocumented Microsoft processes. As such, we were confident that they had been less thoroughly tested.
MSI, Managed Service Identity, a feature of the serverless model, eliminates the user’s need to manage identities, easing development by letting Azure handle it instead.
As for the Mesh binary, we couldn’t find much information (it’s unrelated to Azure’s Fabric Mesh service which has a similar name).
Unfortunately, the binaries belonging to the two processes reside in root-owned directories (e.g. /root/mesh/init) and we didn’t have access to them.
The Mesh process seemed to be less documented and also very relevant for our purposes, so we focused our efforts on finding out what this component does.
After searching for references to the Mesh binary in Google, we found the questioned “/root/mesh/init” path in the build log of a public Docker image in Docker Hub belonging to a Microsoft employee (we deduced this was public on purpose because it’s used internally somehow).
We downloaded the image, created a container with it and extracted the Mesh Init binary. The binary was compiled from a Go codebase and conveniently for our purposes wasn’t stripped.
Immediately as we opened the binary in IDA we noticed some interesting functions:
Figure 6: Mesh binary mount functions
Performing a mount is a privileged operation and should our unprivileged user access this functionality through the HTTP server, it could result in privilege escalation.
With this goal in mind and after some reverse engineering, we found the HTTP paths and variables that would allow us to invoke these functions. The server expected an HTTP variable to specify an operation to invoke: