Three More Google Cloud Shell Bugs Explained

Google Cloud Shell Intro

Because of the pandemic, I ended up with a lot of free time stuck in my apartment so I decided to try to get back into bug bounties. Since it’s been a while, I decided that my strategy would be to find an interesting target and then go very deep on understanding that target rather than jumping around between different targets. I spent a while poking around and decided to target Google Cloud Shell.

Google Cloud Shell is an online IDE that is hosted by Google for free. It has a built-in IDE and terminal, all in the browser. In order to make it easy to use Cloud Shell with Google Cloud, the created instances are automatically authenticated with your credentials. The thing that makes it really interesting as a target, is the Open in Cloud Shell feature which makes it possible to define URLs that when clicked will open Cloud Shell and take a variety of different actions. For example, clicking the below URL will git clone github.com/ddworken/foo and open up bar.py in the IDE:

https://ssh.cloud.google.com/cloudshell/editor?cloudshell_git_repo=https://github.com/ddworken/foo&cloudshell_open_in_editor=bar.py

As an attacker, if we can get the user’s Cloud Shell instance to execute our code just from clicking a link like the above, we can steal the user’s GCP credentials. So despite it being a relatively simple seeming target, there is actually a lot of surface area here with a pretty high impact if successful.

I also was encouraged by the fact a bunch of other people had successfully found some interesting bugs in Cloud Shell. @wtm_offensi has a great blog post on 4 different Cloud Shell bugs he discovered which gave me a good idea of what kind of bugs might exist here. Turns out, there was still plenty to find despite him reporting 9 different bugs. :)

Attacking the IDE: Ruby

From poking around on the host, we can tell that Cloud Shell’s editor is based on Eclipse Theia. Theia is an open-source project to build headless IDEs capable of running in the browser with a remote backend instance.

In order to support IDE features like autocomplete, autoformatting, and linting, many IDEs used Language Server Protocol (LSP) which defines a protocol for IDEs to communicate with language servers to get this metadata. This means multiple IDEs can all use the same language server to add support for a language.

Theia’s Ruby extension uses Solargraph to provide support for Ruby. From auditing Solargraph, I found this chunk of code:

      gemspecs.each do |file|
        base = File.dirname(file)
        # HACK: Evaluating gemspec files violates the goal of not running
        #   workspace code, but this is how Gem::Specification.load does it
        #   anyway.
        Dir.chdir base do
          begin
            # @type [Gem::Specification]
            spec = eval(File.read(file), TOPLEVEL_BINDING, file)

In order to parse gemspec files (which provide metadata about a project, similar to setup.py files), Solargraph simply evals them! This means if we create a repository that contains a gemspec file, and get someone to clone this repository into Cloud Shell, we have code execution. And remember: code execution in Cloud Shell ⟶ full access to the victim’s Google Cloud account.

Crafting our attack, this means the user simply has to click a link that looks like:

https://ssh.cloud.google.com/cloudshell/editor?cloudshell_git_repo=https://github.com/ddworken/rbp&cloudshell_open_in_editor=t.rb

Which will then send them to this page:

confirmation page

Once they click confirm, it is game over.

Attacking the IDE: TypeScript

Similarly, Cloud Shell (aka Theia) supports TypeScript through the TypeScript language server: TSServer. From digging through the docs about TSServer I discovered that it supports loading custom plugins to extend the language server. For example, a custom plugin could customize what autocomplete suggestions are provided in the IDE. This is done by installing a plugin into the global node_modules, and then creating a tsconfig.json file with the contents:

{
    "compilerOptions": {
        "plugins": [{ "name": "plugin-name" }]
    }
}

This causes it to try to load a plugin from: /google/.../node_modules/. So if we could get a package installed into the global node modules, we could use this to get code execution. But that doesn’t seem to be possible, but what happens if we put ../ in the plugin name? Turns out, TSServer doesn’t defend against this so we can easily trick it into loading a custom plugin from any directory:

