def __parse_section_config(self, service, service_config): """ Parse section [config] in <service_name>.conf, retrieve basic info about service (default port/protocol) and retrieve list of supported categories of checks for this service. :param str service: Service name :param defaultdict(str) service_config: Information about the service, updated into this method :return: List of categories of checks :rtype: list(str) :raises SettingsException: Exception raised if any unrecoverable error is encountered while parsing the section """ log_prefix = '[{filename}{ext} | Section "config"]'.format( filename=service, ext=CONF_EXT) # Check presence of mandatory options in [config] optparsed = self.config_parsers[service].options('config') for opt in SERVICE_CHECKS_CONFIG_OPTIONS[MANDATORY]: if opt not in optparsed: raise SettingsException('{prefix} Missing mandatory option "{option}"' \ ', check the file'.format(prefix=log_prefix, option=opt)) # Get port number default_port = self.config_parsers[service].safe_get_int( 'config', 'default_port', None, None) if default_port is None or default_port < 0 or default_port > 65535: raise SettingsException('{prefix} Invalid value for option "default_port",' \ ' must be in the range [0-65535]'.format(prefix=log_prefix)) # Get protocol protocol = self.config_parsers[service].safe_get_lower( 'config', 'protocol', 'tcp', ['tcp', 'udp']) # Get categories of checks as a list, clean each element categories = list(map(lambda x: StringUtils.clean( x.lower(), allowed_specials=('-', '_')), self.config_parsers[service].safe_get_list('config', 'categories', ',', []))) if not categories: raise SettingsException('{prefix} Option "categories" must have at least '\ 'one category'.format(prefix=log_prefix)) # Get authentication type (for HTTP) as a list, clean each element if 'auth_types' in optparsed: auth_types = list(map(lambda x: StringUtils.clean( x.lower(), allowed_specials=('-', '_')), self.config_parsers[service].safe_get_list( 'config', 'auth_types', ',', []))) else: auth_types = None # Update service configuration with parsed information service_config['default_port'] = default_port service_config['protocol'] = protocol service_config['auth_types'] = auth_types return categories
def show(self): """Display selected hosts""" results = self.get_results() if not results: logger.warning('No host to display') else: data = list() columns = [ 'IP', 'Hostname', 'OS', 'Type', 'Vendor', 'Comment', 'TCP', 'UDP', ] for r in results: data.append([ r.ip, StringUtils.wrap(r.hostname, 45) if r.hostname != str(r.ip) else '', StringUtils.wrap(r.os, 50), r.type, StringUtils.wrap(r.vendor, 30), StringUtils.shorten(r.comment, 40), r.get_nb_services(Protocol.TCP), r.get_nb_services(Protocol.UDP), ]) Output.table(columns, data, hrules=False)
def show(self): """Display selected credentials""" results = self.get_results() if not results: logger.warning('No credential to display') else: data = list() columns = [ 'IP', 'Hostname', 'Service', 'Port', 'Proto', 'Type', 'Username', 'Password', 'URL', 'Comment', ] for r in results: data.append([ r.service.host.ip, r.service.host.hostname \ if r.service.host.hostname != str(r.service.host.ip) else '', r.service.name, r.service.port, {Protocol.TCP: 'tcp', Protocol.UDP: 'udp'}.get(r.service.protocol), r.type or '', '<empty>' if r.username == '' else r.username, {'': '<empty>', None: '<???>'}.get(r.password, r.password), StringUtils.wrap(r.service.url, 50), StringUtils.wrap(r.comment, 50), ]) Output.table(columns, data, hrules=False)
def show(self): """Display selected services""" results = self.get_results() if not results: logger.warning('No service to display') else: data = list() columns = [ 'id', 'IP', #'Hostname', 'Port', 'Proto', 'Service', 'Banner', 'URL', 'Comment/Title', 'Checks', 'Creds', 'Vulns', ] for r in results: # Creds numbers nb_userpass = r.get_nb_credentials(single_username=False) nb_usernames = r.get_nb_credentials(single_username=True) nb_creds = '{}{}{}'.format( '{}'.format(Output.colored(str(nb_userpass), color='green' \ if nb_userpass > 0 else None)) if nb_userpass > 0 else '', '/' if nb_userpass > 0 and nb_usernames > 0 else '', '{} usr'.format(Output.colored(str(nb_usernames), color='yellow' \ if nb_usernames > 0 else None)) if nb_usernames > 0 else '') nb_vulns = Output.colored(str(len(r.vulns)), color='green' \ if len(r.vulns) > 0 else None) if len(r.vulns) > 0 else '' # Col "Comment/Title" (title is for HTML title for HTTP) if r.html_title: comment = r.html_title else: comment = r.comment data.append([ r.id, r.host.ip, #r.host.hostname, r.port, { Protocol.TCP: 'tcp', Protocol.UDP: 'udp' }.get(r.protocol), r.name, StringUtils.wrap(r.banner, 55), StringUtils.wrap(r.url, 50), StringUtils.shorten(comment, 40), len(r.results), nb_creds, nb_vulns, ]) Output.table(columns, data, hrules=False)
def __parse_section_products(self, service, service_config): """ Parse section [products] in <service_name>.conf and retrieve supported values for each product type. :param str service: Service name :param dict service_config: Service configuration, updated into this method :return: None :raises SettingsException: Exception raised if unconsistent values detected """ # First, check if config file has a [products] section if not self.config_parsers[service].has_section('products'): service_config['products'] = dict() return log_prefix = '[{filename}{ext} | Section "products"]'.format( filename=service, ext=CONF_EXT) products = dict() optparsed = self.config_parsers[service].options('products') # Loop over product types in [products] for product_type in optparsed: # Clean the product type product_type = StringUtils.clean(product_type.lower(), allowed_specials=('-', '_')) # Get supported product names as a list. # Only some special chars allowed, spaces allowed # '/' is used to separate vendor name (optional) and product name product_names = self.config_parsers[service].safe_get_list( 'products', product_type, ',', []) product_names = list( map( lambda x: StringUtils.clean( x, allowed_specials=('-', '_', '.', '/', '\\', ' ')), product_names)) if not product_names: raise SettingsException( '{prefix} Option "{option}" is empty'.format( prefix=log_prefix, option=opt)) products[product_type] = product_names # Update service configuration with supported products service_config['products'] = products return
def show_summary(self): """ """ data = list() columns = [ 'id', 'IP', 'Hostname', 'Port', 'Proto', 'Service', 'Banner', 'URL', ] id_ = 1 for target in self.targets: pointer_color = 'blue' if self.current_targetid == id_ else None pointer_attr = 'bold' if self.current_targetid == id_ else None data.append([ Output.colored( '>' + str(id_) if self.current_targetid == id_ else str(id_), color=pointer_color, attrs=pointer_attr), Output.colored(target.get_ip(), color=pointer_color, attrs=pointer_attr), Output.colored(target.get_host(), color=pointer_color, attrs=pointer_attr), Output.colored(str(target.get_port()), color=pointer_color, attrs=pointer_attr), Output.colored(target.get_protocol(), color=pointer_color, attrs=pointer_attr), Output.colored(target.get_service_name(), color=pointer_color, attrs=pointer_attr), Output.colored(StringUtils.wrap(target.get_banner(), 70), color=pointer_color, attrs=pointer_attr), Output.colored(StringUtils.wrap(target.get_url(), 50), color=pointer_color, attrs=pointer_attr), ]) id_ += 1 Output.table(columns, data, hrules=False)
def show(self): """Display selected vulnerabilities""" results = self.get_results() if not results: logger.warning('No vulnerability to display') else: data = list() columns = [ 'IP', 'Service', 'Port', 'Proto', 'Vulnerability', ] for r in results: data.append([ r.service.host.ip, r.service.name, r.service.port, { Protocol.TCP: 'tcp', Protocol.UDP: 'udp' }.get(r.service.protocol), StringUtils.wrap(r.name, 140), ]) Output.table(columns, data, hrules=False)
def show_products(self, filter_service=None): """ Display supported products in a table :param list filter_service: Filter on services (default: all) """ data = list() columns = [ 'Type', 'Product Names', ] services = self.list_services() if filter_service is None else [ filter_service ] for service in services: products = self.services[service]['products'] for product_type in products: names = sorted( self.services[service]['products'][product_type]) names = StringUtils.wrap(', '.join(names), 100) data.append([product_type, names]) Output.title1('Available products for {filter}'.format( filter='all services' if filter_service is None \ else 'service ' + filter_service)) if not data: logger.warning('No product') else: Output.table(columns, data)
def show_categories(self, filter_service=None): """ Show list of categories of checks for the given service or all services :param filter_service: None or given service :return: None """ data = list() columns = [ 'Category', 'Services', ] services = self.list_services() if filter_service is None else [ filter_service ] svcbycat = defaultdict(list) for service in services: for category in self.services[service]['checks'].categories: svcbycat[category].append(service) for category in svcbycat: data.append([ category, StringUtils.wrap(', '.join(svcbycat[category]), 100) ]) Output.table(columns, data)
def show(self, highlight=None): """ Display selected missions. :param str highlight: Name of the mission to highlight """ results = self.get_results() if not results: logger.warning('No matching mission') else: data = list() columns = [ 'Mission', 'Creation date', 'Comment', '# Hosts', '# Services', ] for mission in results: color = 'light_green' if mission.name == highlight else None data.append([ Output.colored(mission.name, color=color), Output.colored(str(mission.creation_date), color=color), Output.colored(StringUtils.wrap(mission.comment, 50), color=color), Output.colored(len(mission.hosts), color=color), Output.colored(mission.get_nb_services(), color=color), ]) Output.table(columns, data, hrules=False)
def show(self, filter_service=None): """ Display information about supported attack profiles :param str filter_service: Service name to filter with (default: no filter) """ data = list() columns = [ 'Profile', 'Description', ] for p in self.profiles: #print(p.checks) if not filter_service or p.is_service_supported(filter_service): data.append([ Output.colored(p.name, attrs='bold'), StringUtils.wrap(p.description, 120) ]) if filter_service: service = 'for service {}'.format(filter_service.upper()) else: service = '' Output.title1('Attack Profiles {service}'.format(service=service)) Output.table(columns, data, hrules=False) if not filter_service: print Output.print('Run "info --attack-profiles <service>" to see the attack ' \ 'profiles supported for a given service.')
def __generate_sidebar_checks(self, service): """ Generate the sidebar with the list of checks that have been run for the specified service. :param Service service: Service Model """ req = ResultsRequester(self.sqlsession) req.select_mission(self.mission) # Filter on service id filter_ = Filter(FilterOperator.AND) filter_.add_condition(Condition(service.id, FilterData.SERVICE_ID)) req.add_filter(filter_) results = req.get_results() html = '' i = 0 for r in results: # Icon category icon = IconsMapping.get_icon_html('category', r.category) html += """ <li{class_}> <a href="#{id}">{icon}{check}</a> </li> """.format(class_=' class="active"' if i == 0 else '', id=r.check, icon=icon, check=StringUtils.shorten(r.check, 28)) i += 1 return html
def __parse_section_specific_options(self, service, service_config): """ Parse section [specific_options] in <service_name>.conf and update service config :param service: Service name :param service_config: Dict storing info about service, updated into this method :return: None :raises SettingsException: """ try: optparsed = self.config_parsers[service].options('specific_options') except configparser.NoSectionError: service_config['specific_options'] = dict() return specific_options = dict() for opt in optparsed: option_type = self.config_parsers[service].safe_get_lower('specific_options', opt, None, None) if option_type.count(':') == 1: option_type, default_value = option_type.split(':') opt_clean = StringUtils.clean(opt.lower(), allowed_specials=('-', '_')) if option_type == 'boolean' : specific_options[opt_clean] = OptionType.BOOLEAN elif option_type == 'list' : specific_options[opt_clean] = OptionType.LIST elif option_type == 'var' : specific_options[opt_clean] = OptionType.VAR else: raise SettingsException('[{filename}{ext} | Section "specific_options"] Specific option named "{option}" has ' \ 'an invalid type. Supported types are: boolean, list, var'.format( filename = service, ext=CONF_EXT, option=opt)) service_config['specific_options'] = specific_options
def __parse_section_supported_list_options(self, service, service_config): """ Parse section [supported_list_options] in <service_name>.conf and update service configuration with supported values for specific options of type list. Must be called after self.__parse_section_config() and self.__parse_section_specific_options(). :param defaultdict(str) service_config: Information about the service, updated into this method :return: None :raises SettingsException: Exception raised if any unrecoverable error is encountered while parsing the section """ # Get names of specific options of type list options_list = list( filter( lambda x: service_config['specific_options'][x] == OptionType. LIST, service_config['specific_options'].keys())) if not options_list: return elif not self.config_parsers[service].has_section( 'supported_list_options'): raise SettingsException('[{filename}{ext}] Missing section ' \ '[supported_list_options] to store supported values for specific ' \ 'options of type "list"'.format(filename=service, ext=CONF_EXT)) log_prefix = '[{filename}{ext} | Section "supported_list_options"]'.format( filename=service, ext=CONF_EXT) supported_list_options = dict() optparsed = self.config_parsers[service].options( 'supported_list_options') # Loop over specific options of type list for opt in options_list: # If missing option if 'supported_' + opt not in optparsed: raise SettingsException('{prefix} No option "supported_{option}" ' \ 'is defined'.format(prefix=log_prefix, option=opt)) # Values are put in lowercase, no spaces, no special chars (except -, _) values = list( map( lambda x: StringUtils.clean(x.lower(), allowed_specials=('-', '_')), self.config_parsers[service].safe_get_list( 'supported_list_options', 'supported_' + opt, ',', []))) if not values: raise SettingsException('{prefix} Option "supported_{option}" is ' \ 'empty'.format(prefix=log_prefix, option=opt)) supported_list_options[opt] = values # Update service configuration with lists of supported values service_config['supported_list_options'] = supported_list_options
def show(self): results = self.get_results() if not results: logger.warning('No service to display') else: data = list() columns = [ 'id', 'IP', #'Hostname', 'Port', 'Proto', 'Service', 'Banner', 'URL', 'Comment', 'Checks', 'Creds', ] for r in results: nb_userpass = r.get_nb_credentials(single_username=False) nb_usernames = r.get_nb_credentials(single_username=True) nb_creds = '{}{}{}'.format( '{}'.format(Output.colored(str(nb_userpass), color='green' \ if nb_userpass > 0 else None)) if nb_userpass > 0 else '', '/' if nb_userpass > 0 and nb_usernames > 0 else '', '{} users'.format(Output.colored(str(nb_usernames), color='yellow' \ if nb_usernames > 0 else None)) if nb_usernames > 0 else '') data.append([ r.id, r.host.ip, #r.host.hostname, r.port, { Protocol.TCP: 'tcp', Protocol.UDP: 'udp' }.get(r.protocol), r.name, StringUtils.wrap(r.banner, 65), StringUtils.wrap(r.url, 50), StringUtils.wrap(r.comment, 50), len(r.results), nb_creds, ]) Output.table(columns, data, hrules=False)
def grab_html_title(url): """Return HTML title from an URL""" try: r = requests.get(url, verify=False) html = bs4.BeautifulSoup(r.text, 'html.parser') # Remove non-ASCII characters and duplicate spaces title = StringUtils.remove_non_printable_chars( html.title.text.strip()) title = " ".join(title.split()) # Shorten if necessary title = StringUtils.shorten(title, 250) return title except: return ''
def show_search_results(self, string, nb_words=12): """ Display command outputs search results. For good readability, only some words surrounding the search string are displayed. :param str string: Search string (accepts wildcard "%") :param int nb_words: Number of words surrounding the search string to show """ results = self.query.filter(CommandOutput.output.ilike('%'+string+'%')) if not results: logger.error('No result') else: Output.title2('Search results:') data = list() columns = [ 'IP', 'Port', 'Proto', 'Service', 'Check id', 'Category', 'Check', 'Matching text', ] for r in results: match = StringUtils.surrounding_text(r.outputraw, string, nb_words) # There might have several matches in one command result (one row # per match) for m in match: data.append([ r.result.service.host.ip, r.result.service.port, {Protocol.TCP: 'tcp', Protocol.UDP: 'udp'}.get( r.result.service.protocol), r.result.service.name, r.result.id, r.result.category, r.result.check, StringUtils.wrap(m, 70), ]) print() Output.table(columns, data, hrules=False)
def grab_html_title(url): try: r = requests.get(url, verify=False) html = bs4.BeautifulSoup(r.text, 'html.parser') return StringUtils.remove_non_printable_chars( html.title.text.strip()) except: return ''
def __parse_tool_options(self, section, tool_config): """ Check and parse options from a given tool section :param section: Tool section into the toolbox settings file :param tool_config: A defaultdict(str) storing tool config which is updated into this method :return: Boolean indicating status """ log_prefix = '[{filename}{ext} | Section "{section}"]'.format( filename=TOOLBOX_CONF_FILE, ext=CONF_EXT, section=section) optparsed = self.config_parsers[TOOLBOX_CONF_FILE].options(section) for opt in TOOL_OPTIONS[MANDATORY]: if opt not in optparsed: logger.warning('{prefix} Missing mandatory option "{option}", tool is skipped'.format( prefix=log_prefix, option=opt)) return False tool_config['name_clean'] = section for opt in optparsed: if opt not in TOOL_OPTIONS[MANDATORY]+TOOL_OPTIONS[OPTIONAL]: logger.warning('{prefix} Option "{option}" is not supported, it will be ignored'.format( prefix=log_prefix, option=opt)) continue if opt in TOOL_OPTIONS[MANDATORY]: val = self.config_parsers[TOOLBOX_CONF_FILE].safe_get(section, opt, '', None) if opt == 'name': tool_config[opt]=StringUtils.clean(val, allowed_specials=['-', '_']) elif opt == 'description': tool_config[opt] = val elif opt == 'target_service': tool_config[opt] = val.lower() if tool_config[opt] not in self.services.list_services(multi=True): logger.warning('{prefix} Service specified in "target_service" is not supported, ' \ 'tool is skipped'.format(prefix=log_prefix)) return False if not tool_config[opt]: logger.warning('{prefix} Mandatory option "{option}" is empty, tool is skipped'.format( prefix=log_prefix, option=opt)) return False elif opt == 'install': tool_config[opt] = Command(cmdtype = CMD_INSTALL, cmdline = self.config_parsers[TOOLBOX_CONF_FILE].safe_get(section, opt, '', None)) elif opt == 'update': tool_config[opt] = Command(cmdtype = CMD_UPDATE, cmdline = self.config_parsers[TOOLBOX_CONF_FILE].safe_get(section, opt, '', None)) elif opt == 'check_command': tool_config[opt] = Command(cmdtype = CMD_CHECK, cmdline = self.config_parsers[TOOLBOX_CONF_FILE].safe_get(section, opt, '', None)) return True
def __parse_section_specific_options(self, service, service_config): """ Parse section [specific_options] in <service_name>.conf and update service configuration with supported specific options for the service and their respective types. :param str service: Service name :param defaultdict(str) service_config: Information about the service, updated into this method :return: None :raises SettingsException: Exception raised if any unrecoverable error is encountered while parsing the section """ # Case when no [specific_options] can be found try: optparsed = self.config_parsers[service].options( 'specific_options') except configparser.NoSectionError: service_config['specific_options'] = dict() return specific_options = dict() # Loop over supported specific options for opt in optparsed: # Get option type option_type = self.config_parsers[service].safe_get_lower( 'specific_options', opt, None, None) # Handle case when default value is specified (for boolean) if option_type.count(':') == 1: option_type, default_value = option_type.split(':') opt_clean = StringUtils.clean(opt.lower(), allowed_specials=('-', '_')) if option_type == 'boolean': specific_options[opt_clean] = OptionType.BOOLEAN elif option_type == 'list': specific_options[opt_clean] = OptionType.LIST elif option_type == 'var': specific_options[opt_clean] = OptionType.VAR else: raise SettingsException('[{filename}{ext} | Section ' \ '"specific_options"] Specific option named "{option}" has ' \ 'an invalid type. Supported types are: boolean, list, var'.format( filename = service, ext=CONF_EXT, option=opt)) # Update service configuration with specific options names and types service_config['specific_options'] = specific_options
def __generate_table_credentials(self): """ Generate the table with all credentials registered in the mission """ req = CredentialsRequester(self.sqlsession) req.select_mission(self.mission) credentials = req.get_results() if len(credentials) == 0: html = """ <tr class="notfound"> <td colspan="10">No record found</td> </tr> """ else: html = '' for cred in credentials: html += """ <tr> <td>{ip}</td> <td>{hostname}</td> <td>{service}</td> <td>{port}</td> <td>{proto}</td> <td>{type}</td> <td class="font-weight-bold">{username}</td> <td class="font-weight-bold">{password}</td> <td>{url}</td> <td>{comment}</td> </tr> """.format( ip=cred.service.host.ip, hostname=cred.service.host.hostname \ if cred.service.host.hostname != str(cred.service.host.ip)\ else '', service=cred.service.name, port=cred.service.port, proto={Protocol.TCP: 'tcp', Protocol.UDP: 'udp'}.get( cred.service.protocol), type=cred.type or '', username='******' if cred.username == '' else cred.username, password={'': '<empty>', None: '<???>'}.get( cred.password, cred.password), url='<a href="{}" title="{}">{}</a>'.format( cred.service.url, cred.service.url, StringUtils.shorten(cred.service.url, 50)) \ if cred.service.url else '', comment=cred.comment) return html
def __parse_section_config(self, service, service_config): """ Parse section [config] in <service_name>.conf, retrieve basic info about service (default port/protocol) and retrieve list of categories. :param service: Service name :param service_config: Dict storing info about service, updated into this method :return: List of categories of checks :raises SettingsException: """ log_prefix = '[{filename}{ext} | Section "config"]'.format(filename=service, ext=CONF_EXT) optparsed = self.config_parsers[service].options('config') for opt in SERVICE_CHECKS_CONFIG_OPTIONS[MANDATORY]: if opt not in optparsed: raise SettingsException('{prefix} Missing mandatory option "{option}", check the file'.format( prefix=log_prefix, option=opt)) default_port = self.config_parsers[service].safe_get_int('config', 'default_port', None, None) protocol = self.config_parsers[service].safe_get_lower('config', 'protocol', 'tcp', ['tcp', 'udp']) categories = list(map(lambda x: StringUtils.clean(x.lower(), allowed_specials=('-', '_')), self.config_parsers[service].safe_get_list('config', 'categories', ',', []))) auth_types = list(map(lambda x: StringUtils.clean(x.lower(), allowed_specials=('-', '_')), self.config_parsers[service].safe_get_list('config', 'auth_types', ',', []))) \ if 'auth_types' in optparsed else None if default_port is None or default_port < 0 or default_port > 65535: raise SettingsException('{prefix} Invalid value for option "default_port", must be in the range ' \ '[0-65535]'.format(prefix=log_prefix)) if not categories: raise SettingsException('{prefix} Option "categories" must have at least one category'.format( prefix=log_prefix)) service_config['default_port'] = default_port service_config['protocol'] = protocol service_config['auth_types'] = auth_types return categories
def delete(self): """Delete selected vulnerabilities""" results = self.get_results() if not results: logger.error('No matching vulnerability') else: for r in results: logger.info('Vulnerability deleted: "{vuln}" for service={service} ' \ 'host={ip} port={port}/{proto}'.format( vuln=StringUtils.shorten(r.name, 50), service=r.service.name, ip=r.service.host.ip, port=r.service.port, proto={Protocol.TCP: 'tcp', Protocol.UDP: 'udp'}.get( r.service.protocol))) self.sqlsess.delete(r) self.sqlsess.commit()
def show_toolbox(self, filter_service=None): """ Display a table showing the content of the toolbox. :param str filter_service: Service name to filter with (default: no filter) """ if filter_service is not None and filter_service not in self.services: return data = list() columns = [ 'Name', 'Service', 'Status/Update', 'Description', ] services = self.services if filter_service is None else [ filter_service ] for service in services: for tool in self.tools[service]: # Install status style if tool.installed: status = Output.colored('OK | ' + tool.last_update.split(' ')[0], color='green') else: status = Output.colored('Not installed', color='red') # Add line for the tool data.append([ tool.name, tool.target_service, status, StringUtils.wrap(tool.description, 120), # Max line length ]) Output.title1('Toolbox content - {filter}'.format( filter='all services' if filter_service is None \ else 'service ' + filter_service)) Output.table(columns, data, hrules=False)
def detect_vulns(self): """ Detect vulnerability from command output Important: A command output might contain several vulnerabilities with the same pattern. """ if self.service.name in vulns_match.keys(): if self.tool_name in vulns_match[self.service.name].keys(): p = vulns_match[self.service.name][self.tool_name] for pattern in p.keys(): logger.debug('Search for vulns pattern: {pattern}'.format( pattern=pattern)) # Important: Multiple search/match #m = re.search(pattern, self.cmd_output, re.IGNORECASE) try: mall = re.finditer(pattern, self.cmd_output, re.IGNORECASE | re.MULTILINE) except Exception as e: logger.warning('Error with matchstring [{pattern}], you ' \ 'should review it. Exception: {exception}'.format( pattern=pattern, exception=e)) break # Process each match if mall: for m in mall: name = self.__replace_tokens_from_matchobj( p[pattern], m) if name is None: continue # Add vulnerability to context logger.debug('Vuln pattern matches') self.cu.add_vuln( StringUtils.remove_non_printable_chars(name))
def show_specific_options(self, filter_service=None): """ Display supported specific options in a table. :param list filter_service: Filter on services (default: all) """ data = list() columns = [ 'Option', 'Service', 'Supported values', ] services = self.list_services() if filter_service is None else [ filter_service ] for service in services: options = self.services[service]['specific_options'] for opt in options: if options[opt] == OptionType.BOOLEAN: values = 'true, false' elif options[opt] == OptionType.LIST: values = sorted( self.services[service]['supported_list_options'][opt]) values = StringUtils.wrap(', '.join(values), 80) else: values = '<anything>' data.append([opt, service, values]) Output.title1('Available context-specific options for {filter}'.format( filter='all services' if filter_service is None \ else 'service ' + filter_service)) if not data: logger.warning('No specific option') else: Output.table(columns, data, hrules=False)
def __parse_section_supported_list_options(self, service, service_config): """ Parse section [supported_list_options] in <service_name>.conf and retrieve supported values for specific options of type list. Must be called after self.__parse_section_config() and self.__parse_section_specific_options() :param service: Service name :param service_config: Dict storing info about service, updated into this method :return: None :raises SettingsException: """ options_list = list(filter(lambda x: service_config['specific_options'][x] == OptionType.LIST, service_config['specific_options'].keys())) if not options_list: return dict() elif not self.config_parsers[service].has_section('supported_list_options'): raise SettingsException('[{filename}{ext}] Missing section [supported_list_options] to store supported ' \ 'values for specific options of type list'.format(filename=service, ext=CONF_EXT)) log_prefix = '[{filename}{ext} | Section "supported_list_options"]'.format(filename=service, ext=CONF_EXT) supported_list_options = dict() optparsed = self.config_parsers[service].options('supported_list_options') for opt in options_list: if 'supported_'+opt not in optparsed: raise SettingsException('{prefix} No option "supported_{option}" is defined'.format( prefix=log_prefix, option=opt)) values = list(map(lambda x: StringUtils.clean(x.lower(), allowed_specials=('-', '_')), self.config_parsers[service].safe_get_list('supported_list_options', 'supported_'+opt, ',', []))) if not values: raise SettingsException('{prefix} Option "supported_{option}" is empty'.format( prefix=log_prefix, option=opt)) supported_list_options[opt] = values service_config['supported_list_options'] = supported_list_options
def __generate_table_credentials(self): """ Generate the table with all credentials registered in the mission """ req = CredentialsRequester(self.sqlsession) req.select_mission(self.mission) credentials = req.get_results() if len(credentials) == 0: html = """ <tr class="notfound"> <td colspan="9">No record found</td> </tr> """ else: html = '' for cred in credentials: # Service name service_name = IconsMapping.get_icon_html( 'service', cred.service.name) service_name += str(cred.service.name) # Add color to username/password username = '******' if cred.username == '' else cred.username username = '******'.format( color='green' if cred.password is not None else 'yellow', username=username) password = { '': '<empty>', None: '<???>' }.get(cred.password, cred.password) password = '******'.format( color='green' if cred.password is not None else 'yellow', password=password) html += """ <tr> <td>{ip}</td> <td>{hostname}</td> <td>{service}</td> <td>{port} /{proto}</td> <td>{type}</td> <td class="font-weight-bold">{username}</td> <td class="font-weight-bold">{password}</td> <td>{url}</td> <td>{comment}</td> </tr> """.format( ip=cred.service.host.ip, hostname=cred.service.host.hostname \ if cred.service.host.hostname != str(cred.service.host.ip)\ else '', service=service_name, port=cred.service.port, proto={Protocol.TCP: 'tcp', Protocol.UDP: 'udp'}.get( cred.service.protocol), type=cred.type or '', username=username, password=password, url='<a href="{}" title="{}">{}</a>'.format( cred.service.url, cred.service.url, StringUtils.shorten(cred.service.url, 50)) \ if cred.service.url else '', comment=cred.comment) return html
def run(self): # Create report directory dirname = '{mission}-{datetime}'.format( mission=StringUtils.clean(self.mission.replace(' ', '_'), allowed_specials=('_', '-')), datetime=datetime.datetime.now().strftime('%Y%m%d%H%M%S')) self.output_path = self.output_path + '/' + dirname if not FileUtils.create_directory(self.output_path): logger.error('Unable to create report directory: "{path}"'.format( path=self.output_path)) return False # Retrieve all services in selected mission req = ServicesRequester(self.sqlsession) req.select_mission(self.mission) services = req.get_results() # Generate screenshots processor = ScreenshotsProcessor(self.mission, self.sqlsession) processor.run() screens_dir = self.output_path + '/screenshots' if not FileUtils.create_directory(screens_dir): logger.warning( 'Unable to create screenshots directory: "{path}"'.format( path=screens_dir)) else: for service in services: if service.name == 'http' and service.screenshot is not None \ and service.screenshot.status == ScreenStatus.OK: img_name = 'scren-{ip}-{port}-{id}'.format( ip=str(service.host.ip), port=service.port, id=service.id) path = screens_dir + '/' + img_name ImageUtils.save_image(service.screenshot.image, path + '.png') ImageUtils.save_image(service.screenshot.thumbnail, path + '.thumb.png') # Create index.html html = self.__generate_index() if FileUtils.write(self.output_path + '/index.html', html): logger.info('index.html file generated') else: logger.error('An error occured while generating index.html') return False # Create results-<service>.html (1 for each service) for service in services: # Useless to create page when no check has been run for the service if len(service.results) == 0: continue html = self.__generate_results_page(service) # Create a unique name for the service HTML file filename = 'results-{ip}-{port}-{service}-{id}.html'.format( ip=str(service.host.ip), port=service.port, service=service.name, id=service.id) if FileUtils.write(self.output_path + '/' + filename, html): logger.info( '{filename} file generated'.format(filename=filename)) else: logger.error( 'An error occured while generating {filename}'.format( filename=filename)) return False logger.success('HTML Report written with success in: {path}'.format( path=self.output_path)) logger.info('Important: If running from Docker container, make sure to run ' \ '"xhost +" on the host before') if Output.prompt_confirm('Would you like to open the report now ?', default=True): webbrowser.open(self.output_path + '/index.html') return True
def __generate_table_web(self): """ Generate the table with HTTP services registered in the mission """ req = ServicesRequester(self.sqlsession) req.select_mission(self.mission) filter_ = Filter(FilterOperator.AND) filter_.add_condition(Condition('http', FilterData.SERVICE_EXACT)) req.add_filter(filter_) services = req.get_results() if len(services) == 0: html = """ <tr class="notfound"> <td colspan="7">No record found</td> </tr> """ else: html = '' # Unavailable thumbnail with open(REPORT_TPL_DIR + '/../img/unavailable.png', 'rb') as f: unavailable_b64 = base64.b64encode(f.read()).decode('ascii') for service in services: # Results HTML page name results = 'results-{ip}-{port}-{service}-{id}.html'.format( ip=str(service.host.ip), port=service.port, service=service.name, id=service.id) # Encrypted ? (SSL/TLS) enc = '<span class="mdi mdi-lock" title="SSL/TLS encrypted"></span>' \ if service.is_encrypted() else '' # Web technos (in a specific order) # try: # technos = ast.literal_eval(service.web_technos) # except Exception as e: # logger.debug('Error when retrieving "web_technos" field ' \ # 'from db: {exc} for {service}'.format( # exc=e, service=service)) # technos = list() # tmp = list() # for t in technos: # tmp.append('{}{}{}'.format( # t['name'], # ' ' if t['version'] else '', # t['version'] if t['version'] else '')) # webtechnos = ' | '.join(tmp) webtechnos = '' product_types = ('web-server', 'web-appserver', 'web-cms', 'web-language', 'web-framework', 'web-jslib') for t in product_types: product = service.get_product(t) if product: webtechnos += '<span class="badge badge-{type} badge-light">' \ '{name}{version}</span>'.format( type=t, name=product.name, version=' '+str(product.version) \ if product.version else '') # Web Application Firewall product = service.get_product('web-application-firewall') waf = '' if product: waf = '<span class="badge badge-web-application-firewall ' \ 'badge-light">{name}{version}</span>'.format( name=product.name, version=' '+str(product.version) \ if product.version else '') # Screenshot img_name = 'scren-{ip}-{port}-{id}'.format(ip=str( service.host.ip), port=service.port, id=service.id) path = self.output_path + '/screenshots' if service.screenshot is not None \ and service.screenshot.status == ScreenStatus.OK \ and FileUtils.exists(path + '/' + img_name + '.png') \ and FileUtils.exists(path + '/' + img_name + '.thumb.png'): screenshot = """ <a href="{screenlarge}" title="{url} - {title}" class="image-link"> <img src="{screenthumb}" class="border rounded"> </a> """.format(url=service.url, screenlarge='screenshots/' + img_name + '.png', title=service.html_title, screenthumb='screenshots/' + img_name + '.thumb.png') else: screenshot = """ <img src="data:image/png;base64,{unavailable}"> """.format(unavailable=unavailable_b64) # HTML for table row html += """ <tr{clickable}> <td>{url}</td> <td>{enc}</td> <td>{title}</td> <td>{webtechnos}</td> <td>{waf}</td> <td>{screenshot}</td> <td>{checks}</td> </tr> """.format( clickable=' class="clickable-row" data-href="{results}"'.format( results=results) if len(service.results) > 0 else '', url='<a href="{}" title="{}">{}</a>'.format( service.url, service.url, StringUtils.shorten(service.url, 50)) \ if service.url else '', enc=enc, title=StringUtils.shorten(service.html_title, 40), webtechnos=webtechnos, waf=waf, screenshot=screenshot, checks=len(service.results)) return html