https://kibty.town/blog/arc/

Submitted URL:
https://kibty.town/blog/arc/
Report Finished:

The outgoing links identified from the page

JavaScript Variables · 14 found

Global JavaScript variables loaded on the window object of a page, are variables declared outside of functions and accessible from anywhere in the code within the current scope

Console log messages · 2 found

Messages logged to the web console

HTML

The raw HTML body of the page

<!DOCTYPE html><html><head><title>gaining access to anyones browser without them even visiting a website - eva's site</title><link rel="stylesheet" href="/files/base.css"><link rel="stylesheet" href="/files/hljs.css"><meta charset="utf-8"><meta name="viewport" content="width=device-width"><meta content="gaining access to anyones browser without them even visiting a website - eva's site" property="og:title"><script defer="" src="https://cloud.umami.is/script.js" data-website-id="6c6e1532-c888-4f12-8546-db4c92d49039"></script><link rel="stylesheet" href="/files/post.css"><link rel="alternate" type="application/rss+xml" href="https://kibty.town/blog.rss"><link rel="alternate" type="application/json" href="https://kibty.town/blog.json"><meta content="gaining access to anyones browser without them even visiting a website" property="og:description"><link href="https://storage.ko-fi.com/cdn/scripts/floating-chat-wrapper.css" rel="stylesheet" type="text/css"><link href="https://fonts.googleapis.com/css?family=Nunito:400,700,800&amp;display=swap" rel="stylesheet" type="text/css"></head><body><img src="https://analytics.kibty.town/visitor-satisfaction" style="width:0px;height:0px;display:none;"><script src="/files/js/oneko.js"></script><div id="oneko" aria-hidden="true" style="width: 32px; height: 32px; position: fixed; pointer-events: none; background-image: url(&quot;https://raw.githubusercontent.com/adryd325/oneko.js/main/oneko.gif&quot;); image-rendering: pixelated; left: 16px; top: 16px; background-position: -96px -96px;"></div><div class="main"><div><nav class="nav"><a href="/">home</a><a href="/blog">blog</a><a href="/contact">contact</a></nav><div class="post-header"><h1>gaining access to anyones browser without them even visiting a website</h1><p>and of course, firebase was the cause (CVE-2024-45489)</p></div><article><div><p>we start at the homepage of arc. where i first landed when i first heard of it. i snatched a download and started analysing, the first thing i realised was that arc requires an account to use, why do they require an account?</p>
<h2>introducing arcs cloud features</h2>
<p>so i boot up my mitmproxy instance and i sign up, and i see that they are using firebase for authentication, but no other requests, are they really just using firebase only for authentication?</p>
<p>after poking around for a bit, i discovered that there was a arc featured called easels, easels are a whiteboard like interface, and you can share them with people, and they can view them on the web. when i clicked the share button however, there was no requests in my mitmproxy instance, so whats happening here?</p>
<h2>hacking objective-c based firebase apps</h2>
<p>from previous experience hacking an IOS based app, i immediately had a hunch on what this was, firestore.</p>
<p>firestore is a database-as-a-backend service that allows for developers to not care about writing a backend, and instead write database security rules and make users directly access the database.</p>
<p>this has <a href="https://env.fail/posts/firewreck-1">of course sparked a lot of services having insecure or insufficient security rules</a> and since researching that, i would like to call myself a firestore expert.</p>
<p>firestore has a tendency to not abide by the system proxy settings in the Swift SDK for firebase, so going off my hunch, i wrote a frida script to dump the relevant calls.</p>
<pre><code class="language-js hljs language-javascript"><span class="hljs-keyword">var</span> documentWithPath =
  <span class="hljs-title class_">ObjC</span>.<span class="hljs-property">classes</span>.<span class="hljs-property">FIRCollectionReference</span>[<span class="hljs-string">"- documentWithPath:"</span>];
<span class="hljs-keyword">var</span> queryWhereFieldIsEqualTo =
  <span class="hljs-title class_">ObjC</span>.<span class="hljs-property">classes</span>.<span class="hljs-property">FIRQuery</span>[<span class="hljs-string">"- queryWhereField:isEqualTo:"</span>];
