A look into Google's internal Android Authentication Service

21/09/1991

If you've ever logged into a Google account via Google Play Services (GPS) on an Android device, you might have noticed that all Google apps are able to authenticate as your Google account seamlessly, without having to manually log into each one.
ā€Ž
The way this works is your Google account's Android session is actually tied to a refresh token that's generated the first time you log in. Unlike on the web where Google internal APIs use cookies for authentication, on Android and iOS scoped bearer tokens generated from a refresh token are used instead.

How Google API authentication works on the web

If we look at a random request to the People Internal API to lookup a Google user from the web that we can find from DevTools on https://chat.google.com:

POST /$rpc/google.internal.people.v2.minimal.InternalPeopleMinimalService/GetPeople HTTP/2
Host: people-pa.clients6.google.com
Cookie: <redacted>
Content-Type: application/json+protobuf
X-Goog-Api-Key: AIzaSyB0RaagJhe9JF2mKDpMml645yslHfLI8iA
Origin: https://chat.google.com
Authorization: SAPISIDHASH <redacted>
...

This request is in the context of the Google Cloud project ID tied to the chat.google.com key AIzaSyB0RaagJhe9JF2mKDpMml645yslHfLI8iA and the request is authenticated as your Google account through the Cookie and SAPISIDHASH (this value is generated using the SAPISID cookie)

If you're worked with Google Cloud before, you might know that APIs need to be enabled for your project before you can make calls to them. If we tried taking this key and doing a call to some random unrelated googleapi like the Play Atoms Private API playatoms-pa.googleapis.com:

GET /$discovery/rest
Host: playatoms-pa.googleapis.com
X-Goog-Api-Key: AIzaSyB0RaagJhe9JF2mKDpMml645yslHfLI8iA

We would get the following error:

{
  "error": {
    "code": 403,
    "message": "Play Atoms Private API has not been used in project 576267593750 before or it is disabled. Enable it by visiting https://console.developers.google.com/apis/api/playatoms-pa.googleapis.com/overview?project=576267593750 then retry. If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry.",
    ...
  }
}

This is because the Play Atoms Private API wasn't enabled for the Google Cloud project ID 576267593750 that's associated with that API key.

What about on Android?

On Android, that same Internal People API request would look something like this:

POST /$rpc/google.internal.people.v2.minimal.InternalPeopleMinimalService/GetPeople HTTP/2
Host: people-pa.clients6.google.com
Content-Type: application/json+protobuf
Authorization: ya29.<redacted>
...

There is no need for an API key for this request, as the bearer token actually includes the context of the Google API project that you used to generate the bearer token. (this will make more sense once we look into how bearer tokens are generated from an android refresh token)

The interesting thing about Google APIs is that requests from the context of certain Google Cloud project IDs have extra functionality/permissions enabled just for that project on that API. This is usually based on the requirements of the client (ex. the Google Chat app may need to be able to fetch extra information on other Google users from the Internal People API as compared to something like Google Earth)

Android Refresh Tokens (aas/xx)

So, how can we generate an Android refresh token to use for testing? It's actually quite simple. We can simply visit https://accounts.google.com/EmbeddedSetup, go through the authentication flow, and at the end there will be a cookie set called oauth_token

We can then do the following request to exchange this oauth_token for an Android refresh token:

POST /auth
Host: android.googleapis.com
User-Agent: com.google.android.gms/243530022
Content-Type: application/x-www-form-urlencoded

androidId=fb213fefa471dcde&Token=<oauth_token>&service=ac2dm&get_accountid=1&ACCESS_TOKEN=1&callerPkg=com.google.android.gms&add_account=1&callerSig=38918a453d07199354f8b19af05ec6562ced5788

The androidId is just any random 16 character hex string. At the moment you don't require this for generating a bearer token, but this could change in the future so it's advisable to store it along with your Android refresh token.

On newer Android versions, a DroidGuard token is also supplied to this request. My guess is that it's an anti-abuse measure. However, they're unable to enforce this token without breaking Google Play Services support for older Android devices. It's possible this could be changed in the future though.

The response to the request will look something like this:

HTTP/2 200 OK
Content-Type: text/plain; charset=utf-8

Token=aas_et/<redacted>
Auth=g.a000<redacted>
SID=BAD_COOKIE
LSID=BAD_COOKIE
services=mail,hist,dynamite,cl,youtube,jotspot,uif,multilogin,analytics
Email=<redacted>@gmail.com
GooglePlusUpdate=0
firstName=<redacted>
lastName=<redacted>
capabilities.canHaveUsername=1
capabilities.canHavePassword=1
...

You can actually see this Android device on https://myaccount.google.com/device-activity

Generating a Bearer Token

Now that you have an Android refresh token, you can use this to generate a bearer token in the context of a Android app's Google Cloud project with the scopes that you require.

This is an example request to generate scopes for Google Play Games to use with playgateway-pa.googleapis.com

POST /auth HTTP/2
Host: android.googleapis.com
User-Agent: GoogleAuth/1.4
Content-Length: 808
Content-Type: application/x-www-form-urlencoded

androidId=fb213fefa471dcde&app=com.google.android.play.games&service=oauth2:https://www.googleapis.com/auth/games.firstparty https://www.googleapis.com/auth/googleplay&client_sig=38918a453d07199354f8b19af05ec6562ced5788&has_permission=1&Token=<redacted>

