Bypassing CSRF token protection by abusing a misconfigured CORS policy

Lihaft
13 min readMar 10, 2022

So, today I am going to teach you about a cool and interesting way of bypassing the token protection used against CSRF attacks by finding misconfigured CORS policies. This, can be useful if you are a Bug Bounty Hunter, Pentester or maybe just a Developer looking to write more secure code (I respect you!).

I will be using a Pentesterlab Pro ISO file and environment that you don’t need to have to follow this writeup. I won’t actually follow the room as it’s intended to be done (the ISO is buggy actually), I will just use this lab environment to make it simpler to understand and to practice this bug, but I will detail everything you need to know to perform this attack in real websites.
(If you have a Pro subscription you can download the ISO by clicking here and learn how to install the ISO in VirtualBox by clicking here).

OK. Let’s begin.

CORS

Brief explanation of SOP, CORS and CSRF (skip if you know already)

If we want to understand this attack we need to have knowledge first about the Same Origin Policy.

Same-Origin Policy (SOP)

This is the fundamental security model of the web: 2 web pages from different sources should not be allowed to interfere with each other.

SOP prevents:

  • JavaScript on Site A from reading the cookies of Site B.
  • JavaScript on Site A from reading the content of Site B.

To further understand this, we need to think as the web as an operating system:
- An origin is analogous to an OS process
- The web browser itself is analogous to an OS kernel
- Sites rely on the browser to enforce all the system’s security rules, therefore just like operating systems, if there is a bug in the browser itself then all these rules go out the window.

If we understand the above, we can understand the basic rule for the Same-Origin Policy is:
Given 2 separate Javascript execution contexts, one should be able to access the other only if the protocols, hostname and port numbers associated with their host documents match exactly. This is called ORIGIN.

Example URL:

https://example.com:4000/a/b.html?user=Alice&year=2019#part2

