During the course of a web application test, occasionally our automated tools will miss a serious vulnerability. Cross-Site Request Forgery is one of these vulnerabilities that our scanners sometimes miss. You can read more about CSRF here. This is why it is so important for web application tests to perform manual analysis of code, which is a key component of our testing methodology at Anitian.
During a recent web application test, I discovered several CSRF vulnerabilities while conducting a manual review. I was able to chain a series of attacks together to perform a more serious attack. The following is a walkthrough of how these vulnerabilities were discovered and ultimately exploited to take control of a victim’s account. Please note, some content of this has been obfuscated to protect the client’s information.
A Tantalizing Discovery
The first step of this attack was to do some reconnaissance. I started by logging into the web application using a low-level account provided to me by the client. Most of the pages on this particular web site required prior knowledge of account names, ID numbers, etc. Since I didn’t have any of that on hand to begin with, the only real interesting web page was the account profile page. This page allows a user to update their account information. This includes first name, last name, email address, city, state, etc.
I chose Firefox as my browser because I like to use the Firefox plugin TamperData to manipulate form fields in order to see what interesting things I may find. Usually, I’ll throw some strange data in there such as quotation marks or asterisks in order to try various injection attacks. In this case, I noticed there was a hidden form field being transmitted along with the other account information. This form field was called “_RequestVerificationToken”. I attempted to manipulate this field, but it was not vulnerable to injection attacks as far as I could ascertain. I did notice that the web site didn’t seem to care if I manipulated this field, though.
This was strange to me. Based on the name of the field, I made a guess that it was being used as a security measure to verify that each request submitted was coming from the actual user of the website. The purpose of this is typically to prevent cross-site request forgery (CSRF) attacks. Since the web page did not seem to validate this token, it meant that the profile update form was possibly susceptible to CSRF.
I made a mental note of this and decided to test other parts of the web site. I figured that if one page was broken, the rest of the site was likely to be broken as well. Unfortunately for me, that was not the case. On any other page, I tested, manipulating the token would result in a server error and the form would not be processed. That meant I had actually stumbled upon the one broken page right out of the gate. The next step would be to figure out how to use this to do something more malicious.
This website, like many others, has a “forgot password” feature. The goal is of course to assist users who have forgotten their passwords. This automated system is very simple. The user enters their user name into a form field and clicks submit. If the user name is valid, the system generates a new random password for that user’s account and then sends the password to the email address associated with that user’s account. It’s very simple, but it would come in very handy with this hack.
I realized that if I could use the CSRF vulnerability to update a user’s email address to my own, I would then be able to use the forgotten password feature to have the system send the user’s password to me instead of the user themselves. Unfortunately, I ran into a problem pretty quickly.
The test account provided to me by the client (referred to as example.com) included an email address with the client’s domain name. Something like firstname.lastname@example.org. The system would allow me to change this to any email address that I wanted. The problem was that as soon as I told the system to email me a new password I would get a server error. For some reason, it only seemed to like email@example.com.
After a bit more manual testing, I discovered that it would accept any username ending with example.com. I wasn’t exactly sure why this was happening since other accounts in the system include email addresses from different domains. I assumed there must be some extra security verification step happening in the background that compares the email address’ domain name against a white list. My personal email address or domain was not on that list. That’s where the next bit of trickery came in.
Gmail to the Rescue
I had a hunch that the email address whitelisting step would be broken in some way, and I turned out to be correct. I discovered that any email address would work, so long as it included “example.com” somewhere in the name. It didn’t have to be the actual domain of the email address. For example, something like “firstname.lastname@example.org” would be accepted by the password emailing system. I just needed an email account with “example.com” in the name somewhere.
That’s where GMail comes in. GMail has a nifty feature that allows you to append a plus sign followed by any text after your GMail user name but before the @ symbol. For example, if your email address is email@example.com, you can also use firstname.lastname@example.org and you will receive emails just fine to your email@example.com address. Gmail actually ignores everything between the plus sign and the @ symbol.
With that in mind, I logged into the web application for a test and tried updating the account’s email address to “firstname.lastname@example.org”. Sure enough, this worked and the password reset function sent me a new password. With this working, it was time to make a proof of concept web form to exploit this CSRF vulnerability.
A Simple Web Form
Since this vulnerable application submits data using the POST method, I had to build my own web form to do my bidding. Since this was a proof of concept, I didn’t go out of my way to make it pretty. The form I built includes hidden text fields for all of the form items that area normally transmitted to the web application when a profile is updated. This included the CSRF token, though in this case, the value of the token does not matter since the application fails to verify it. The most critical piece was the EmailAddress field. I left the EmailAddress field hidden, but included a default value of “email@example.com”.
<form action="https://www.example.com/account/EditProfile/" id="EditProfile" method="post" name="EditProfile"> <input name="__RequestVerificationToken" type="hidden" value="ANYTHING CAN GO HERE" /> <input id="PasswordChgRequired" name="PasswordChgRequired" type="hidden" value="No" /> <input id="UsersName" name="UsersName" type="hidden" value="ANITIAN" /> <input id="returnURL" type="hidden" /> <input class="userName text-box" id="FirstName" maxlength="25" name="FirstName" style="width:160px" type="hidden" value="Anitian" /> <input class="userName text-box" id="LastName" maxlength="25" name="LastName" style="width:160px" type="hidden" value="Anitian" /> <input id="Phone" name="Phone" style="width:160px" type="hidden" value="" /> <input id="Phone" name="Phone" type="hidden" value="" /> <input id="PhoneExt" maxlength="5" name="PhoneExt" style="width:160px" title="1 to 5 numbers. " type="hidden" value=" " /> <input id="PhoneExt" name="PhoneExt" type="hidden" value=" " /> <input class="text-box" id="City" maxlength="20" name="City" placeholder="Office Location" style="width:160px" title="Enter the city of your office location." type="hidden" value="Winterville" /> <input id="EmailAddress" maxlength="50" name="EmailAddress" style="width:394px" title="Email is limited to 50 characters." type="hidden" value="firstname.lastname@example.org" /> <input class="jobFunc" id="JobFunction2" name="JobFunction2" type="hidden" value="Vice President" /> <input class="jobFunc" id="JubFunction" name="JobFunction" type="hidden" value="ACCOUNT HACKED"> Click this awesome button! <input id="submit" type="submit" value="Save" /> </form>
The only visible piece of the form was a Submit button. For the proof of concept, I loaded the new web page on my local machine. A real attacker would host this on a web server somewhere on the Internet, most likely a compromised system.
It’s simple and not at all subtle, but it works as a proof of concept. Now I had to play the role of the victim. To do this, I first opened up Firefox and logged into the web application with my normal account as if I were a normal user. I then loaded up the malicious form web page in another tab. The attacker would likely have sent this link to me in an email. I then clicked the button, and it worked as expected.
The malicious web page submitted all of the information to the target site and updated the email address field accordingly. It should be noted that this attack would have worked even if the target web page was closed in the victim’s browser. All that is necessary is for the user’s session to still be open. Often times a session can stay open for a long time even after the web page is closed.
At this point, the only thing left to do as the attacker was to go use the password reset function to email myself the victim’s new password.
At that point, the victim’s account was completely under my control.
Let’s Improve the Attack
The other problem is that you have to know the victim’s username. You can’t just target any random user with this. You need to know the victim’s username in order to utilize the forgotten password function. Luckily, I discovered another trick that can get around this problem.
Secret Form Fields
I noticed that the profile update web page also submits a field for UsersName. I thought this was interesting because on the web page itself you cannot modify the username field. It’s a static entry that stays the same no matter what. Out of curiosity I decided to try modifying this variable using Tamper Data once again. To my surprise, it actually worked. The web site actually accepted my modified username value and changed the account’s user name. It would not allow me to update the username to one that already exists, but it’s easy enough to choose something random that would be unlikely to be taken.
To take advantage of this, I can simply update my malicious web form to include a new, currently unused, value for the username field. Now when the victim clicks on the Submit button not only does their email address get updated, but their username is changed to a value that I already know ahead of time. This means that the link can be sent to any person who is logged into the website and their account can be stolen. This process can even be scripted so that a different form is emailed out in bulk to many different people. They can all be compromised with just one click.
How could this attack have been prevented? There were really two core vulnerabilities that allowed this attack to occur, with a fourth providing extra assistance. Let’s break them down and see what could have been done to prevent each piece from succeeding.
The CSRF attack was the worst problem. Without this vulnerability, the whole thing would have been dead before it even got started. OWASP recommends several different methods for protecting against CSRF attacks, but the safest method is to use a unique token in a hidden field. That is what this example web site attempted to do. If properly implemented, this should drastically decrease the chance of a CSRF attack from being successful. There just happened to be one single web page that did not properly validate this token. That’s all it takes is one tiny hole, and remember that the bad guys have all the time in the world to find it.
The next issue was with user input validation. It appeared as though the system attempted to verify email addresses against some sort of whitelist, but the method was faulty. It checked for the presence of a whitelisted domain name anywhere in the email address. Instead, it should have focused on the last section of the email address, or the entire email address. Another, safer option would be to require the user to re-enter their current password in order to update their profile information. A CAPTCHA could be used in place of the password re-entry as well since a malicious web form would be unable to decipher the CAPTCHA automatically.
The final vulnerability also involved user input validation. Based on the design of the profile update page, it seems as though the web page does not intend for the user to update their username. The UsersName field is not available for editing in the original form. If this is the case, the form should not even submit the username as a field. If it needs to be submitted for some reason, then the web page should be re-written to disallow editing of the username.
CSRF attacks are one of the many ways attackers can gain access to your data. However, merely scanning sites for vulnerabilities is not enough. It takes careful review of how the site works to detect more subtle flaws. This was a case where we were able to detect this flaw and help our client defend themselves against what could have led to a devastating data breach.