<span class="hljs-keyword">var</span> collectionWithPath = <span class="hljs-title class_">ObjC</span>.<span class="hljs-property">classes</span>.<span class="hljs-property">FIRFirestore</span>[<span class="hljs-string">"- collectionWithPath:"</span>];

<span class="hljs-keyword">function</span> <span class="hljs-title function_">getFullPath</span>(<span class="hljs-params">obj</span>) {
  <span class="hljs-keyword">if</span> (obj.<span class="hljs-property">path</span> &amp;&amp; <span class="hljs-keyword">typeof</span> obj.<span class="hljs-property">path</span> === <span class="hljs-string">"function"</span>) {
    <span class="hljs-keyword">return</span> obj.<span class="hljs-title function_">path</span>().<span class="hljs-title function_">toString</span>();
  }
  <span class="hljs-keyword">return</span> obj.<span class="hljs-title function_">toString</span>();
}

<span class="hljs-keyword">var</span> queryStack = [];

<span class="hljs-keyword">function</span> <span class="hljs-title function_">logQuery</span>(<span class="hljs-params">query</span>) {
  <span class="hljs-keyword">var</span> queryString = <span class="hljs-string">`firebase.<span class="hljs-subst">${query.type}</span>("<span class="hljs-subst">${query.path}</span>")`</span>;
  query.<span class="hljs-property">whereClauses</span>.<span class="hljs-title function_">forEach</span>(<span class="hljs-function">(<span class="hljs-params">clause</span>) =&gt;</span> {
    queryString += <span class="hljs-string">`.where("<span class="hljs-subst">${clause.fieldName}</span>", "==", "<span class="hljs-subst">${clause.value}</span>")`</span>;
  });
  <span class="hljs-variable language_">console</span>.<span class="hljs-title function_">log</span>(queryString);
}

<span class="hljs-title class_">Interceptor</span>.<span class="hljs-title function_">attach</span>(documentWithPath.<span class="hljs-property">implementation</span>, {
  <span class="hljs-attr">onEnter</span>: <span class="hljs-keyword">function</span> (<span class="hljs-params">args</span>) {
    <span class="hljs-keyword">var</span> parent = <span class="hljs-title class_">ObjC</span>.<span class="hljs-title class_">Object</span>(args[<span class="hljs-number">0</span>]);
    <span class="hljs-keyword">var</span> docPath = <span class="hljs-title class_">ObjC</span>.<span class="hljs-title class_">Object</span>(args[<span class="hljs-number">2</span>]).<span class="hljs-title function_">toString</span>();
    <span class="hljs-keyword">var</span> fullPath = <span class="hljs-title function_">getFullPath</span>(parent) + <span class="hljs-string">"/"</span> + docPath;
    <span class="hljs-keyword">var</span> query = { <span class="hljs-attr">type</span>: <span class="hljs-string">"doc"</span>, <span class="hljs-attr">path</span>: fullPath, <span class="hljs-attr">whereClauses</span>: [] };
    queryStack.<span class="hljs-title function_">push</span>(query);
    <span class="hljs-title function_">logQuery</span>(query);
  },
});

<span class="hljs-title class_">Interceptor</span>.<span class="hljs-title function_">attach</span>(collectionWithPath.<span class="hljs-property">implementation</span>, {
  <span class="hljs-attr">onEnter</span>: <span class="hljs-keyword">function</span> (<span class="hljs-params">args</span>) {
    <span class="hljs-keyword">var</span> collectionPath = <span class="hljs-title class_">ObjC</span>.<span class="hljs-title class_">Object</span>(args[<span class="hljs-number">2</span>]).<span class="hljs-title function_">toString</span>();
    <span class="hljs-keyword">var</span> query = { <span class="hljs-attr">type</span>: <span class="hljs-string">"collection"</span>, <span class="hljs-attr">path</span>: collectionPath, <span class="hljs-attr">whereClauses</span>: [] };
    queryStack.<span class="hljs-title function_">push</span>(query);
  },
});

