Remote Code Execution in Gitlab vscode extension when checking out merge request

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 #1924917 by inspector-ambitious on 2023-03-30, assigned to @fvpotvin:

Report | Attachments | How To Reproduce

Report

Summary

I believe I found a vulnerability that allow an attacker to create a malicious Merge Request, that when checked out in gitlab-vscode-extension leads to a remote code execution on the local machine of the victim.

In the exploitation demonstration of this attack I will use 2 vulnerabilities and chain them:

  1. Forge a merge request which hides malicious content. (optional)
  2. A way to automatically execute code on checkout.

Vulnerability 1: Hide malicious content

Using non printable characters U+FEFF (zero widh no-break space) it is possible to create a branch which is displayed the exact same way as the default branch in Gitlab.com.
As you can see below it seems we have 2 branches called main.

image.png

image.png

We see that the 2 branches are labelled differently (default, protected...)
However what is particularly interesting about the Merge Request UI, is that it's really impossible to distinguish if the MR target the real default branch or not.

image.png

image.png

So if someone creates a Merge Request targeting our fake mainU+FEFF branch it will look like the MR is targeted at the real main branch.
Also when we inspect the Merge Request in Gitlab Workflow Extension there is no information about the target branch, which even make this first vulnerability optional... But we will talk about this more in details in the demonstration of the exploit.

Vulnerability 2: Remote code execution in VSCode on git checkout

To exploit this vulnerability we're going to use .gitmodules and bare repositories.

A bare repository is different from a regular repository, instead of storing all of the housekeeping information (including config) in the .git subdirectory of the repository, a bare repository stores all of those files directly in the directory where the repository is created.
Bare repositories don't have a "work tree" so many git commands will fail (for example git status).
But this can be easily solved if we modify the config file with the following.

[core]  
        repositoryformatversion = 0  
        filemode = true  
        bare = false  
        worktree = "./"  

As you may have noticed bare is set to false and we defined the worktree to the root of the repository.
Using this configuration we get a "fake" bare git repository.
Still it's not malicious yet, but if we add the following fsmonitor config

fsmonitor = "C:\\\\Windows\\\\System32\\\\calc.exe"  

executing any git commands relying on fsmonitor in the bare repository will immediately execute calc.exe on windows.
(Please note that all operating system are impacted but for demonstration purpose I only focused on windows, for example on OSX or linux you could use the command touch /tmp/p0wned)

At this stage we have a malicious bare repository, however if we just embed this bare repository inside another git repo, no code will be executed because VSCode doesn't have a reason to execute a git command into that folder.

But if we create a .gitmodules that point to the folder that contains the bare repository, the command in fsmonitor will be executed at checkout !

Let's assume that the malicious bare repository is inside a folder called bare.
If we add a .gitmodules file at the root of the repository with the following content

[submodule "bare"]  
        path = bare  
        url = https://github.com/git/git.git  

We will trick VSCode into thinking that the malicious bare repository is a git submodule and it will execute calc.exe as soon as the branch is checked out.

Also as a side note, VSCode does protect against this kind of attacks using Workspace Trust, but in order to run Gitlab Workflow extension the workspace must be trusted ! Because the extension relies on git and since git is disabled if the workspace is not trusted. So in summary, it is only possible to checkout a merge request if the workspace is already trusted...

Steps to reproduce

We will distinguish 2 users to reproduce this vulnerability:

  • victim01 the victim
  • inspector-ambitious the attacker

Step 1: In victim context (victim01): The repo setup

  1. Create a blank project with a README.md file

image.png

image.png

  1. Invite the attacker as a member with Developer role. (only if project is private)

image.png

Step 2: In victim context (victim01): Setup VSCode, Gitlab Workflow extension

We will use a windows 10 virtual machine with a fresh installation of VSCode (1.77.1 as time of writing) and the Gitlab Workflow extension (3.61.0 as time of writing). Please note that Gitlab Workflow extension must be installed and configured to allow access to gitlab see Setup section at https://marketplace.visualstudio.com/items?itemName=GitLab.gitlab-workflow

  1. Head to the repository in my case (https://gitlab.com/victim01/gitlab-workflow-extension-rce-demo), click clone and Open in your IDE (Visual Studo Code HTTPS or SSH depending on your setup).

  2. You need to trust the repository in order to be able to launch Gitlab Workflow Extension.

Step3 : In attacker context (inspector-ambitious): Preparation of the attack

  1. We're going to prepare the fake main branch first, embed the bare repository and the .gitmodules file and push it.
git clone git@gitlab.com:victim01/gitlab-workflow-extension-rce-demo.git  
cd gitlab-workflow-extension-rce-demo  
git checkout -b fake_main  
cat <<EOF >>.gitmodules  
[submodule "foobar"]  
    path = foobar  
    url = https://github.com/git/git.git  
EOF  
###  the following is going to create manually a malicious bare repository  
mkdir foobar  
mkdir foobar/refs  
mkdir foobar/objects  
touch foobar/refs/.keeparound  
touch foobar/objects/.keeparound  
echo "ref: refs/heads/main" > foobar/HEAD  
cat <<EOF >> foobar/config  
[core]  
    repositoryformatversion = 0  
    filemode = true  
    bare = false  
    worktree = "./"  
    fsmonitor = "C:\\\\\\\\Windows\\\\\\\\System32\\\\\\\\calc.exe"  
EOF  
git add foobar  
git add .gitmodules  
git commit -m "you're hacked"  
git push origin fake_main:refs/heads/`printf 'main\uFEFF'`  

If everything went well you should now see 2 main branches

image.png

  1. Now we're going to create a new branch with a legit commit, still in the local copy of the git repo.
git checkout -b patch-1  
echo 1 > legit_change  
git add legit_change  
git commit -m "a legit change"  
git push origin patch-1  
  1. Finally we create a Merge Request with patch-1 as a source branch and our fake main branch as a target.

image.png

Ensure you see the proper commit for the target branch since it's easy to get confused...

You should see only 1 commit in the MR and just our legit change.

image.png

image.png

Step 3: In victim context (victim01): The attack

In VSCode

  1. Go to the Gitlab Workflow Extension

image.png

  1. Click on All project merge requests expand the first merge request and then click on Overview

image.png

You can note that we only see the legit_change as expected

Regarding the overview panel

image.png

You can see that there is no information regarding the target branch and that's a bit problematic I think because we don't know at what branch the MR is aimed and there could be anything in there... That's why I was saying that the first vulnerability is optional...

  1. (Optional Step to demonstrate vulnerability 1) Click on Open in Gitlab in the overview panel

image.png

It will launch the browser and what you'll see is what seems a perfectly safe merge request aimed at the default branch so indeed the maintainer would assume it will be safe.

image.png

Close the browser and go back to VSCode.

  1. Right Click on the Merge request and select Check out Merge Request Branch

image.png

As soon as the merge request is checked out...

image.png

calc.exe is launched !

Video

out.mp4

Export of the project for faster testing.

2023-04-11_17-51-335_victim01_gitlab-workflow-exten_export.tar.gz

Output of checks

This bug happens on GitLab.com (concerning Vulnerability 1), but this also affect self hosted instances...

Impact

This vulnerability allow an attacker to execute code remotely.

Please note that while in the demonstration the victim was the repository maintainer, unfortunately this vulnerability impacts anyone who has at least a read access to a repository, since it is possible to checkout a merge request on a public repository.

So we could also imagine a scenario where an attacker creates a public repository with malicious merge requests to attack random users, or a rogue project collaborator in a open source project who creates malicious merge request...

Also I only created an exploit on windows to get a nice visualization in the video, but all operating systems are affected.

Attachments

Warning: Attachments received through HackerOne, please exercise caution!

How To Reproduce

Please add reproducibility information to this section: