This is a story about how I (re)discovered an exploitation technique and took a bug with fairly limited impact to a 5 digit bounty by bypassing existing mitigations.
A Curious Case of HTML Injection
André Baptista and Cache-Money were working on a very strange bug. It started off as a simple character-set bypass and through a crazy series of steps evolved into HTML injection somewhere else in the target (not full-blown XSS, though, due to DomPurify). It was a cool chain and they could tell they were onto something big, but the next step was giving them a fair bit of trouble.
After hearing about the progress they made, I jumped in to see if I could offer any assistance in escalating this bug. At this point, we determined the worst we could do was a fairly effective clickjacking attack. 0xACB (Andre) and Cache-Money had a beautiful idea on how to chain a couple more attacks together to potentially achieve a high impact vuln, but I had different ideas.
After we determined that DomPurify allowed style
tags by default, I became fairly interested in how we could leverage CSS to do more than just manipulate the DOM. If you’ve read some posts of mine already, you’ll know that I’m no stranger to CSS injection exploitation. Unfortunately, this page both had frame protection and required some user interaction to trigger the injection. It seemed that if we wanted to exfiltrate something sensitive it would take a long time (over the course of many days) to perform the leakage. Of course this is a fairly weak, error-prone attack and would, at best, yield a low 4 digit bounty on this target.
I needed a way to get the browser, without reloading, iframes, or additional user interaction, to reevaluate multiple CSS payloads to make an attack like this work. Additionally, we had limitations on the length of the payload we could inject setting us back even further. It didn’t really seem possible to exploit this using just a <style>
tag… until I started thinking about interesting CSS features, namely @import
.
A Primer on CSS Injection
Before I get into the meat of the technique, I want to leave a short section here to describe the traditional token exfiltration technique used in CSS injection. If you’re already familiar, feel free to skip ahead to the next section. Also, I go into more depth on the technique in a previous blog post.
The traditional CSS injection token exfiltration technique relies on a feature of CSS called Attribute Selectors. Attribute selectors allow a developer to specify that a particular style should only apply to an element if an attribute of that element meets the condition indicated by the selector.
We can leverage the attribute selector to create rules that only apply to sensitive elements on the page given certain conditions. Then we can use properties like background-image
to make the browser call out to an attacker controlled system when these styles apply. This allows us to establish a feedback loop that drives the token exfiltration.
In this example, we tell the browser that “if the CSRF token starts with an a
then set the background-image
to be the image found at https://attacker.com/exfil/a
”. We then repeat this rule for every possible character the token could start with (a, b, c, .. 7, 8, 9, etc.).
Once we learn what the first character of the token is we can perform the attack again (usually using iframes) but with a slightly modified payload.
In this example, we assume that the first token of the CSRF token is a c
. In this way, we’re able to determine the second character of the CSRF token by rerunning the previous rule but with all tokens prepended with a c
.
Prerequisites
The aforementioned technique generally requires 3 things to be true:
- The CSS injection needs to allow for sufficiently long payloads
- Ability to frame the page to trigger CSS re-evaluation of newly generated payloads
- Ability to use externally hosted images (could be blocked by CSP)
This means that the previous technique might not be applicable if the injection doesn’t allow for sufficiently sized payloads or if the page cannot be framed. In our case, that meant we were unable to use this technique due to both the presence of framing mitigations and the limited number of characters we could actually inject.
@import
to the Rescue
Many programming languages have the ability to import code from other source files. CSS is no exception. While many might only be aware of <link href="..." rel="stylesheet">
, CSS itself actually has a way to perform a similar (but different) type of stylesheet inclusion using an at-rule called @import
.
@import
, in most cases, performs a direct in-place swap of the fetched styles into the current stylesheet. This allows a developer to pull in external styles and simultaneously override any undesirable directives defined in the external resource below the @import
line.
An interesting side-effect of how this feature is implemented in some browsers (namely Chrome) is that the external resource can be fetched in parallel to the browser also processing the rest of the stylesheet. My understanding is that this behavior increases TTI of the page while attempting to mitigate “flashes of unstyled content” (see: FOUC problem) but it actually has practical use in exploitation.
Imagine for a moment we had a webpage containing the following stylesheet:
@import url(http://laggysite.com/base.css);* { color: red; }
Chrome processes this stylesheet in 3 steps:
- Issue a request to
http://laggysite.com/base.css
- Evaluate the remaining directives (apply
* { color: red; }
) - When
http://laggysite.com/base.css
returns, substitute the response into the stylesheet and reevaluate the stylesheet.
We could leverage this browser behavior of re-evaluating stylesheets when @import
targets respond simulating the same “ability to control reevaluation of CSS” behavior that we needed from iframes in the old technique. The only requirement we have to use @import
is we must have control at the start of a style
tag (which we have since this is HTML injection).
To do this, we’ll create a couple of @import
rules in our stylesheet and let our server keep all of the connections open. Then we’ll use a standard CSS injection token exfil payload to pull out the first token from our target attribute. After our server receives this token from the background
style, we can generate the next token exfil payload and respond to the next pending @import
rule using the newly minted payload.
To make it all nice and neat, we can kick everything off with a lone, initial @import
rule (also helping us accomplish our goal of limiting the size of the payload).
To prove that this attack could work, I went about creating a tool I call sic
(which stands for Sequential Import Chaining). The name inspired by the sequential “1 by 1” nature of the @import
rules being chained together to cause controlled reevaluation of the CSS.
You can see an example of a vanilla HTML injection being leveraged with sic
to exfil an href
on the page with a single page load below. In a more practical example, an attacker would pull something more sensitive such as a CSRF token in the DOM using the value
attribute.
The code for the tool is here:
https://github.com/d0nutptr/sic
With this new technique, we were now able to demonstrate how a victim would, with a single click, leak sensitive information off of the current page increasing the bounty on the report tremendously.
A Rediscovery
Well you can imagine my surprise as I watched my tool (that I spent hours working on) finally succeeded in its task…
All of the hackers I showed this to had never seen something like this before reaffirming my conviction that I was first to this new technique. It wasn’t until I shared the concept with EdOverflow that he pointed me to Vila, who actually came up with a variation of this technique roughly half a year ago.
Funny enough, I tried to build a PoC similar to Vila’s PoC but was unable to figure out how to chain the stylesheets recursively while simultaneously getting the browser to respect the newly injected styles. The recursive method requires that you add “additional specificity” to each newly added rules otherwise the browser will only respect the last applicable styles in the stylesheet (which also happen to be the first styles injected into the page). This is why I came up with a sequential technique which doesn’t require additional specificity. Nonetheless, both techniques use the same core principles.
Potential Applications
There are a number of places where this could be applicable. Outside of the obvious case of HTML injection in web applications, I’ve done some attempts at poking around with email clients to see if any of them exhibited similar behavior to Chrome; however, an interesting application suggested by Vila is in electron applications.
“…this approach can be really useful for HTML injections in Electron apps (attackers usually don’t have iframes or control over navigation)” — Pepe Vila
Lastly, I’m curious about the implications of server-side rendering. I can imagine a case of, perhaps, PDF generation where both SSRF and javascript execution are mitigated. This could potentially allow an attacker to leak contents injected into this document efficiently.
Thanks
Huge shoutout to the following people for help in one way or another along this process: