An Introduction to CSRF Attacks

by Paradox

There was a time (not that long ago) that Cross-Site Scripting (XSS) attacks were relatively unknown.

Web developers could be excused for not properly sanitizing inputs.  Fortunately, that time has long since passed.  There is no excuse anymore for writing code that is vulnerable to XSS attacks (at least the basic ones).  The information is out there and, I dare say, average coders are hearing about it.  Microsoft's ASP.NET platform even includes XSS prevention support!

Unfortunately, with XSS taking the spotlight, developers feel like they are writing secure code when it is merely XSS-resistant.  Other attacks still remain less well known outside of the security community.

One of the prime examples of this is the Cross-Site Request Forgery (CSRF) attack.

All is not lost, however; plenty of material exists to teach you defenses.  I figure the best way to learn is from a real-life example.  The following is a learning text based on an actual vulnerability in a real website with a working proof of concept: a CSRF worm that steals account credentials!

A bit of explanation is probably in order at this point, as you try to comprehend an admission to writing code of the nature described above.  The proof of concept was very carefully neutered and, when the attack was proven feasible, the administrator of the site was notified in a manner upholding the tenets of responsible disclosure.  The hole has since been patched, and I would like to commend the owner on his prompt and courteous dealings with me.  Let me reiterate: this worm never spread past my accounts.

So first, the concept of the exploit.  The basic idea of a CSRF attack is that it is possible to force authenticated users to perform actions in an automated fashion without being authenticated yourself.

The first example usually given to describe CSRF is the idea of a server-side script that performs an action of some sort when it receives a GET request from the user.

For example, imagine you had written a website with a members only section.  Naturally you would need a way for authenticated users to log out.  A popular approach is to have a logout.php script that, when loaded by the user, logs him out.

The problem with this approach is that it is probably performing an action when the user GETs the relevant script without verifying that it was the user himself that sent the GET request.

This might seem strange at first, but think about how images are loaded for an <img> tag.

Via a GET request the browser performs, right?  Have you ever had to click a box to allow an image to be loaded?  I think it's safe to say no!  Can you imagine having to allow every image on the page, one at a time?  So your browser already makes GET requests on your behalf without asking.  Surprising?

The tricky bit is that you should now consider that other people are invoking these GET requests when they embed images and things of that nature.  Not only that, but they control the destination for your GET request based on the address of the resource!

The impact of this immediately becomes clear when you think of an image tag that, instead of pointing to a JPG or a GIF, points to http://yoursite.com/logout.php.

Anyone that loads that "image" tag would have a GET request sent to logout.php at yoursite.com.  If that person happens to be logged in at yoursite.com, then his cookie would be dutifully passed along with the GET.  What do you think would happen then?

It's easy to dismiss this example.

For one thing, it's against web development best practices to perform any action on a GET request.  It's bad form!  Unfortunately, this line of thinking is eliminated when you realize that POST based forms are just as vulnerable!

It's not immediately clear how this could be the case.  You can't easily force a POST request on behalf of the user.  The browser never does this automatically for things like images or other HTML elements, right?  It's true, POSTs don't usually happen automatically.  When paired with JavaScript, however, it's trivial to submit a POST-based form automatically.

You might think that by preventing XSS you would prevent such JavaScript from being executed and submitting the form.  This is also true!

The problem is that the vast majority of server-side scripts will gladly accept a POST from outside of their domain.  The script probably has no idea where the POST came from!  This is a feature of the web; it allows sites to perform API requests across domains.

So, if we merely create a website on our own server that has a form we want to post on behalf of the user and some JavaScript to do the posting, we just have to lure an authenticated user to the site.  The JavaScript will execute and the form will post to the action located on the target server, using the credentials of the authenticated user.

Victory is ours!

So with that theory in mind, onto the real deal!

The first file: news.php is the meat of the exploit.

It contains a clever way to convince a target that he isn't being tricked.  It decodes a parameter to the script that is Base64 encoded to be non-obvious.  It then creates an <iframe> that loads that Base64 decoded string as the target URL.

The beauty of this is that it allows us to to convince the user that he is viewing a regular website while our exploit code submits the form.  It makes luring someone to the site all that much easier!  Simply Base64 encode something like http://www.google.com and pass that as a parameter to the news.php link you distribute.

news.php also contains a JavaScript section that creates a function crossDomainPost() that embeds an <iframe> (created with form_writer.php) that will submit, via POST, the data contained in the last argument to crossDomainPost().

This allows the one script to quickly perform three POSTs to the server.

The first post leaves a tracking comment in my inbox.

The second POST sets the target account's registered e-mail to one under my control.  This allowed me to invoke the password reset function and have the "forgotten" password sent clear text and unhashed to my e-mail.

The third and final POST is the fun one... It forces the user to update his "status" with a link pointing back to the exploit.

So when a user is exploited he advertises the exploit to his friends, who are also likely to then be exploited.  You can see how quickly that could spread if it were left unchecked.

The beauty of using <iframe> to contain the form submit and JavaScript is that, by making them 1x1 in size, the user never sees the response sent for the POST.  It gets loaded into the tiny <iframe> and is effectively hidden.

So after seeing how easy it is to create a password stealing web worm, I'm sure you are eager to learn how to prevent it.  It's not really that hard.  The basic idea is that you need your scripts to verify that the person they authenticated is the person submitting data, and only when you are expecting it.

The traditional way to do this is to embed a "secret" inside every form you present to the user.  The server-side processing for that script should then only perform an action when it receives that secret.

To make this work, it's vital that the secret changes for every request.  If you can predict the secret, then you can exploit the script.

If the secret is random, then the only way to exploit the script is via an XSS attack that lets you first gain access to the secret.

It's a bit tedious, but most good web frameworks, such as Struts or CakePHP, can automate this process for you.  Don't be fooled into thinking that merely checking the referer header on every POST is good enough; with Flash and other exploits it can be possible to fake a referer.

I'd also like to point you to: w-shadow.com/blog/2008/11/20/cross-domain-post-with-javascript

form_writer.php and the crossDomainPost() function were taken from that blog post.

I've modified it into a specific exploit for the purpose of this article and integrated the aforementioned trick, to make it less obvious.  No point in reinventing the wheel, after all.  :)

Until next time!  Be safe, and practice responsible disclosure!

Return to $2600 Index