def detect_os(self): """ Detect product from command output """ for os in os_match.keys(): if self.tool_name in os_match[os].keys(): patterns = os_match[os][self.tool_name] if type(patterns) == str: patterns = [patterns] for pattern in patterns: logger.debug('Search for os pattern: {pattern}'.format( pattern=pattern)) try: m = re.search(pattern, self.cmd_output, re.IGNORECASE) except Exception as e: logger.warning('Error with matchstring [{pattern}], ' \ 'you should review it. Exception: {exc}'.format( pattern=pattern, exc=e)) break # If pattern matches, add detected OS if m: logger.debug('OS pattern matches') # Add detected OS to the context self.cu.add_os(os) return
def check_args_attack(self): """Check arguments for subcommand Attack""" status = True if self.args.target_ip_or_url and self.args.mission: logger.error( '--target and --mission cannot be used at the same time') return False elif self.args.target_ip_or_url: status &= self.__check_args_attack_single_target() elif self.args.mission: status &= self.__check_args_attack_multi_targets() else: #logger.error('At least one target must be selected') self.subparser.print_help() return False if self.args.debug: logger.setLevel('DEBUG') logger.debug('Debug mode enabled') # status &= self.__check_args_attack_single_target() # status &= self.__check_args_attack_multi_targets() status &= self.__check_args_attack_bruteforce() status &= self.__check_args_attack_selection() status &= self.__check_args_attack_context() return status
def __detect_specific_options(self): """Detect specific option update from command output""" if self.service.name in options_match.keys(): if self.tool_name in options_match[self.service.name].keys(): p = options_match[self.service.name][self.tool_name] for pattern in p.keys(): logger.debug('Search for option pattern: {pattern}'.format( pattern=pattern)) try: m = re.search(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 # If pattern matches cmd output, update specific option if m: logger.debug('Option pattern matches') if 'name' in p[pattern]: name = self.__replace_tokens_from_matchobj( p[pattern]['name'], m) if name is None: continue else: logger.smarterror('Invalid matchstring for ' \ 'service={service}, tool={tool}: Missing ' \ '"name" key'.format( service=self.service.name, tool=self.tool_name)) continue if 'value' in p[pattern]: value = self.__replace_tokens_from_matchobj( p[pattern]['value'], m) if value is None: continue else: logger.smarterror('Invalid matchstring for ' \ 'service={service}, tool={tool}: Missing ' \ '"value" key'.format( service=self.service.name, tool=self.tool_name)) continue # Add specific option to context self.cu.add_option(name, value)
def start_http(self): """Method run specifically for HTTP services""" # Autodetect HTTPS if self.service.url.lower().startswith('https://'): logger.smartinfo('HTTPS protocol detected from URL') self.cu.add_option('https', 'true') # Check if HTTP service is protected by .htaccess authentication if self.service.http_headers \ and '401 Unauthorized'.lower() in self.service.http_headers.lower(): logger.smartinfo('HTTP authentication (htaccess) detected ' \ '(401 Unauthorized)') self.cu.add_option('htaccess', 'true') # Update context with web technologies if self.service.web_technos: # Detect OS if not self.service.host.os: processor = MatchstringsProcessor(self.service, 'wappalyzer', self.service.host.os, self.cu) processor.detect_os() # Detect products try: technos = ast.literal_eval(self.service.web_technos) except Exception as e: logger.debug('Error when retrieving "web_technos" field ' \ 'from db: {}'.format(e)) technos = list() for t in technos: for prodtype in products_match['http']: p = products_match['http'][prodtype] for prodname in p: if 'wappalyzer' in p[prodname]: pattern = p[prodname]['wappalyzer'] #m = re.search(pattern, t['name'], re.IGNORECASE|re.DOTALL) if pattern.lower() == t['name'].lower(): version = t['version'] self.cu.add_product(prodtype, prodname, version) # Move to next product type if something found break
def check_target_compliance(self, target): """ Check if target complies with any of the context requirements of the different commands defined in the security check. :param Target target: Target :return: Check result :rtype: bool """ i = 1 for command in self.commands: logger.debug( '{check} - Command #{i} context requirements: {rawstr}'.format( check=self.name, i=i, rawstr=command.context_requirements)) i += 1 if command.context_requirements.check_target_compliance(target): return True return False
def start_http(self): # Autodetect HTTPS if self.service.url.lower().startswith('https://'): logger.smartinfo('HTTPS protocol detected from URL') self.cu.add_option('https', 'true') # Check if HTTP service is protected by .htaccess authentication if '401 Unauthorized'.lower() in self.service.http_headers.lower(): logger.smartinfo('HTTP authentication (htaccess) detected ' \ '(401 Unauthorized)') self.cu.add_option('htaccess', 'true') # Try to detect web server and/or appserver from Nmap banner self.__detect_product_from_banner('web-server') self.__detect_product_from_banner('web-appserver') # Try to detect supported products from web technologies if self.service.web_technos: try: technos = ast.literal_eval(self.service.web_technos) except Exception as e: logger.debug('Error when retrieving "web_technos" field ' \ 'from db: {}'.format(e)) technos = list() for t in technos: for prodtype in products_match['http']: p = products_match['http'][prodtype] for prodname in p: if 'wappalyzer' in p[prodname]: pattern = p[prodname]['wappalyzer'] #m = re.search(pattern, t['name'], re.IGNORECASE|re.DOTALL) if pattern.lower() == t['name'].lower(): version = t['version'] self.cu.add_product(prodtype, prodname, version) # Move to next product type if something found break
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 run(self): """Run the Attack Controller""" args = self.arguments.args logger.debug('CLI arguments:') logger.debug(args) # Attack configuration: Categories of checks to run categories = self.settings.services.list_all_categories() # default: all if args.cat_only: categories = [ cat for cat in categories if cat in args.cat_only ] elif args.cat_exclude: categories = [ cat for cat in categories if cat not in args.cat_exclude ] # Create the attack scope self.attack_scope = AttackScope( self.settings, self.arguments, self.sqlsess, args.mission or args.add, filter_categories=categories, filter_checks=args.checks, attack_profile=args.profile, fast_mode=args.fast_mode) # Run the attack begin = datetime.datetime.now() if args.target_ip_or_url: self.__run_for_single_target(args) else: self.__run_for_multi_targets(args) print() duration = datetime.datetime.now() - begin logger.info('Finished. Duration: {}'.format(format_timespan(duration.seconds)))
def run(self, target, arguments, sqlsession, fast_mode=False): """ Run the security check. It consists in running commands with context requirements matching with the target's context. :param Target target: Target :param ArgumentsParser arguments: Arguments from command-line :param Session sqlsession: SQLAlchemy session :param SmartModulesLoader smartmodules_loader: Loader of SmartModules :param bool fast_mode: Set to true to disable prompts :return: Status :rtype: bool """ if not self.tool.installed: return False i = 1 command_outputs = list() for command in self.commands: if command.context_requirements.check_target_compliance(target): if not command.context_requirements.is_empty: logger.info('Command #{num:02} matches requirements: ' \ '{context}'.format(num=i, context=command.context_requirements)) cmdline = command.get_cmdline(self.tool.tool_dir, target, arguments) if fast_mode: logger.info('Run command #{num:02}'.format(num=i)) mode = 'y' else: mode = Output.prompt_choice( 'Run command {num}? [Y/n/f/q] '.format( num='' if len(self.commands) == 1 else \ '#{num:02} '.format(num=i)), choices={ 'y': 'Yes', 'n': 'No', #'t': 'New tab', #'w': 'New window', 'f': 'Switch to fast mode (do not prompt anymore)', 'q': 'Quit the program', }, default='y') if mode == 'q': logger.warning('Exit !') sys.exit(0) elif mode == 'n': logger.info('Skipping this command') continue else: if mode == 'f': logger.info('Switch to fast mode') arguments.args.fast_mode = True Output.begin_cmd(cmdline) process = ProcessLauncher(cmdline) if mode == 'y' or mode == 'f': output = process.start() # elif mode == 't': # output = process.start_in_new_tab() # logger.info('Command started in new tab') # else: # output = process.start_in_new_window(self.name) # logger.info('Command started in new window') Output.delimiter() print() output = StringUtils.interpret_ansi_escape_clear_lines( output) outputraw = StringUtils.remove_ansi_escape(output) command_outputs.append( CommandOutput(cmdline=cmdline, output=output, outputraw=outputraw)) # Run smartmodule method on output postcheck = SmartPostcheck( target.service, sqlsession, self.tool.name, '{0}\n{1}'.format(cmdline, outputraw)) postcheck.run() else: logger.info('Command #{num:02} does not match requirements: ' \ '{context}'.format(num=i, context=command.context_requirements)) logger.debug('Context string: {rawstr}'.format( rawstr=command.context_requirements)) i += 1 # Add outputs in database if command_outputs: results_requester = ResultsRequester(sqlsession) results_requester.add_result(target.service.id, self.name, self.category, command_outputs) 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="5">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) # Web technos 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) # 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="{title}" class="image-link"> <img src="{screenthumb}" class="border rounded"> </a> """.format(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>{title}</td> <td>{webtechnos}</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 '', title=StringUtils.shorten(service.html_title, 40), webtechnos=webtechnos, screenshot=screenshot, checks=len(service.results)) return html
def __detect_credentials(self): """ Detect usernames/credentials from command output Important: A command output might contain several usernames/passwords with the same pattern. Example method "search": >>> text = " ... Prefix ... Found credentials: ... admin:pass ... toto:pwd ... lorem ipsum ... lorem ipsum" >>> import regex >>> m = regex.search('Pre[\s\S]*?Found credentials:(\s*(?P<m1>\S+):(?P<m2>\S+)\s*\n)+', text) >>> matchs = m.capturesdict() >>> matchs {'m1': ['admin', 'toto'], 'm2': ['pass', 'pwd']} >>> m = regex.search('(\[v\] Trying Credentials:\s*(?P<user>\S+)\s*(?P<password>\S+)\s*\n)+', text) >>> m.capturesdict() {'user': ['Miniwick', 'Miniwick', 'Miniwick', 'Miniwick', 'Miniwick'], 'password': ['password', 'admin', '123456', 'Password1', 'Miniwick']} >>> m = regex.search('WordPress[\s\S]*?(\[v\] Trying Credentials:\s*(?P<user>\S+)\s*(?P<password>\S+)\s*\n)+', text) >>> m.capturesdict() {'user': ['Miniwick', 'Miniwick', 'Miniwick', 'Miniwick', 'Miniwick'], 'password': ['password', 'admin', '123456', 'Password1', 'Miniwick']} """ if self.service.name in creds_match.keys(): if self.tool_name in creds_match[self.service.name].keys(): p = creds_match[self.service.name][self.tool_name] for pattern in p.keys(): # Important: Multiple search/match #m = re.search(pattern, self.cmd_output, re.IGNORECASE|re.DOTALL) logger.debug('Search for creds pattern: {pattern}'.format( pattern=pattern)) if 'user' not in p[pattern]: logger.smarterror('Invalid matchstring for service={service}, ' \ ' tool={tool}: Missing "user" key'.format( service=self.service.name, tool=self.tool_name)) continue # Matching method if 'meth' in p[pattern] \ and p[pattern]['meth'] in ('finditer', 'search'): method = p[pattern]['meth'] else: method = 'finditer' # Perform regexp matching try: if method == 'finditer': m = re.finditer(pattern, self.cmd_output, re.IGNORECASE) else: m = regex.search(pattern, self.cmd_output, regex.IGNORECASE) except Exception as e: logger.warning('Error with matchstring [{pattern}], you should ' \ 'review it. Exception: {exception}'.format( pattern=pattern, exception=e)) break if not m: continue pattern_match = False # Method "finditer" if method == 'finditer': for match in m: pattern_match = True cred = dict() # Replace tokens in user, pass, type cred['user'] = self.__replace_tokens_from_matchobj( p[pattern]['user'], match) if cred['user'] is None: continue if 'pass' in p[pattern]: cred[ 'pass'] = self.__replace_tokens_from_matchobj( p[pattern]['pass'], match) if cred['pass'] is None: continue if 'type' in p[pattern]: cred[ 'type'] = self.__replace_tokens_from_matchobj( p[pattern]['type'], match) if cred['type'] is None: continue # Add username/cred to context if 'pass' in cred: self.cu.add_credentials( username=cred.get('user'), password=cred.get('pass'), auth_type=cred.get('type')) elif 'user' in cred: self.cu.add_username( username=cred.get('user'), auth_type=cred.get('type')) # Method "search" else: pattern_match = True matchs = m.capturesdict() if 'm1' not in matchs: logger.smarterror('Invalid matchstring for ' \ 'service={service}, tool={tool}: Missing match ' \ 'group'.format( service=self.service.name, tool=self.tool_name)) return nb_groups = len(matchs['m1']) for i in range(nb_groups): cred = dict() # Replace tokens in user, pass, type cred['user'] = self.__replace_tokens_from_captdict( p[pattern]['user'], matchs, i) if cred['user'] is None: continue if 'pass' in p[pattern]: cred[ 'pass'] = self.__replace_tokens_from_captdict( p[pattern]['pass'], matchs, i) if cred['pass'] is None: continue if 'type' in p[pattern]: cred[ 'type'] = self.__replace_tokens_from_captdict( p[pattern]['type'], matchs, i) if cred['type'] is None: continue # Add username/cred to context if 'pass' in cred: self.cu.add_credentials( username=cred.get('user'), password=cred.get('pass'), auth_type=cred.get('type')) elif 'user' in cred: self.cu.add_username( username=cred.get('user'), auth_type=cred.get('type')) # If a pattern has matched, skip the next patterns if pattern_match: logger.debug('Creds pattern matches (user only)') return
def __detect_products(self): """ Detect product from command output For a given tool, and for a given product, if there are several matchstrings defined, their order is important because it stops after the first match. """ if self.service.name in products_match.keys(): for prodtype in products_match[self.service.name].keys(): p = products_match[self.service.name][prodtype] break_prodnames = False for prodname in p.keys(): if self.tool_name in p[prodname].keys(): patterns = p[prodname][self.tool_name] # List of patterns is supported (i.e. several different # patterns for a given tool) if type(patterns) == str: patterns = [patterns] for pattern in patterns: version_detection = '[VERSION]' in pattern pattern = pattern.replace('[VERSION]', VERSION_REGEXP) logger.debug( 'Search for products pattern: {pattern}'. format(pattern=pattern)) try: m = re.search(pattern, self.cmd_output, re.IGNORECASE) except Exception as e: logger.warning('Error with matchstring [{pattern}], ' \ 'you should review it. Exception: ' \ '{exception}'.format( pattern=pattern, exception=e)) break # If pattern matches cmd output, add detected product # Note: For a given product type, only one name(+version) # can be added. if m: logger.debug('Product pattern matches') # Add version if present if version_detection: try: if m.group('version') is not None: version = m.group('version') else: version = '' logger.debug( 'Version detected: {version}'. format(version=version)) except: version = '' else: version = '' # Add detected product to context self.cu.add_product(prodtype, prodname, version) # Move to next product type because only one name # (potentially with version) is supported per type. # If name not found yet, give a try to next pattern break_prodnames = True #break if break_prodnames: break
def __check_product(self, prodtype, prodname, prodversion): """ Check if a product of a given type complies with the requirements. Requirements can be based on: - the product name only, - the product name and version. Compliance checks on product names are following these rules: Product name Requirement Result None val False val1 val1,val2 True val1 val2,val3 False any None True any 'undefined' False None 'undefined' True Examples of possible context requirements on versions: any any|version_known vendor/product_name vendor/product_name|version_known vendor/product_name|7.* vendor/product_name|7.1.* vendor/product_name|>7.1 vendor/product_name|<=7.0 vendor/product_name|7.1.1 :param str prodtype: Product type :param str prodname: Product name to check (can be None) :param str prodversion: Product version number to check (can be None) """ requirement = self.products[prodtype] status = requirement is None status |= (requirement == ['undefined'] and prodname is None) if status: return True if prodname: for req_prod in requirement: req_prodname, req_prodvers = VersionUtils.extract_name_version( req_prod) logger.debug( 'Required product: type={}, name={}, version={}'.format( prodtype, req_prodname, req_prodvers)) logger.debug( 'Target product: type={}, name={}, version={}'.format( prodtype, prodname, prodversion)) # Handle case where prefixed with "!" for inversion if len(req_prodname) > 0 and req_prodname[0] == '!': inversion = True req_prodname = req_prodname[1:] else: inversion = False # When no special requirement on vendor/product_name but must be known if req_prodname.lower() == 'any': # When version can be unknown status = not req_prodvers # When the version must be known (any value) status |= (req_prodvers.lower() == 'version_known' and \ prodversion != '') # When the version is unknown status |= (req_prodvers.lower() == 'version_unknown' and \ prodversion == '') # When requirement on a defined vendor/product_name and it is matching elif req_prodname.lower() == prodname.lower(): # When no requirement on the version number status = not req_prodvers # When the version must be known but no requirement on its value # status |= (req_prodvers.lower() == 'version_known' \ # and prodversion != '') # # When the version is unknown # status |= (req_prodvers.lower() == 'version_unknown' and \ # prodversion == '') # When explicit requirement on the version number status |= VersionUtils.check_version_requirement( prodversion, req_prodvers) if inversion and not status: return True if status: return True return False
def __check_product(self, prodtype, prodname, prodversion): """ Check if a product of a given type complies with the requirements. Requirements can be based on: - the product name only, - the product name and version. Compliance checks on product names are following these rules: Product name Requirement Result None val False val1 val1,val2 True val1 val2,val3 False any None True any 'undefined' False None 'undefined' True Examples of possible context requirements on versions: any any|version_known vendor/product_name vendor/product_name|version_known vendor/product_name|7.* vendor/product_name|7.1.* vendor/product_name|>7.1 vendor/product_name|<=7.0 vendor/product_name|7.1.1 :param str prodtype: Product type :param str prodname: Product name to check (can be None) :param str prodversion: Product version number to check (can be None) """ requirement = self.products[prodtype] status = requirement is None status |= (requirement == ['undefined'] and prodname is None) if status: return True try: if prodname: for req_prod in requirement: req_prodname, req_prodvers = VersionUtils.extract_name_version(req_prod) logger.debug('Required product: type={}, name={}, version={}'.format( prodtype, req_prodname, req_prodvers)) logger.debug('Target product: type={}, name={}, version={}'.format( prodtype, prodname, prodversion)) # Handle case where prefixed with "!" for inversion if len(req_prodname) > 0 and req_prodname[0] == '!': inversion = True req_prodname = req_prodname[1:] else: inversion = False # When no special requirement on vendor/product_name but must be known if req_prodname.lower() == 'any': # When version can be unknown status = not req_prodvers # When the version must be known (any value) status |= (req_prodvers.lower() == 'version_known' and \ prodversion != '') # When the version is unknown status |= (req_prodvers.lower() == 'version_unknown' and \ prodversion == '') # When requirement on a defined vendor/product_name and it is matching elif req_prodname.lower() == prodname.lower(): # When no requirement on the version number status = not req_prodvers # When the version must be known but no requirement on its value # status |= (req_prodvers.lower() == 'version_known' \ # and prodversion != '') # # When the version is unknown # status |= (req_prodvers.lower() == 'version_unknown' and \ # prodversion == '') # When explicit requirement on the version number # Perform version requirement check only if version of product # has been detected. Otherwise, we condider it is better to # perform the check anyway in order to avoid to miss stuff status |= (prodversion != '' and \ VersionUtils.check_version_requirement( prodversion, req_prodvers)) if inversion and not status: return True if status: return True except Exception as e: logger.error('An error occured when checking product requirements: {}'.format(e)) logger.error('Following requirements syntax should be reviewed: {}'.format( requirement)) logger.warning('Product requirements are ignored for this check') return True return False