<span class="hljs-title class_">Interceptor</span>.<span class="hljs-title function_">attach</span>(queryWhereFieldIsEqualTo.<span class="hljs-property">implementation</span>, {
  <span class="hljs-attr">onEnter</span>: <span class="hljs-keyword">function</span> (<span class="hljs-params">args</span>) {
    <span class="hljs-keyword">var</span> fieldName = <span class="hljs-title class_">ObjC</span>.<span class="hljs-title class_">Object</span>(args[<span class="hljs-number">2</span>]).<span class="hljs-title function_">toString</span>();
    <span class="hljs-keyword">var</span> value = <span class="hljs-title class_">ObjC</span>.<span class="hljs-title class_">Object</span>(args[<span class="hljs-number">3</span>]).<span class="hljs-title function_">toString</span>();

    <span class="hljs-keyword">if</span> (queryStack.<span class="hljs-property">length</span> &gt; <span class="hljs-number">0</span>) {
      <span class="hljs-keyword">var</span> currentQuery = queryStack[queryStack.<span class="hljs-property">length</span> - <span class="hljs-number">1</span>];
      currentQuery.<span class="hljs-property">whereClauses</span>.<span class="hljs-title function_">push</span>({ <span class="hljs-attr">fieldName</span>: fieldName, <span class="hljs-attr">value</span>: value });
    }
  },
  <span class="hljs-attr">onLeave</span>: <span class="hljs-keyword">function</span> (<span class="hljs-params">retval</span>) {},
});

<span class="hljs-keyword">var</span> executionMethods = [
  <span class="hljs-string">"- getDocuments"</span>,
  <span class="hljs-string">"- addSnapshotListener:"</span>,
  <span class="hljs-string">"- getDocument"</span>,
  <span class="hljs-string">"- addDocumentSnapshotListener:"</span>,
  <span class="hljs-string">"- getDocumentsWithCompletion:"</span>,
  <span class="hljs-string">"- getDocumentWithCompletion:"</span>,
];

executionMethods.<span class="hljs-title function_">forEach</span>(<span class="hljs-keyword">function</span> (<span class="hljs-params">methodName</span>) {
  <span class="hljs-keyword">if</span> (<span class="hljs-title class_">ObjC</span>.<span class="hljs-property">classes</span>.<span class="hljs-property">FIRQuery</span>[methodName]) {
    <span class="hljs-title class_">Interceptor</span>.<span class="hljs-title function_">attach</span>(<span class="hljs-title class_">ObjC</span>.<span class="hljs-property">classes</span>.<span class="hljs-property">FIRQuery</span>[methodName].<span class="hljs-property">implementation</span>, {
      <span class="hljs-attr">onEnter</span>: <span class="hljs-keyword">function</span> (<span class="hljs-params">args</span>) {
        <span class="hljs-keyword">if</span> (queryStack.<span class="hljs-property">length</span> &gt; <span class="hljs-number">0</span>) {
          <span class="hljs-keyword">var</span> query = queryStack.<span class="hljs-title function_">pop</span>();
          <span class="hljs-title function_">logQuery</span>(query);
        }
      },
    });
  }
});

<span class="hljs-keyword">function</span> <span class="hljs-title function_">formatFirestoreData</span>(<span class="hljs-params">data</span>) {
  <span class="hljs-keyword">if</span> (data.<span class="hljs-title function_">isKindOfClass_</span>(<span class="hljs-title class_">ObjC</span>.<span class="hljs-property">classes</span>.<span class="hljs-property">NSDictionary</span>)) {
    <span class="hljs-keyword">let</span> result = {};
    data.<span class="hljs-title function_">enumerateKeysAndObjectsUsingBlock_</span>(
      <span class="hljs-title class_">ObjC</span>.<span class="hljs-title function_">implement</span>(<span class="hljs-keyword">function</span> (<span class="hljs-params">key, value</span>) {
        result[key.<span class="hljs-title function_">toString</span>()] = value.<span class="hljs-title function_">toString</span>();
      })
    );
    <span class="hljs-keyword">return</span> <span class="hljs-title class_">JSON</span>.<span class="hljs-title function_">stringify</span>(result);
  }
  <span class="hljs-keyword">return</span> data.<span class="hljs-title function_">toString</span>();
}

