本文的目的是深層次的分析Flash的ExternalInterface的XSS漏洞,並提出比較有效的解決方案。
首先,我們看看什么情況下,會出現XSS。
第一種情況:
把flashvars傳入的參數(或者其他能被別人控制的方式)當ExternalInterface.call的第一個參數
package { import flash.display.Sprite; import flash.external.ExternalInterface; public class XSSTest extends Sprite { public function XSSTest() { var jsFunction:String = loaderInfo.parameters.jsFunction; var param:String = "abc"; ExternalInterface.call(jsFunction, param); } } }
注意,這里通過flashvars傳遞了一個參數,是js的函數。這種方式比較常見,swf可以做成通用,放到不同的業務中使用,每次只需要傳入對應的js函數即可。但是,這里就存在漏洞了。
在瀏覽器中,構造
url: XSSTest.swf?jsFunction=alert(/XSS/),訪問swf,並以get參數的形式傳入flashvars,結果,造成了
甚至更狠一點,
jsFunction=function(){alert(1);alert(2);}。。。
當然,這么惡作劇alert一下,貌似對小白用戶沒什么損失,但如果在function內調用這個域名的CGI,就能帶來很大的驚喜了~~~因為這里能獲取到對應的cookie,時間有限,具體攻擊的方式,這里不多說。
第二種情況:
把flashvars傳入的參數(或者其他能被別人控制的方式)當ExternalInterface.call的第二和第三個參數
這次,我們使用這段代碼:
public function XSSTest() { var param:String = loaderInfo.parameters.param; ExternalInterface.call("console.log", param); }
這個方式也許沒有這么簡單進行XSS,但對於黑客來說,還是有辦法的。
在IE8下調試模式下,我們可以看到ExternalInterface的代碼:

正常情況下,Flash player會生成這樣的代碼:
try { __flash__toXML(console.log("good" )) ; } catch (e) { "<undefined/>"; }
對比自己寫的as代碼和生成的這段js代碼,可以猜測,Flash player是以一種簡單的拼接字符串的方式實現的。
稍稍做個小把戲,結果就可以注入代碼執行了。

是不是很神奇?怎么做到的呢?為什么url稍稍變化可以達到這樣呢。我們看看現在的
js代碼:
try { __flash__toXML(console.log("\\" ));alert(/XSS/);}catch(e){} //")) ; } catch (e) { "<undefined/>"; }
正好跟原來的雙引號對上了,結果,最后的catch也被替換了。。。也就是說,黑客可以寫自己的函數了,想怎么執行都可以了。。。
至於為什么這里雙引號對上了,可以簡單猜測flash遇到字符串中有雙引號的時候,只是簡單的以 \" 方式打印成js代碼,但如果用戶再惡意拼一個\,就負負得正了。。。
(這里__flash__toXML的代碼並不是關鍵點了,所以將在文章最后再列出)
第三種情況:
沒有對swf Object的id沒有過濾
頁面加載Flash,我們需要設定Object或者embed的id,否則ExternalInterface會失效。而這個地方,也會被黑客利用。
我們看看實際執行的代碼:
try { document.getElementById("XSSTest" ).SetReturnValue(__flash__toXML(alert( null)) ); } catch (e) { document.getElementById("XSSTest" ).SetReturnValue("<undefined/>"); }
看到這里,應該發現跟上邊說的第二種情況很類似,黑客可以通過修改了Object id,惡意閉合雙引號,達到目的。
接下來,簡潔的總結一下怎么防XSS。
對於第一和第三種情況,我們應該對字符進行過濾,例如用以下的兩個函數:
public static function checkJsFunctionValid(functionName:String):Boolean { var reg:RegExp = /^[a-zA-Z0-9_\.]+$/; return reg.test(functionName); } public static function checkObjectIdValid():Boolean { if (ExternalInterface.available) { var objectId:String = ExternalInterface.objectID; if (!objectId || (objectId == objectId.replace(/[^0-9a-zA-Z_]/g , ""))) return true; else return false; } return true; }
對於第二種情況,我們應該盡量避免這樣跟js傳遞數據,但如果實在無法避免。可以用這樣的方式轉義字符串:
str.replace( /[\"\\]/g , function(d:String, b:*, c:*){ return '\\' + d.charCodeAt(0).toString(8); });
簡單解釋一下,這里把雙引號和反斜杠這樣比較敏感的字符,替換為轉義表示。再輸出成js代碼時,正好又還原回去了。
例如:
\\\"\\\"})));}catch(e){alert(/xss/);}//
變成了 \134\42\134\42})));}catch(e){alert(/xss/);}//
附帶額外的在IE8 開發工具中抓獲到的代碼:
function __flash__arrayToXML(obj) { var s = "<array>" ; for (var i=0; i<obj.length; i++) { s += "<property id=\"" + i + "\">" + __flash__toXML(obj[i]) + "</property>"; } return s+"</array>" ; } function __flash__argumentsToXML(obj,index) { var s = "<arguments>" ; for (var i=index; i<obj.length; i++) { s += __flash__toXML(obj[i]); } return s+"</arguments>" ; } function __flash__objectToXML(obj) { var s = "<object>" ; for (var prop in obj) { s += "<property id=\"" + prop + "\">" + __flash__toXML(obj[prop]) + "</property>" ; } return s+"</object>" ; } function __flash__escapeXML(s) { return s.replace(/&/g, "&" ).replace(/</g, "<").replace(/>/g, ">" ).replace(/"/g, "" ").replace(/'/g, "'"); } function __flash__toXML(value) { var type = typeof(value); if (type == "string" ) { return "<string>" + __flash__escapeXML(value) + "</string>"; } else if (type == "undefined") { return "<undefined/>" ; } else if (type == "number") { return "<number>" + value + "</number>"; } else if (value == null) { return "<null/>" ; } else if (type == "boolean") { return value ? "<true/>" : "<false/>"; } else if (value instanceof Date) { return "<date>" + value.getTime() + "</date>"; } else if (value instanceof Array) { return __flash__arrayToXML(value); } else if (type == "object") { return __flash__objectToXML(value); } else { return "<null/>" ; //??? } } function __flash__addCallback(instance, name) { instance[name] = function () { return eval(instance.CallFunction("<invoke name=\"" +name+"\" returntype=\"javascript\">" + __flash__argumentsToXML(arguments,0) + "</invoke>" )); } } function __flash__removeCallback(instance, name) { instance[name] = null; }