def wpscan(self, *args): (exit_code, output) = (0, "") # WPScan arguments cmd = shlex.split(self.path) + list(args) # Log wpscan command without api token log.debug("Running WPScan command: %s" % ' '.join(safe_log_wpscan_args(cmd))) # Run wpscan ------------------------------------------------------------------- try: process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=open(os.devnull, 'w')) # Append process to current process list and launch self.processes.append(process) wpscan_output, _ = process.communicate() self.processes.remove(process) try: wpscan_output = wpscan_output.decode("utf-8") except UnicodeDecodeError: wpscan_output = wpscan_output.decode("latin1") # Error when wpscan failed, except exit code 5: means the target has at least one vulnerability. # See https://github.com/wpscanteam/CMSScanner/blob/master/lib/cms_scanner/exit_code.rb if process.returncode not in [0, 5]: # Handle error err_string = "WPScan command '%s' failed with exit code %s %s" % ( ' '.join(safe_log_wpscan_args(cmd)), str( process.returncode), ". WPScan output: %s" % wpscan_output if wpscan_output else '') log.error(oneline(err_string)) else: # WPScan comamnd success log.debug("WPScan raw output:\n" + wpscan_output) (exit_code, output) = (process.returncode, wpscan_output) except (CalledProcessError) as err: # Handle error -------------------------------------------------- wpscan_output = str(err.output) err_string = "WPScan command '%s' failed with exit code %s %s\nError:\n%s" % ( ' '.join(safe_log_wpscan_args(cmd)), str(process.returncode), ". WPScan output: %s" % wpscan_output if wpscan_output else '', traceback.format_exc()) log.error(oneline(err_string)) (exit_code, output) = (err.returncode, wpscan_output) except FileNotFoundError as err: err_string = "Could not find wpscan executable. \nError:\n%s" % ( traceback.format_exc()) log.error(oneline(err_string)) (exit_code, output) = (-1, "") return ((exit_code, output))
def _wpscan(self, *args): # WPScan arguments cmd = self.wpscan_executable + list(args) # Log wpscan command without api token log.debug("Running WPScan command: %s" % " ".join(safe_log_wpscan_args(cmd))) # Run wpscan process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) # Append process to current process list and launch self.processes.append(process) wpscan_output, stderr = process.communicate() self.processes.remove(process) try: wpscan_output = wpscan_output.decode("utf-8") except UnicodeDecodeError: wpscan_output = wpscan_output.decode("latin1") """ # Error when wpscan failed, except exit code 5: means the target has at least one vulnerability. # See https://github.com/wpscanteam/CMSScanner/blob/master/lib/cms_scanner/exit_code.rb if process.returncode in [0,5]: # WPScan comamnd success log.debug("WPScan raw output:\n"+wpscan_output) # Log error else : err_string, full_err_string=self.get_full_err_string(cmd, process.returncode, wpscan_output, stderr) log.error(err_string) log.debug(full_err_string) """ return (process.returncode, wpscan_output)
def terminate_scan(self, wp_site, wp_report): # Kill process if stilla live for p in self.wpscan.processes: if (wp_site['url'] in p.args) and not p.returncode: log.info('Killing WPScan process %s' % (safe_log_wpscan_args(p.args))) p.kill() # Discard wpscan_output from report if 'wpscan_output' in wp_report: del wp_report['wpscan_output']
def get_full_err_string(cmd, returncode, wpscan_output, stderr): try: reason_short = [ line for line in wpscan_output.splitlines() if 'aborted' in line.lower() ][0].replace('"', '').strip() except IndexError: reason_short = "" full = "%s %s" % ( "\nWPScan output: %s" % wpscan_output if wpscan_output else '', "\nStandard error output: %s" % stderr if stderr else '') short = "WPScan command '%s' failed with exit code %s %s" % (' '.join( safe_log_wpscan_args(cmd)), str(returncode), reason_short) return (short, full)
def dump_config(self): bump_conf=copy.deepcopy(self.conf) string='' for k in bump_conf: v=bump_conf[k] if k == 'wpscan_args': v=safe_log_wpscan_args(v) if k == 'smtp_pass' and bump_conf[k] != "" : v = '***' if isinstance(v, (list, dict)): v=json.dumps(v) else: v=str(v) string+=("\n{:<25}\t=\t{}".format(k,v)) return(string)
def dump_config(conf): '''Print the config without passwords''' dump_conf = copy.deepcopy(conf) string = '' for k in dump_conf: v = dump_conf[k] if k == 'wpscan_args': v = safe_log_wpscan_args(v) if k == 'smtp_pass' and v != "": v = '***' if isinstance(v, (list, dict)): v = json.dumps(v) else: v = str(v) string += ("\n{:<25}\t=\t{}".format(k, v)) return (string)
def dump_config(conf): """Print the config without passwords""" dump_conf = copy.deepcopy(conf) string = "" for k in dump_conf: v = dump_conf[k] if k == "wpscan_args": v = safe_log_wpscan_args(v) if k == "smtp_pass" and v != "": v = "***" if isinstance(v, (list, dict)): v = json.dumps(v) else: v = str(v) string += "\n{:<25}\t=\t{}".format(k, v) return string
def __repr__(self) -> str: """Get the config representation without passwords, ready for printing. """ dump_conf = copy.deepcopy(self) string = "" for k in dump_conf: v = dump_conf[k] if k == "wpscan_args": v = safe_log_wpscan_args(v) if k == "smtp_pass" and v != "": v = "***" if isinstance(v, (list, dict)): v = json.dumps(v) else: v = str(v) string += f"\n{k:<25}\t=\t{v}" return string
def _wpscan_site(self, wp_site, wp_report): # WPScan arguments wpscan_arguments = self.wpscan_args + wp_site['wpscan_args'] + [ '--url', wp_site['url'] ] # Output log.info("Scanning site %s" % wp_site['url']) # Launch WPScan (wpscan_exit_code, wp_report["wpscan_output"]) = self.wpscan.wpscan(*wpscan_arguments) # Exit code 0: all ok. Exit code 5: Vulnerable. Other exit code are considered as errors if wpscan_exit_code in [0, 5]: # Call parse_result from parser.py log.debug("Parsing WPScan output") try: wp_report['infos'], wp_report['warnings'], wp_report[ 'alerts'] = parse_results( wp_report['wpscan_output'], self.false_positive_strings + wp_site['false_positive_strings'] + ['No WPVulnDB API Token given']) wp_report['errors'] = [] # clear errors if any except Exception as err: err_str = "Could not parse WPScan output for site %s\n%s" % ( wp_site['url'], traceback.format_exc()) log.error(err_str) raise RuntimeError(err_str) from err else: return wp_report # Handle scan errors ----- # Quick return if interrupting and/or if user cacelled scans if self.interrupting or wpscan_exit_code in [2, -2, -9]: return None # Other errors codes : -9, -2, 127, etc: # or wpscan_exit_code not in [1,3,4] # If WPScan error, add the error to the reports # This types if errors will be written into the Json database file exit codes 1,3,4 err_str = "WPScan failed with exit code %s. \nWPScan arguments: %s. \nWPScan output: \n%s" % ( (wpscan_exit_code, safe_log_wpscan_args(wpscan_arguments), wp_report['wpscan_output'])) raise RuntimeError(err_str)
def wpscan_site(self, wp_site, wp_report): '''Timeout wrapper arround `WPWatcherScanner._wpscan_site()` Launch WPScan. Returns filled wp_report or None ''' try: wp_report_new= timeout(self.scan_timeout.total_seconds(), self._wpscan_site, args=(wp_site, wp_report) ) if wp_report_new: wp_report.update(wp_report_new) else : return None except TimeoutError: wp_report['error']+="Timeout scanning site after %s seconds.\nSetup scan_timeout in config file to allow more time"%self.scan_timeout.total_seconds() log.error("Timeout scanning site %s after %s seconds. Setup scan_timeout in config file to allow more time"%(wp_site['url'], self.scan_timeout.total_seconds())) # Kill process for p in self.wpscan.processes: if ( wp_site['url'] in p.args ) and not p.returncode: log.info('Killing WPScan process %s'%(safe_log_wpscan_args(p.args))) p.kill() self.check_fail_fast() return wp_report
def _wpscan_site(self, wp_site, wp_report): '''Handled WPScan scanning , parsing, errors and reporting. Returns filled wp_report, None if interrupted or killed. Can raise RuntimeError if WPScan failed''' # WPScan arguments wpscan_arguments=self.wpscan_args+wp_site['wpscan_args']+['--url', wp_site['url']] # Output log.info("Scanning site %s"%wp_site['url'] ) # Launch WPScan (wpscan_exit_code, wp_report["wpscan_output"]) = self.wpscan.wpscan(*wpscan_arguments) log.debug("Parsing WPScan output") try: # Call parse_results_from_string from wpscan_out_parse module results = parse_results_from_string(wp_report['wpscan_output'] , self.false_positive_strings + wp_site['false_positive_strings'] + ['No WPVulnDB API Token given'] ) wp_report['infos'], wp_report['warnings'] , wp_report['alerts'], wp_report['summary'] = results['infos'], results['warnings'], results['alerts'], results['summary'] if results['error']: wp_report['error']+=results['error'] except Exception as err: err_str="Could not parse WPScan output for site %s\n%s"%(wp_site['url'],traceback.format_exc()) log.error(err_str) raise RuntimeError(err_str) from err # Exit code 0: all ok. Exit code 5: Vulnerable. Other exit code are considered as errors if wpscan_exit_code in [0,5]: return wp_report # Quick return if interrupting and/or if user cacelled scans if self.interrupting or wpscan_exit_code in [2, -2, -9]: return None # Other errors codes : 127, etc, simply raise error err_str="WPScan failed with exit code %s. \nArguments: %s. \nOutput: \n%s"%((wpscan_exit_code, safe_log_wpscan_args(wpscan_arguments), re.sub(r'(\x1b|\[[0-9][0-9]?m)','', wp_report['wpscan_output']) )) raise RuntimeError(err_str)
def _wpscan(self, *args): (exit_code, output) = (0, "") # WPScan arguments cmd = self.wpscan_executable + list(args) # Log wpscan command without api token log.debug("Running WPScan command: %s" % ' '.join(safe_log_wpscan_args(cmd))) # Run wpscan ------------------------------------------------------------------- try: process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) # Append process to current process list and launch self.processes.append(process) wpscan_output, stderr = process.communicate() self.processes.remove(process) try: wpscan_output = wpscan_output.decode("utf-8") except UnicodeDecodeError: wpscan_output = wpscan_output.decode("latin1") # Error when wpscan failed, except exit code 5: means the target has at least one vulnerability. # See https://github.com/wpscanteam/CMSScanner/blob/master/lib/cms_scanner/exit_code.rb if process.returncode in [0, 5]: # WPScan comamnd success log.debug("WPScan raw output:\n" + wpscan_output) # Log error ---- else: err_string, full_err_string = self.get_full_err_string( cmd, process.returncode, wpscan_output, stderr) log.error(err_string) log.debug(full_err_string) return ((process.returncode, wpscan_output)) except FileNotFoundError as err: err_string = "Could not find wpscan executable.\n%s" % ( traceback.format_exc()) log.error(oneline(err_string)) raise RuntimeError(err_string) from err
def scan_site(self, wp_site: Site, last_wp_report: Optional[ScanReport] = None ) -> Optional[ScanReport]: """ Orchestrate the scanning of a site. :Return: The scan report or `None` if something happened. """ # Init report variables wp_report: ScanReport = ScanReport({ "site": wp_site["url"], "datetime": datetime.now().strftime(DATE_FORMAT) }) # Launch WPScan try: # If report is None, return None right away if not self._scan_site(wp_site, wp_report): return None except RuntimeError: self._fail_scan( wp_report, f"Could not scan site {wp_site['url']} \n{traceback.format_exc()}", ) # Updating report entry with data from last scan wp_report.update_report(last_wp_report) self.log_report_results(wp_report) wpscan_command = " ".join( safe_log_wpscan_args(["wpscan"] + self.wpscan_args + wp_site["wpscan_args"] + ["--url", wp_site["url"]])) try: # Notify recepients if match triggers if self.mail.notify(wp_site, wp_report, last_wp_report, wpscan_command=wpscan_command): # Store report time wp_report["last_email"] = wp_report["datetime"] # Discard fixed items because infos have been sent wp_report["fixed"] = [] # Handle sendmail errors except (SMTPException, ConnectionRefusedError, TimeoutError): self._fail_scan( wp_report, f"Could not send mail report for site {wp_site['url']}\n{traceback.format_exc()}", ) # Send syslog if self.syslog is not None if self.syslog: try: self.syslog.emit_messages(wp_report) except Exception: self._fail_scan( wp_report, f"Could not send syslog messages for site {wp_site['url']}\n{traceback.format_exc()}", ) # Save scanned site self.scanned_sites.append(wp_site["url"]) # Discard wpscan_output from report if "wpscan_output" in wp_report: del wp_report["wpscan_output"] # Discard wpscan_parser from report if "wpscan_parser" in wp_report: del wp_report["wpscan_parser"] return wp_report
def scan_site(self, wp_site): wp_site=self.format_site(wp_site) # Init report variables wp_report={ "site":wp_site['url'], "status":None, "datetime": datetime.now().strftime('%Y-%m-%dT%H-%M-%S'), "last_email":None, "errors":[], "infos":[], "warnings":[], "alerts":[], "fixed":[], "wpscan_output":None # will be deleted } # Find last site result if any last_wp_report=[r for r in self.wp_reports if r['site']==wp_site['url']] if len(last_wp_report)>0: last_wp_report=last_wp_report[0] # Skip if the daemon mode is enabled and scan already happend in the last configured `daemon_loop_wait` if ( self.conf['daemon'] and datetime.strptime(wp_report['datetime'],'%Y-%m-%dT%H-%M-%S') - datetime.strptime(last_wp_report['datetime'],'%Y-%m-%dT%H-%M-%S') < self.conf['daemon_loop_sleep']): log.info("Daemon skipping site %s because already scanned in the last %s"%(wp_site['url'] , self.conf['daemon_loop_sleep'])) self.scanned_sites.append(None) return None else: last_wp_report=None # WPScan arguments wpscan_arguments=self.conf['wpscan_args']+wp_site['wpscan_args']+['--url', wp_site['url']] # Output log.info("Scanning site %s"%wp_site['url'] ) # Launch WPScan ------------------------------------------------------- (wpscan_exit_code, wp_report["wpscan_output"]) = self.wpscan.wpscan(*wpscan_arguments) # Exit code 0: all ok. Exit code 5: Vulnerable. Other exit code are considered as errors # Handle scan errors if wpscan_exit_code not in [0,5]: # Quick return if interrupting if self.interrupting: return None # Quick return if user cacelled scans if wpscan_exit_code in [2]: return None # Fail fast if self.conf['fail_fast']: if not self.interrupting: log.error("Failure") self.interrupt() else: return None # Interrupt will generate other errors # If WPScan error, add the error to the reports # This types if errors will be written into the Json database file if wpscan_exit_code in [1,3,4]: err_str="WPScan failed with exit code %s. \nWPScan arguments: %s. \nWPScan output: \n%s"%((wpscan_exit_code, safe_log_wpscan_args(wpscan_arguments), wp_report['wpscan_output'])) wp_report['errors'].append(err_str) log.error("Could not scan site %s"%wp_site['url']) # Try to handle error and return wp_report, handled = self.handle_wpscan_err(wp_site, wp_report) if handled: return wp_report # Other errors codes : -9, -2, 127, etc: Just return None right away else: return None # No errors with wpscan ----------------------------- else: # Write wpscan output wpscan_results_file=None if self.conf['wpscan_output_folder'] : wpscan_results_file=os.path.join(self.conf['wpscan_output_folder'], get_valid_filename('WPScan_results_%s_%s.txt' % (wp_site['url'], wp_report['datetime']))) with open(wpscan_results_file, 'w') as wpout: wpout.write(re.sub(r'(\x1b|\[[0-9][0-9]?m)','', str(wp_report['wpscan_output']))) log.debug("Parsing WPScan output") # Call parse_result from parser.py ------------------------ wp_report['infos'], wp_report['warnings'] , wp_report['alerts'] = parse_results(wp_report['wpscan_output'] , self.conf['false_positive_strings']+wp_site['false_positive_strings'] ) # Updating report entry with data from last scan if any if last_wp_report: self.update_report(wp_report, last_wp_report) # Print WPScan findings ------------------------------------------------------ for info in wp_report['infos']: log.info(oneline("** WPScan INFO %s ** %s" % (wp_site['url'], info ))) for fix in wp_report['fixed']: log.info(oneline("** FIXED %s ** %s" % (wp_site['url'], fix ))) for warning in wp_report['warnings']: log.warning(oneline("** WPScan WARNING %s ** %s" % (wp_site['url'], warning ))) for alert in wp_report['alerts']: log.critical(oneline("** WPScan ALERT %s ** %s" % (wp_site['url'], alert ))) if wpscan_results_file: log.info("WPScan results saved to file %s"%wpscan_results_file) # Report status ------------------------------------------------ if len(wp_report['errors'])>0:wp_report['status']="ERROR" elif len(wp_report['warnings'])>0 and len(wp_report['alerts']) == 0: wp_report['status']='WARNING' elif len(wp_report['alerts'])>0: wp_report['status']='ALERT' elif len(wp_report['fixed'])>0: wp_report['status']='FIXED' else: wp_report['status']='INFO' # Will print parsed readable Alerts, Warnings, etc as they will appear in email reports log.debug("\n%s\n"%(build_message(wp_report, warnings=self.conf['send_warnings'] or self.conf['send_infos'], # switches to include or not warnings and infos infos=self.conf['send_infos']))) # Notify recepients if match triggers and no errors self.notify(wp_site, wp_report, last_wp_report) # Save scanned site self.scanned_sites.append(wp_site['url']) # Discard wpscan_output from report del wp_report['wpscan_output'] # Save report in global instance database and to file when a site has been scanned self.update_and_write_wp_reports([wp_report]) # Print progress print_progress_bar(len(self.scanned_sites), len(self.conf['wp_sites'])) return(wp_report)