<span class="hljs-keyword">var</span> documentMethods = [
  { <span class="hljs-attr">name</span>: <span class="hljs-string">"- updateData:completion:"</span>, <span class="hljs-attr">type</span>: <span class="hljs-string">"update"</span> },
  { <span class="hljs-attr">name</span>: <span class="hljs-string">"- updateData:"</span>, <span class="hljs-attr">type</span>: <span class="hljs-string">"update"</span> },
  { <span class="hljs-attr">name</span>: <span class="hljs-string">"- setData:completion:"</span>, <span class="hljs-attr">type</span>: <span class="hljs-string">"set"</span> },
  { <span class="hljs-attr">name</span>: <span class="hljs-string">"- setData:"</span>, <span class="hljs-attr">type</span>: <span class="hljs-string">"set"</span> },
];

documentMethods.<span class="hljs-title function_">forEach</span>(<span class="hljs-keyword">function</span> (<span class="hljs-params">method</span>) {
  <span class="hljs-keyword">if</span> (<span class="hljs-title class_">ObjC</span>.<span class="hljs-property">classes</span>.<span class="hljs-property">FIRDocumentReference</span>[method.<span class="hljs-property">name</span>]) {
    <span class="hljs-title class_">Interceptor</span>.<span class="hljs-title function_">attach</span>(
      <span class="hljs-title class_">ObjC</span>.<span class="hljs-property">classes</span>.<span class="hljs-property">FIRDocumentReference</span>[method.<span class="hljs-property">name</span>].<span class="hljs-property">implementation</span>,
      {
        <span class="hljs-attr">onEnter</span>: <span class="hljs-keyword">function</span> (<span class="hljs-params">args</span>) {
          <span class="hljs-keyword">var</span> docRef = <span class="hljs-title class_">ObjC</span>.<span class="hljs-title class_">Object</span>(args[<span class="hljs-number">0</span>]);
          <span class="hljs-keyword">var</span> data = <span class="hljs-title class_">ObjC</span>.<span class="hljs-title class_">Object</span>(args[<span class="hljs-number">2</span>]);
          <span class="hljs-keyword">var</span> fullPath = <span class="hljs-title function_">getFullPath</span>(docRef);
          <span class="hljs-keyword">var</span> formattedData = <span class="hljs-title function_">formatFirestoreData</span>(data);
          <span class="hljs-variable language_">console</span>.<span class="hljs-title function_">log</span>(
            <span class="hljs-string">`firebase.doc("<span class="hljs-subst">${fullPath}</span>").<span class="hljs-subst">${method.type}</span>(<span class="hljs-subst">${formattedData}</span>)`</span>
          );
        },
      }
    );
  } <span class="hljs-keyword">else</span> {
    <span class="hljs-variable language_">console</span>.<span class="hljs-title function_">log</span>(<span class="hljs-string">"Warning: "</span> + method.<span class="hljs-property">name</span> + <span class="hljs-string">" not found"</span>);
  }
});
</code></pre>
<p>hacky script, but it works. so i launched arc with the script loaded on startup and this is what i got:</p>
<pre><code class="language-js hljs language-javascript">firebase.<span class="hljs-title function_">doc</span>(<span class="hljs-string">"preferences/UvMIUnuxJ2h0E47fmZPpHLisHn12"</span>);
firebase.<span class="hljs-title function_">doc</span>(
  <span class="hljs-string">"preferences/UvMIUnuxJ2h0E47fmZPpHLisHn12/stringValues/autoArchiveTimeThreshold"</span>
);
firebase.<span class="hljs-title function_">doc</span>(<span class="hljs-string">"preferences/UvMIUnuxJ2h0E47fmZPpHLisHn12"</span>);
firebase.<span class="hljs-title function_">doc</span>(
  <span class="hljs-string">"preferences/UvMIUnuxJ2h0E47fmZPpHLisHn12/stringValues/autoArchiveLittleArcTimeThreshold"</span>
);
firebase.<span class="hljs-title function_">doc</span>(<span class="hljs-string">"preferences/UvMIUnuxJ2h0E47fmZPpHLisHn12"</span>);
firebase.<span class="hljs-title function_">doc</span>(
  <span class="hljs-string">"preferences/UvMIUnuxJ2h0E47fmZPpHLisHn12/stringValues/autoArchiveTimeThresholdsPerProfile"</span>
);
firebase.<span class="hljs-title function_">doc</span>(<span class="hljs-string">"users/UvMIUnuxJ2h0E47fmZPpHLisHn12"</span>);
firebase
  .<span class="hljs-title function_">collection</span>(<span class="hljs-string">"user_referrals"</span>)
  .<span class="hljs-title function_">where</span>(<span class="hljs-string">"inviter_id"</span>, <span class="hljs-string">"=="</span>, <span class="hljs-string">"UvMIUnuxJ2h0E47fmZPpHLisHn12"</span>);