Protocol (https) — Hostname (example.com) — Port (4000) — Path (/a/b.html) — Query (?user=Alice&year=2019) — Fragment (#part2)

SO here we only care about the protocol, hostname, and port i.e. ORIGIN. For this case the Origin is: https://example.com:4000

You can also understand this better with curl .

If you curl a website you will get the response for you and no cookies will be sent back because you are not in a browser, but if for example you fetch()it with Javascript from another website in the browser (e.g. using your browser’s Console) you won’t get the website back because the browser will try to attach the cookies and data and you will be blocked by CORS. That would be insecure, websites would easily be able to access your cookies/data of other sites. This can be DEACTIVATED with 2 special headers that will allow credentials to be sent: Access-Control-Allow-Originand Access-Control-Allow-Credentials: true which is our next topic: Cross-Origin Resource Sharing.

Note: XMLHttpRequest and the Fetch API follow the same-origin policy too

Note 2: SOP is a broad topic so I encourage you to read more about this here: https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy

Cross-Origin Resource Sharing (CORS)

This is a browser mechanism which enables controlled access to resources located outside of a given domain. It extends and adds flexibility to the Same-Origin policy (SOP). However, it also provides potential for cross-domain attacks if a website’s CORS policy is poorly configured and implemented.

CORS will use HTTP headers to allow or deny a request and define trusted web origins and associated properties such as whether authenticated access is permitted. The browser will send a request with an Origin header and the domain where the request originated as its value and the server will decide whether it allows this request or not.

The server can allow the browser to make CORS requests by responding with the header Access-Control-Allow-Origin: and a list of hostnames. By default, the browser will send unauthenticated requests unless the server sets the header Access-Control-Allow-Credentialto true.

Note: CORS is not a protection against cross-origin attacks such as CSRF and it may actually increase the possibility of CSRF attacks or exarcebate their impact as we will see later.

Cross-Site Request Forgery (CSRF)

Attack which forces an end user to execute unwanted actions on a web app in which they’re currently authenticated. This attack can force a user to perform requests like transferring funds, changing email address, run commands in the server (if admin), etc by just visiting a malicious site with some malicious code on it (if the vulnerability is present in the target site). It is effective when the attacker can’t read the HTTP response.

Websites that try to mitigate CSRF attacks will use CSRF Tokens but if tokens are not well implemented or a misconfigured CORS policy is being used then this protection can easily be bypassed!

For a CSRF attack to be possible we need:

  • A relevant action
  • Cookie-based session handling (no other auth method is used so cookies are sent)
  • No unpredictable request parameters: the attacker doesn’t have to guess any values. For example, when causing a user to change its password, the function is not vulnerable if an attacker needs to know the value of the current password. (A valid action would be changing the account’s email to one that the attacker controls).

Example attack: A bank is vulnerable to a CSRF attack in a Transfer Funds page. Attacker sends victim a malicious link where the main page of this site contains a simple script with a POST request to transfer funds to his own account.
Victim while being logged in visits this malicious site and without even noticing anything strange, his account balance is drained! Attacker has stolen the victim’s funds because the site did not have any protection to CSRF attacks.

Victim must be logged in in the site we want to attack to make this work!

Understanding our target before the attack

Ok, so now that we know the basic concepts we can get into the juicy part! As I said before, I will be using a Pentesterlab environment just for demonstration purposes but you don’t need to do anything! The site will be running in my local network in 192.168.0.13 and as you will see later I will host my own “site” in 192.168.0.4.

The site I am going to use is simple: we can register, log in, log out and change our password. That’s it. The functionality that we want to focus on is Change Password because of how sensitive it is and how dangerous it could be if exploited. In real life, any sensitive action with a CSRF token and a misconfigured CORS setup would be a similar exploit scenario!

main page

I created a user with username:user and password:user

Registration as user/user

And then I logged in (see the request below. Nothing important for now):

Login request. Not important

Great! We have logged in. What’s next?

Logged in and Change Password page

As we can see, the main page after we log in is the Change Password page. Here, we will notice that when we change our password (user123 will be the new pass), there is a token sent with each request. This token is the CSRF token to prevent CSRF attacks.

You can see that this token is embedded in the HTML form as a hidden input:

CSRF token in html form

And then included in the request (pay attention to the Origin header too):

Change password request

Now, in this site, the response we get back after changing our password is a 200 which also includes some CORS headers (see below image). We can see here that the domain that was included in the Origin is reflected back in the Access-Control-Allow-Origin header with Access-Control-Allow-Credentials:true included. This means that the site permits access to the response to the domain that matches this header (The browser will allow code running from the website of the Origin header to access the response of the target website if origins match.)

Change password response

Well, nothing seems strange right? We are executing an action from the same site (192.168.0.13) and CORS allows it. But, what if we try to change the Origin header value to a domain that we as attackers control? I will repeat the request with a different password but changing the value of the Origin header to some dummy host (192.168.0.123):

Change password request testing CORS vuln
Change password response testing CORS vuln

Voila! It is accepting any host in the Origin header and it is also allowing credentials. What does this mean? This means that from another domain that we control, we can access the response. So, if we can access the response, we can read the CSRF token that we previously saw it was embedded in the HTML form and use this token to perform a CSRF attack by including it inside our malicious password change request (NOTE: victim must visit our site while being logged in).

Wait, wait, wait… How the heck do we even do that? I’ll show you :)

The Attack

Because I am working in my local network, I will first get my local IP address. You can get it on Windows by opening cmd and executing ipconfig to then look for your private IP address. I am working with Linux so I can just type ip a to find it in my wlps40 network interface →192.168.0.4.

Excellent. We have our private IP which would be the equivalent of having a website (you can actually use one to make the attack more realistic). Now, just like every CSRF attack we need to write some code and host it in our site. I will run python3 -m http.server 80 inside a folder where I will have a HTML file called malicious.html that will contain our code but this could be hosted in a website that you control in real life scenarios.

For this attack, we first need to find a way of stealing the CSRF token of the victim. We know that the token is embedded in the HTML form and because of the CORS policy misconfiguration, we can access the response from our domain. Thus, we need to identify the name or ID of the tag where the token is stored and just grab it.

I showed you in the previous section a screenshot of how the token was stored inside a HTML form. It looked something like this tag:

<input type="hidden" name="token" value="xxxxxxxxx" />

So, in order to retrieve the victim’s token we need to send a cross-site request with AJAX to the vulnerable site, look for the token in the DOM, grab it, and then assign its value to a variable so we can use it later in the CSRF attack.

Exploit: Part 1

We first create a XMLHttpRequestobject and a token variable with the prepended value of “token=” because as we will see later, the CSRF attack will make a POST request from a form and forms follow a special body format. More on this in a few moments.

