I used Google Drawings and there’s no shame in that

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.

A typical CSS injection token exfil payload

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.

A typical CSS injection token exfil payload given the first char of the csrf token is ‘c’

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.