{
    "compilerOptions": {
        "plugins": [{ "name": "../../../../../../../../home/david/cloudshell_open/tp/plugin-name" }]
    }
}

Which causes it to load the plugin from /home/david/cloudshell_open/tp/plugin-name which we as an attacker can easily inject code into by tricking the user into cloning a repository. But, this requires knowing the exact path to where our repository is cloned! But, this is Cloud Shell–we know it will be cloned into ~/cloudshell_open. So as an attacker, we just have to guess the victim’s username. We can easily work around this by just defining 10,000 plugins, one for each of the ten thousand most common first names.

So our final attack here just has the user clone a repository and open a TypeScript file, which triggers TSServer to load a custom plugin that was also defined in the cloned repository.

Our final link to exploit this ends up being:

https://ssh.cloud.google.com/cloudshell/editor?cloudshell_git_repo=https://github.com/ddworken/tp&cloudshell_open_in_editor=app/t.ts

Git Cloned, Go

Google Cloud Run allows users to deploy containers to Google Cloud and have them automatically scale as necessary. One key feature of Cloud Run is the Cloud Run Button, which allows people to create links that when clicked will guide a user through deploying an app to Cloud Run. This is built on top of Cloud Shell, so a Cloud Run Button is simply a link to something like:

https://ssh.cloud.google.com/cloudshell/editor?cloudshell_git_repo=https://github.com/user/project&cloudshell_image=gcr.io/cloudrun/button&shellonly=true

This creates a Cloud Shell instance using the special docker image gcr.io/cloudrun/button. When run, it will clone that repository into the user’s home directory, and then start guiding them through deploying it.

While at first glance this seems secure, if we look at $PATH we see:

/home/david/gopath/bin:/google/gopath/bin:/home/david/.gems/bin:/usr/local/go/bin:/opt/gradle/bin:/opt/maven/bin:/google/google-cloud-sdk/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/nvm/versions/node/v10.14.2/bin:/google/go_appengine:/google/google_appengine:/google/migrate/anthos/

Notably, ~/gopath/bin/ is the first thing in $PATH. This means that if we can place a binary in ~/gopath/bin/ we have code execution because we could swap out something like ls for our own malicious ls that steals user credentials. But, cloud run attempts to defend against this by checking whether the cloned directory collides with $PATH:

	cloneDir, err := handleRepo(repo)
	end(err == nil)
	if err != nil {
		return err
	}

	if ok, err := hasSubDirsInPATH(cloneDir); err != nil {
		return fmt.Errorf("failed to determine if clone dir has subdirectories in PATH: %v", err)
	} else if ok {
		return fmt.Errorf("cloning git repo to %s could potentially add executable files to PATH", cloneDir)
	}

But, this is actually not a complete fix for this bug because they clone the repository, and then check if it placed any files in the path. If it did, they simply crash with a warning. This leaves the user’s Cloud Shell machine in a dangerous state since the cloned repository is still on the disk and could still contain malicious binaries. So if the user ran any command (ie ls since they were confused about the error message) or even opened a new Cloud Shell instance (since the contents of ~/ persist for some period of time), this would lead to code execution. The final exploit for this vulnerability required clicking a link that looked like:

https://ssh.cloud.google.com/cloudshell/editor?cloudshell_git_repo=https://github.com/ddworken/gopath.git&cloudshell_image=gcr.io/cloudrun/button&shellonly=true

Then confirming they were okay cloning the repository, and then running any command after the crash.

Conclusion

Hopefully this blog post encourages other people to get out there and give bug bounties a shot. It was a lot of fun to work with Google on these bugs and it was really interesting to get the chance to dig deep into one product.

What ended up making this so interesting from a security perspective is that Cloud Shell’s threat model did not match the threat model of the software it was built on. Most IDEs seem to operate under the assumption that if a user opens a project in their IDE, they trust the project. But, Cloud Shell exposes an easy way to trick a user into opening a potentially untrusted project in a very privileged environment. This mismatch plus the fact that Cloud Shell is built on top of lots of open-source software leads to some fun bugs.