Afterwards, we set the response type for the request to document . This is important considering that we want to interact with the DOM to get the CSRF token value without much hassle. The object returned will be a HTMLDocument which is like an alias for Document.

Then, we create the GET request to the vulnerable site (192.168.0.13/index.php), we specify withCredentials=true which tells the browser that we want to do a cross-site request using credentials (such as cookies, authorization headers or TLS client certificates) and we send the request using send() .

We also have to specify what will happen after we get the response with onload() so we can extract the token and do the password reset attack after this. Thus, we get the response and store it in a variable and since we are interacting with a DOM we can use the classic JS functions to get the value of the inputtag with querySelector() by specifying the name of the element. Finally, we append at the end of the token string we previously had the CSRF token.

Remember, we are allowed to make a cross-site request and steal the victim’s CSRF token thanks to the misconfigured CORS policy. Otherwise, this would not be possible.

(If you want to learn more about getting the value of an input field like I did read this answer: https://stackoverflow.com/questions/7609130/set-the-value-of-an-input-field/64918427?noredirect=1#comment119362498_64918427)

Exploit: Part 2

Great, now we got the first part completed. The last thing we need is the CSRF password reset function to change the victim’s password and try our attack. Below, you will find the complete exploit code for the whole attack with the second part that I will explain now.

As you can see, we now got a passwordReset() function included after the logic of the onload() code. This function is what will execute the CSRF attack with the CSRF token included and the password pwned123 (it could be any value though). I also added a sarcastic friendly message to our victim :)

The passwordReset() function starts by creating an XMLHttpRequest object to make an AJAX POST request to the vulnerable site and setting withCredentials=true like we did in part 1.

Afterwards, we set the Content-Type request header to application/x-www-form-urlencoded because as I mentioned before, HTML forms have a special body format. This format is understood by servers by setting this header and it basically says that we will send a list of name/value pairs separated by an ampersand & and names separated from values with an equal sign = like : param=123&otherParam=999 . That’s it!

Finally, we send the request with this format by including the CSRF token stored in the token variable and concatenating this with the password and password_again fields that we can find in the HTML form (you can press F12 or right-click and Inspect to see the name of these inputs).

Cool, we are done. Let’s try it!

Wrapping everything up!

First, I will log into the vulnerable site again as we previously did with my user user and the password user987 .

Now, my malicious site is hosted on my computer so we only need open a Python server like I explained inside the folder where we have the HTML file and visit our private IP address + the name of the file like this:

Malicious.html

When we visit this malicious site while being logged into the other website, 3 requests are fired:

The first request is the GET to our malicious.html file. Nothing special here.

Complete attack requests

The second request is the cross-site GET to index.php which is the main page with the Password Change form and the CSRF token embedded into it. Our malicious script will make the request and then grab the CSRF token to store it inside the token variable: (see the Origin header)

2nd request. CSRF Token extraction request
2nd Request Response. CSRF Token extraction response

We can see that the in the Access-Control-Allow-Origin header the Origin was reflected and our malicious site is allowed to read the response. (Note that Access-Control-Allow-Credentials is set to true too).

To finish everything up, our script will call our final function passwordReset() and make a POST request to / .

Third request:

3rd Request. Actual CSRF attack with the token included and the password we chose!! ACCOUNT TAKEOVER

This final request includes the stolen CSRF token and the password we chose which is genuine proof that the attack worked successfully.

Boom! We got an Account takeover. All this happened in seconds and the victim that visited our site would not be able to tell that something malicious occurred.
Now we can log in using his/her username user and the password we chose pwned123 to test that everything worked right.

Final words

I hope that you learned something interesting and useful today. Developers make mistakes everyday and bugs will always exist. It is a matter of looking at the right place and understanding what the application is doing and what did the developers intend to do when coding it.

I encourage you to learn more about the Same-Origin Policy and CORS as these are pivotal topics of how the Internet works that will boost your hacker skills and career.

If you liked this article why not share it so others can learn too?
Also, you can ask me any question in the comments and I will happily answer it. I’ll also accept any feedback!

You can support me by following on Medium and liking/sharing this article :D
Thanks for reading!

Twitter @LihaftSechttps://twitter.com/LihaftSec

Roberto Ffrench-Davis (a.k.a. @Lihaft)

--

--