Skip to content

Back to the catalog

I Found a $90k Remote Code Execution Vulnerability in Google
simonkoeck.comwriteup

I Found a $90k Remote Code Execution Vulnerability in Google

Writeup of an unauthenticated RCE on Google's shared DevSite platform (developers.google.com, firebase.google.com, developer.android.com and others). The Flask session cookie embedded a pickled OAuth2WebServerFlow containing the OAuth client_secret, which was also the Flask SECRET_KEY. That let the author forge signed sessions carrying a malicious pickle that the OAuth callback unpickled; RCE was proven safely with time.sleep timing and negative controls. It earned a $90,000 bounty and lists a four-part fix.

TL;DR Flask SECRET_KEY was the OAuth client secret leaked inside the session cookie, so forged cookies reached pickle.loads and gave RCE across Google DevSite domains ($90k).
Document

What it is

An unauthenticated remote code execution in Google's shared DevSite platform. A single Flask session key signed valid sessions on developers.google.com, firebase.google.com, developer.android.com, source.android.com and docs.partner.android.com. The root cause is a Flask SECRET_KEY that is the same value as an OAuth client_secret, and that secret is embedded in the cookie it signs.

How it works

  1. GET /oauth2authorize?return_url=%2F needs no account. It sets a ~1,200-byte Flask session cookie.
  2. The cookie is only signed, not encrypted. A leading . on the payload means zlib compression. Decoding it shows google_oauth2_flow_<csrf> holding a base64 value tagged " b" (Flask's bytes marker).
  3. That value is a pickled oauth2client.client.OAuth2WebServerFlow containing client_id and client_secret in plaintext.
  4. The client_secret is also the Flask signing key. The author recomputed the signature offline and it matched the issued cookie byte for byte:
derived_key = hmac.digest(client_secret, b"cookie-session", "sha1")
expected = hmac.digest(
    derived_key,
    f"{payload}.{timestamp}".encode(),
    "sha1",
)
  1. The OAuth callback verifies the signature, reads the flow entry, base64-decodes it and calls pickle.loads. With the key known, an attacker forges a session with an arbitrary CSRF token and a matching flow key holding a malicious pickle. The state parameter is attacker-controlled, so nothing has to match server-side.
flowchart LR
  A[Cookie leaks client_secret] --> C[Forge signed session]
  B[client_secret = SECRET_KEY] --> C
  C --> D[Callback pickle.loads]
  D --> E[RCE]

The author avoided running commands on production. The pickle's __reduce__ returned time.sleep(N), and response time tracked N:

NResponse
00.207 s
33.193 s
55.194 s
88.283 s

Negative controls:

  • No cookie, or one flipped signature byte: Invalid request.
  • Valid signature with no flow entry: Invalid request state.
  • Valid signature with the real flow: reached the token exchange and failed with invalid_grant.

Preconditions / Affected

  • The app signs sessions with a key that is exposed or derivable.
  • The app unpickles session-held data on a reachable endpoint.
  • Here, both held on every DevSite deployment sharing the key. The page says the secret was still live when the post was written, but the issue is reported as fixed.

What to do

For your own Flask or OAuth apps, the page lists four fixes. Any one breaks the chain:

  1. Rotate both exposed OAuth client secrets and invalidate every session signed with them.
  2. Give each deployment its own high-entropy SECRET_KEY from a server-side store, unrelated to any OAuth credential.
  3. Keep the OAuth flow object server-side. The cookie should carry only an opaque reference.
  4. Don't use pickle across a client boundary. Use a data-only format with schema validation so a forged cookie is inert.

A quick audit follows from the write-up: decode your own session cookies, because Flask cookies are readable by anyone holding one. Look for secrets, pickled blobs, or long payloads. Then check whether the signing key is derived from, or equal to, any value that appears in them.

Caveats

  • Code execution was inferred from timing and controls, not from running commands on Google's servers.
  • The page doesn't give the report or fix dates, and the timeline entries carry no timestamps.
  • The reward was $90,000: $75,000 base for S0 RCE in a Normal Google Application (T2), times 1.2 for report quality.

Organisations named here

  • DevSite
  • pickle deserialization
  • REDUCE opcode
  • session forgery

Technologies named here

  • curl
  • Google DevSite platform
  • Python
  • developers.google.com
  • firebase.google.com
  • pickletools.dis
  • Python hmac
  • developer.android.com
  • source.android.com
  • itsdangerous
  • docs.partner.android.com
  • Python pickle with a __reduce__ time.sleep payload
  • Flask session cookies signed via itsdangerous
  • Google Bug Hunters
  • Google OAuth client_id and client_secret exposed in cookie
  • Flask
  • OAuth
  • Google

Techniques named here

  • Information disclosure
  • Offline signature recomputation
  • Insecure deserialization
  • Time-based blind proof of RCE with time.sleep
  • Negative controls
  • Cross-domain impact
  • Reward