Let's breakdown everything in that request:

  • android_id: this isn't validated, it can be any 16 character hex string
  • app: this is the package name of the app who's google cloud project context you wish to use (you can find this from the web play.google.com url, ex. https://play.google.com/store/apps/details?id=com.google.android.play.games)
  • service: these are space seperated scopes you wish to generate the bearer token with (note: they have to be approved for that app's google cloud project)
  • client_sig: this is the SHA1 hash in hex format of the app's signature
  • has_permission: this is only required on few android clients that don't have auto mode enabled for them. essentially, this means that on the Android device, it would prompt the user for permission before actually issuing the token. we can just set this to 1 to avoid this issue
  • Token: this is your Android refresh token

It's actually possible to omit client_sig and app for certain scopes, but you wouldn't have the context of the Google API project and this does not work for most scopes.

The first problem we have is, let's say we want to get authentication on the following Google Internal People API endpoint: https://people-pa.googleapis.com/v2/people to start playing around with it, how would we know what scopes this endpoint needs?

In this case, there's a public discovery document that lists all the endpoints and the scopes for each of them, but many googleapis may require an API key to access the discovery document which we may not always have (ex. gameswhitelisted).

Turns out, if we send a request to an endpoint with a bearer token with insufficient scopes, it actually tells us all the scopes we need:

GET /v2/people
Authorization: Bearer ya29.<redacted>
HTTP/2 403 Forbidden
Www-Authenticate: Bearer realm="https://accounts.google.com/", error="insufficient_scope", scope="https://www.googleapis.com/auth/peopleapi.legacy.readwrite https://www.googleapis.com/auth/plus.peopleapi.readwrite https://www.googleapis.com/auth/peopleapi.readonly https://www.googleapis.com/auth/peopleapi.readwrite openid https://www.googleapis.com/auth/plus.me"
Content-Type: application/json; charset=UTF-8

{
  "error": {
    "code": 403,
    "message": "Request had insufficient authentication scopes.",
    ...
        "metadata": {
          "service": "people-pa.googleapis.com",
          "method": "google.internal.people.v2.InternalPeopleService.GetPeople"
        }
    ...
  }
}

Something interesting to note: google.internal.people.v2.InternalPeopleService.GetPeople is actually the gRPC service name of the endpoint. Perhaps I'll discuss more about how we can use this on a future blog post.

To simply this process, I wrote a Go script that I've published on GitHub that we can use to easily get this information:

$ export ANDROID_REFRESH_TOKEN=<redacted>
$ git clone https://github.com/ddd/aas
$ cd grpc-service-info
$ go build # this requires golang to be installed, see https://go.dev/doc/install
$ ./grpc-service-info -e https://people-pa.googleapis.com/v2/people

Now that we have the scopes we need. Let's say we want to call this endpoint in the context of Google Chat. We can get the package name com.google.android.apps.dynamite from the Play Store web URL (https://play.google.com/store/apps/details?id=com.google.android.apps.dynamite) but we still need the client_sig of the app.

While this is true for most cases, the client signature isn't necessarily always the SHA1 hash of the target app's signature. To solve this problem, I collected the package names as well as SHA1 client signature of all Google apps and wrote a Rust program that bruteforces all SHA1 signature and package name combinations to find working ones.

You can find the output of this script here

We can simply search this file for com.google.android.apps.dynamite and we can see that the client_sig change this works for this app:

"com.google.android.apps.dynamite": [
    {
      "spatula": "CkAKIGNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLmR5bmFtaXRlGhxVWnhhRjZZRmx1YitXVE81eTBLRjU3RGw2M3M9GLingOeJmKD6Ng==",
      "sig": "519c5a17a60596e6fe5933b9cb4285e7b0e5eb7b"
    }
  ],

A word on X-Goog-Spatula

If you've ever looked at Android traffic to googleapis, you might have noticed this header. It's actually just a keyless authentication header. Similar to an API key, it's used to provide context to a specific Google Cloud project.

They look like this (base64-encoded protobuf):

CjoKGmFwcC5nZXRsb2FkZWQuYml0Y29pbmJsYXN0Ghw2Wmk4VHdRTnlpT0QrdXMyNC81YVlwd3h0NUE9GLingOeJmKD6Ng==

If we look at how this is formed:

$ echo -n "CjoKGmFwcC5nZXRsb2FkZWQuYml0Y29pbmJsYXN0Ghw2Wmk4VHdRTnlpT0QrdXMyNC81YVlwd3h0NUE9GLingOeJmKD6Ng==" | base64 -d | protoc --decode_raw
1 {
  1: "app.getloaded.bitcoinblast" // package name
  3: "6Zi8TwQNyiOD+us24/5aYpwxt5A=" // base64 of SHA1 hash of the app signature
}
3: 3959931537119515576 // this is generated from DroidGuard using the device_key

This example is from some Spatula header I found on the internet

If you wish to dive into how this DroidGuard value is generated, there's an awesome gist on this, but we don't actually need to care about that in order to utilize it. As it turns out, this value isn't actually validated, and we can impersonate any client we want by simply changing the package name and SHA1 hash of the app signature.

From what I've tested, the package name and client sig pairs from the output of the Rust program earlier seem to work for this as well.

Since just like API keys, they provide context of a Google cloud project, we're actually able to use this to fetch discovery documents of several Android Google APIs like gameswhitelisted.googleapis.com:

GET /$discovery/rest
Host: gameswhitelisted.googleapis.com
X-Goog-Spatula: CjoKGmFwcC5nZXRsb2FkZWQuYml0Y29pbmJsYXN0Ghw2Wmk4VHdRTnlpT0QrdXMyNC81YVlwd3h0NUE9GLingOeJmKD6Ng==
discovery document here

These discovery documents are similar to swagger documents and list all endpoints a Google API has so they're extremely useful for pentesting Google APIs.

That's all for now! Happy hacking and feel free to reach out to me if you have any questions.

why herllo there aa bbb
cccc dddd 1234 haaa 567
dddd fff ccc eee ggg
bbb llll jjj iii hhh
  • helloooo
  • hahaha
  • wassup
  1. meow
  2. meow
  3. meow

You can contact me via signal icon or email icon