Bench Press: Leaking Text Nodes with CSS (2024)

gslin1 pts0 comments

Bench Press: Leaking Text Nodes with CSS

Home

Back to top

1. The Challenge<br>2. Current Techniques Won't Work<br>3. Prior Art: Leaking the Charset<br>4. Measuring Height using Animation Timelines<br>5. The Plan5.1. Unique Letter Heights<br>5.2. Iteratively Removing Letters<br>5.3. Calculating the Height Difference<br>5.4. Exfiltrating the Letters<br>5.5. Putting It All Together

6. Limitations6.1. Floating-Point Inaccuracies<br>6.2. Chrome-only (For Now)<br>6.3. Local Fonts<br>6.4. Speed

7. Summary<br>8. Final payload

Some time ago, while reading up on new CSS features, I asked myself: Is it possible to leak the entire content of an HTML text node only using CSS?

The answer is yes! Well, kinda. I found a technique that generally allows this, but bumps into the limitations of the CSS engine at some point 🙃 But I'm getting ahead of myself…

I really liked all the new things I learned about CSS while researching this, so I created a challenge for Hack.lu CTF 2024: Bench Press. In this blog post, I'm going to walk you through the solution as a practical example of my new technique.

The Challenge<br>The goal of the challenge is to leak an authentication token from a tag:

html><br>html><br>head>

script nonce="...">t="01b275146755aac26d5b2c7821b7c3"script>

The only interesting thing we can control is the ?theme= query parameter:

10<br>11<br>12<br>13<br>14<br>15<br>app.get('/articles/:slug', (req, res) => {<br>// ...<br>let theme = '';<br>if (typeof req.query.theme === 'string') {<br>theme = req.query.theme;<br>if (theme.includes(')) {<br>theme = '*{color:red}';

res.render('article', {<br>theme,<br>// ...<br>});<br>});

The parameter ends up inside a tag, giving us CSS Injection!

style>{{{theme}}}style>

Cross-checking this with the Content Security Policy (CSP) also looks promising:

style-src 'unsafe-inline'<br>we're big fans of inline styles here

img-src http: https:<br>remote images are welcome too

frame-ancestors 'none'<br>no framing

base-uri 'none'<br>no

form-action 'self'<br>no weird forms

default-src 'none'<br>no other stuff (read: no JS)

sandbox allow-forms<br>really, really no JS (missing allow-scripts)

Our situation now boils down to leaking the content of a text node using only inline styles and remote images.

Current Techniques Won't Work<br>In such a scenario, there are multiple known CSS Injection techniques that can help stealing data from the DOM. However, none of them fits due to their requirements and limitations. A few examples:

Recursive @import for incremental leaks doesn't work because we can't load remote styles.

Custom ligature fonts can't be used because remote fonts are blocked.

Using the "contains" attribute selector to leak data chunks doesn't apply because the token is not inside an attribute.

However, there are some techniques that leak the charset of a text node that would fit our conditions:

Using default fonts to cause size differences

Using default fonts to cause timing differences

Timing side-channels are generally slower and less reliable, so let's go with the size differences and see if we can refine the technique.

Prior Art: Leaking the Charset<br>To detect the characters present in a text node, the size difference technique iterates over all possible characters. For each of them, a specific font-face is applied that only matches when the current character is included. This is done using the unicode-range descriptor.

To detect if the current character is present, the CSS measures the height difference of the text node. If the character is present, then the font applies and makes the text node bigger. This height difference is detected using a scrollbar background image which is only loaded from remote if the scrollbar is shown.

The ::first-line pseudo selector is used to make only the first line of text visible (font-size: 30px) while making the rest of the text invisible (font-size: 0px). This avoids that the text wraps around and messes with the node's height that way.

But there's still a problem: If the text node contains duplicate characters, no additional font request is triggered for the second one. For example, if the text is LOL, the leak goes like this:

Make 1 char appear (L) and iterate over the possible character fonts<br>When checking if there is an L, the font applies and makes the text bigger<br>The height difference is detected and a request is sent (attacker.com/L)

Make 2 chars appear (LO) and iterate over the possible character fonts<br>When checking if there is an L, the font applies and makes the text bigger<br>No request (font already loaded)

When checking if there is an O, the font applies and makes the text bigger<br>Font is requested (attacker.com/O)

Make 3 chars appear (LOL) and iterate over the possible character fonts<br>When checking if there is an L, the font applies and makes the text bigger<br>No request (font already loaded)

When checking if there is an O, the font applies and makes the text bigger<br>No request (font already loaded)

Now we only know that the text begins with LO, and that there might be more Ls or more Os in there. This is quite...

text font theme using fonts node

Related Articles