firebase
  .<span class="hljs-title function_">collection</span>(<span class="hljs-string">"boosts"</span>)
  .<span class="hljs-title function_">where</span>(<span class="hljs-string">"creatorID"</span>, <span class="hljs-string">"=="</span>, <span class="hljs-string">"UvMIUnuxJ2h0E47fmZPpHLisHn12"</span>);
</code></pre>
<p>sick. so it looks like arc stores some preferences in firestore, along with a basic user object, referrals and boosts</p>
<h2>what the hell are arc boosts</h2>
<p>arc boosts are a way for users to customize websites, by blocking elements, changing fonts, colors, and even using their own custom css and js.</p>
<p><strong>do you see where this is going?</strong>, so, i manually logged into my account using my dummy page to test firebase accounts, and executed the exact same query to get my boosts:</p>
<p><img src="/files/img/posts/arc/firebase-query-1.png" alt=""></p>
<p>cool, let me create a simple boost on google.com</p>
<p><img src="/files/img/posts/arc/firebase-query-2.png" alt=""></p>
<p>hey! theres our boost, lets try changing some parameters around.</p>
<p>i see that it queries by <code>creatorID</code>, and we cant <em>query</em> a different creator ID than the original, but what if we update our own boost to have another users id?</p>
<p>well, i tried it with another account of mine, and this way the result when i went to google.com on the other computer (the victim one)</p>
<p><img src="/files/img/posts/arc/alert-popup.png" alt=""></p>
<p><strong>what the fuck? it works?</strong></p>
<h3>quick recap</h3>
<ul>
<li>arc boosts can contain arbitrary javascript</li>
<li>arc boosts are stored in firestore</li>
<li>the arc browser gets which boosts to use via the <code>creatorID</code> field</li>
<li><strong>we can arbitrarily chage the <code>creatorID</code>&nbsp;field to any user id</strong></li>
</ul>
<p>thus, if we were to find a way to easily get someone elses user id, we would have a full attack chain</p>
<h2>getting another users id</h2>
<h3>user referrals</h3>
<p>when someone referrs you to arc, or you referr someone to arc, you automatically get their user id in the <code>user_referrals</code> table, which means you could just ask someone for their arc invite code and they'd likely give it</p>
<h3>published boosts</h3>
<p>you can share arc boosts (only if they don't have js in them) with other people, and arc has a <a href="https://arc.net/boosts">public site</a> with boosts, and boostSnapshots (published boosts) contain the user id of the creator.</p>
<h3>user easels</h3>
<p>arc has a feature called easels, which are basically whiteboards, you can share easels, and this also allows you to get someones user id.</p>
<h2>putting it together</h2>
<p>this would be the final attack chain:</p>
<ul>
<li>obtain the user id of the victim via one of the mentioned methods</li>
<li>create a malicious boost with whatever payload you want on your own account</li>
<li>update the boost <code>creatorID</code> field to the targets</li>
<li>whenever the victim visits the targeted website, they will get compromised</li>
</ul>
<p>the browser company normally does not do bug bounties, but for this catastrophic of a vuln, they decided to award me with <strong>$2,000</strong> USD</p>
<p>the timeline for the vulnerability:</p>
<ul>
<li><strong>aug 25 5:48pm</strong>: got initial contact over signal (encrypted) with arc co-founder hursh</li>
<li><strong>aug 25 6:02pm</strong>: vulnerability poc executed on hursh's arc account</li>
<li><strong>aug 25 6:13pm</strong>: added to slack channel after details disclosed over encrypted format</li>
<li><strong>aug 26 9:41pm</strong>: vulnerability patched, bounty awarded</li>
<li><strong>sep 6 7:49pm</strong>: cve assigned (CVE-2024-45489)</li>
</ul>
<h2>rce on priviliged pages</h2>
<p>while poking around, i saw that boosts actually execute for other protocols aswell (even though you cant create them in the client), so someone could create a boost targeting the page <code>settings</code>, and it would execute on <code>chrome://settings</code>, which allows further escalation of priviliges.</p>
<h2>privacy concerns</h2>
<p>while researching, i saw some data being sent over to the server, like this query everytime you visit a site:</p>
<pre><code class="language-js hljs language-javascript">firebase
  .<span class="hljs-title function_">collection</span>(<span class="hljs-string">"boosts"</span>)
  .<span class="hljs-title function_">where</span>(<span class="hljs-string">"creatorID"</span>, <span class="hljs-string">"=="</span>, <span class="hljs-string">"UvMIUnuxJ2h0E47fmZPpHLisHn12"</span>)
  .<span class="hljs-title function_">where</span>(<span class="hljs-string">"hostPattern"</span>, <span class="hljs-string">"=="</span>, <span class="hljs-string">"www.google.com"</span>);
</code></pre>
<p>the <code>hostPattern</code> being the site you visit, this is against <a href="https://arc.net/privacy">arc's privacy policy</a> which clearly states arc does not know which sites you visit.</p>
</div></article></div><footer><p>© 2024 xyzeva</p></footer></div><script src="/files/js/ko-fi.js"></script><script src="/files/js/kofi-init.js"></script><div id="kofi-widget-overlay-99c02322-e953-4c14-842e-94978fa55105"><div class="floatingchat-container-wrap" style="z-index: 10000;"><iframe class="floatingchat-container" style="" id="kofi-wo-containerkofi-widget-overlay-99c02322-e953-4c14-842e-94978fa55105"></iframe></div><div class="floatingchat-container-wrap-mobi" style="z-index: 10000;"><iframe class="floatingchat-container-mobi" style="" id="kofi-wo-container-mobikofi-widget-overlay-99c02322-e953-4c14-842e-94978fa55105"></iframe></div><div id="kofi-widget-overlay-99c02322-e953-4c14-842e-94978fa55105-kofi-popup-iframe" class="floating-chat-kofi-popup-iframe" style="z-index: 10000; height: 0px; width: 0px; opacity: 0; transition: all 0.6s ease 0s;"><div class="floating-chat-kofi-popup-iframe-notice"><a href="https://ko-fi.com/xyzeva" target="_blank" class="kfds-text-is-link-dark">ko-fi.com/xyzeva</a></div><div class="floating-chat-kofi-popup-iframe-closer"><span><svg height="0px" width="15px"><line x1="2" y1="8" x2="13" y2="18" style="stroke:#000; stroke-width:3"></line><line x1="13" y1="8" x2="2" y2="18" style="stroke:#000; stroke-width:3"></line></svg></span></div><div class="floating-chat-kofi-popup-iframe-container" id="kofi-widget-overlay-99c02322-e953-4c14-842e-94978fa55105-kofi-popup-iframepopup-iframe-container" style="height: 100%;"></div></div><div id="kofi-widget-overlay-99c02322-e953-4c14-842e-94978fa55105-kofi-popup-iframe-mobi" class="floating-chat-kofi-popup-iframe-mobi" style="z-index: 10000; height: 0px; width: 0px; opacity: 0; transition: all 0.6s ease 0s;"><div class="floating-chat-kofi-popup-iframe-notice-mobi"><a href="https://ko-fi.com/xyzeva" target="_blank" class="kfds-text-is-link-dark">ko-fi.com/xyzeva</a></div><div class="floating-chat-kofi-popup-iframe-closer-mobi"><span><svg height="0px" width="15px"><line x1="2" y1="8" x2="13" y2="18" style="stroke:#000; stroke-width:3"></line><line x1="13" y1="8" x2="2" y2="18" style="stroke:#000; stroke-width:3"></line></svg></span></div><div class="floating-chat-kofi-popup-iframe-container-mobi" id="kofi-widget-overlay-99c02322-e953-4c14-842e-94978fa55105-kofi-popup-iframe-mobipopup-iframe-container-mobi" style="height: 100%;"></div></div></div></body></html>